diff --git a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts index b9d01702e66e..da946b78a056 100644 --- a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts +++ b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts @@ -1,13 +1,83 @@ import * as core from '@actions/core'; import * as github from '@actions/github'; +import type {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types'; import {getJSONInput} from '@github/libs/ActionUtils'; import GithubUtils from '@github/libs/GithubUtils'; import GitUtils from '@github/libs/GitUtils'; +type WorkflowRun = RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']['workflow_runs'][number]; + +const BUILD_AND_DEPLOY_JOB_NAME_PREFIX = 'Build and deploy'; + +/** + * This function checks if a given release is a valid baseTag to get the PR list with `git log baseTag...endTag`. + * + * The rules are: + * - production deploys can only be compared with other production deploys + * - staging deploys can be compared with other staging deploys or production deploys. + * The reason is that the final staging release in each deploy cycle will BECOME a production release. + * For example, imagine a checklist is closed with version 9.0.20-6; that's the most recent staging deploy, but the release for 9.0.20-6 is now finalized, so it looks like a prod deploy. + * When 9.0.21-0 finishes deploying to staging, the most recent prerelease is 9.0.20-5. However, we want 9.0.20-6...9.0.21-0, + * NOT 9.0.20-5...9.0.21-0 (so that the PR CP'd in 9.0.20-6 is not included in the next checklist) + */ +async function isReleaseValidBaseForEnvironment(releaseTag: string, isProductionDeploy: boolean) { + if (!isProductionDeploy) { + return true; + } + const isPrerelease = ( + await GithubUtils.octokit.repos.getReleaseByTag({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + tag: releaseTag, + }) + ).data.prerelease; + return !isPrerelease; +} + +/** + * Was a given platformDeploy workflow run successful on at least one platform? + */ +async function wasDeploySuccessful(runID: number) { + const jobsForWorkflowRun = ( + await GithubUtils.octokit.actions.listJobsForWorkflowRun({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // eslint-disable-next-line @typescript-eslint/naming-convention + run_id: runID, + filter: 'latest', + }) + ).data.jobs; + return jobsForWorkflowRun.some((job) => job.name.startsWith(BUILD_AND_DEPLOY_JOB_NAME_PREFIX) && job.conclusion === 'success'); +} + +/** + * This function checks if a given deploy workflow is a valid basis for comparison when listing PRs merged between two versions. + * It returns the reason a version should be skipped, or an empty string if the version should not be skipped. + */ +async function shouldSkipVersion(lastSuccessfulDeploy: WorkflowRun, inputTag: string, isProductionDeploy: boolean): Promise { + if (!lastSuccessfulDeploy?.head_branch) { + // This should never happen. Just doing this to appease TS. + return ''; + } + + // we never want to compare a tag with itself. This check is necessary because prod deploys almost always have the same version as the last staging deploy. + // In this case, the next for wrong environment fails because the release that triggered that staging deploy is now finalized, so it looks like a prod deploy. + if (lastSuccessfulDeploy?.head_branch === inputTag) { + return `Same as input tag ${inputTag}`; + } + if (!(await isReleaseValidBaseForEnvironment(lastSuccessfulDeploy?.head_branch, isProductionDeploy))) { + return 'Was a staging deploy, we only want to compare with other production deploys'; + } + if (!(await wasDeploySuccessful(lastSuccessfulDeploy.id))) { + return 'Was an unsuccessful deploy, nothing was deployed in that version'; + } + return ''; +} + async function run() { try { const inputTag = core.getInput('TAG', {required: true}); - const isProductionDeploy = getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false); + const isProductionDeploy = !!getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false); const deployEnv = isProductionDeploy ? 'production' : 'staging'; console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`); @@ -27,33 +97,26 @@ async function run() { // Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully let lastSuccessfulDeploy = completedDeploys.shift(); - while ( - lastSuccessfulDeploy?.head_branch && - (( - await GithubUtils.octokit.repos.getReleaseByTag({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - tag: lastSuccessfulDeploy.head_branch, - }) - ).data.prerelease === isProductionDeploy || - !( - await GithubUtils.octokit.actions.listJobsForWorkflowRun({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - // eslint-disable-next-line @typescript-eslint/naming-convention - run_id: lastSuccessfulDeploy.id, - filter: 'latest', - }) - ).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success')) - ) { - console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`); - lastSuccessfulDeploy = completedDeploys.shift(); - } if (!lastSuccessfulDeploy) { throw new Error('Could not find a prior successful deploy'); } + let reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy); + while (lastSuccessfulDeploy && reason) { + console.log( + `Deploy of tag ${lastSuccessfulDeploy.head_branch} was not valid as a base for comparison, looking at the next one. Reason: ${reason}`, + lastSuccessfulDeploy.html_url, + ); + lastSuccessfulDeploy = completedDeploys.shift(); + + if (!lastSuccessfulDeploy) { + throw new Error('Could not find a prior successful deploy'); + } + + reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy); + } + const priorTag = lastSuccessfulDeploy.head_branch; console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`); const prList = await GitUtils.getPullRequestsMergedBetween(priorTag ?? '', inputTag); diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 05ae086fcc24..e8bd7057d40e 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -11502,10 +11502,68 @@ const github = __importStar(__nccwpck_require__(5438)); const ActionUtils_1 = __nccwpck_require__(6981); const GithubUtils_1 = __importDefault(__nccwpck_require__(9296)); const GitUtils_1 = __importDefault(__nccwpck_require__(1547)); +const BUILD_AND_DEPLOY_JOB_NAME_PREFIX = 'Build and deploy'; +/** + * This function checks if a given release is a valid baseTag to get the PR list with `git log baseTag...endTag`. + * + * The rules are: + * - production deploys can only be compared with other production deploys + * - staging deploys can be compared with other staging deploys or production deploys. + * The reason is that the final staging release in each deploy cycle will BECOME a production release. + * For example, imagine a checklist is closed with version 9.0.20-6; that's the most recent staging deploy, but the release for 9.0.20-6 is now finalized, so it looks like a prod deploy. + * When 9.0.21-0 finishes deploying to staging, the most recent prerelease is 9.0.20-5. However, we want 9.0.20-6...9.0.21-0, + * NOT 9.0.20-5...9.0.21-0 (so that the PR CP'd in 9.0.20-6 is not included in the next checklist) + */ +async function isReleaseValidBaseForEnvironment(releaseTag, isProductionDeploy) { + if (!isProductionDeploy) { + return true; + } + const isPrerelease = (await GithubUtils_1.default.octokit.repos.getReleaseByTag({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + tag: releaseTag, + })).data.prerelease; + return !isPrerelease; +} +/** + * Was a given platformDeploy workflow run successful on at least one platform? + */ +async function wasDeploySuccessful(runID) { + const jobsForWorkflowRun = (await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // eslint-disable-next-line @typescript-eslint/naming-convention + run_id: runID, + filter: 'latest', + })).data.jobs; + return jobsForWorkflowRun.some((job) => job.name.startsWith(BUILD_AND_DEPLOY_JOB_NAME_PREFIX) && job.conclusion === 'success'); +} +/** + * This function checks if a given deploy workflow is a valid basis for comparison when listing PRs merged between two versions. + * It returns the reason a version should be skipped, or an empty string if the version should not be skipped. + */ +async function shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy) { + if (!lastSuccessfulDeploy?.head_branch) { + // This should never happen. Just doing this to appease TS. + return ''; + } + // we never want to compare a tag with itself. This check is necessary because prod deploys almost always have the same version as the last staging deploy. + // In this case, the next for wrong environment fails because the release that triggered that staging deploy is now finalized, so it looks like a prod deploy. + if (lastSuccessfulDeploy?.head_branch === inputTag) { + return `Same as input tag ${inputTag}`; + } + if (!(await isReleaseValidBaseForEnvironment(lastSuccessfulDeploy?.head_branch, isProductionDeploy))) { + return 'Was a staging deploy, we only want to compare with other production deploys'; + } + if (!(await wasDeploySuccessful(lastSuccessfulDeploy.id))) { + return 'Was an unsuccessful deploy, nothing was deployed in that version'; + } + return ''; +} async function run() { try { const inputTag = core.getInput('TAG', { required: true }); - const isProductionDeploy = (0, ActionUtils_1.getJSONInput)('IS_PRODUCTION_DEPLOY', { required: false }, false); + const isProductionDeploy = !!(0, ActionUtils_1.getJSONInput)('IS_PRODUCTION_DEPLOY', { required: false }, false); const deployEnv = isProductionDeploy ? 'production' : 'staging'; console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`); const completedDeploys = (await GithubUtils_1.default.octokit.actions.listWorkflowRuns({ @@ -11520,25 +11578,18 @@ async function run() { .filter((workflowRun) => workflowRun.conclusion !== 'cancelled'); // Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully let lastSuccessfulDeploy = completedDeploys.shift(); - while (lastSuccessfulDeploy?.head_branch && - ((await GithubUtils_1.default.octokit.repos.getReleaseByTag({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - tag: lastSuccessfulDeploy.head_branch, - })).data.prerelease === isProductionDeploy || - !(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - // eslint-disable-next-line @typescript-eslint/naming-convention - run_id: lastSuccessfulDeploy.id, - filter: 'latest', - })).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success'))) { - console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`); - lastSuccessfulDeploy = completedDeploys.shift(); - } if (!lastSuccessfulDeploy) { throw new Error('Could not find a prior successful deploy'); } + let reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy); + while (lastSuccessfulDeploy && reason) { + console.log(`Deploy of tag ${lastSuccessfulDeploy.head_branch} was not valid as a base for comparison, looking at the next one. Reason: ${reason}`, lastSuccessfulDeploy.html_url); + lastSuccessfulDeploy = completedDeploys.shift(); + if (!lastSuccessfulDeploy) { + throw new Error('Could not find a prior successful deploy'); + } + reason = await shouldSkipVersion(lastSuccessfulDeploy, inputTag, isProductionDeploy); + } const priorTag = lastSuccessfulDeploy.head_branch; console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`); const prList = await GitUtils_1.default.getPullRequestsMergedBetween(priorTag ?? '', inputTag); diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 0004b2d3eaf8..9f81222898ad 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -38,6 +38,7 @@ jobs: secrets: inherit android: + # WARNING: getDeployPullRequestList depends on this job name. do not change job name without adjusting that action accordingly name: Build and deploy Android needs: validateActor if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} @@ -122,6 +123,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} desktop: + # WARNING: getDeployPullRequestList depends on this job name. do not change job name without adjusting that action accordingly name: Build and deploy Desktop needs: validateActor if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} @@ -165,6 +167,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} iOS: + # WARNING: getDeployPullRequestList depends on this job name. do not change job name without adjusting that action accordingly name: Build and deploy iOS needs: validateActor if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} @@ -276,6 +279,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} web: + # WARNING: getDeployPullRequestList depends on this job name. do not change job name without adjusting that action accordingly name: Build and deploy Web needs: validateActor if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} diff --git a/android/app/build.gradle b/android/app/build.gradle index a3cff286241f..3dc717de963c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -108,8 +108,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009002101 - versionName "9.0.21-1" + versionCode 1009002200 + versionName "9.0.22-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/box.svg b/assets/images/box.svg index ba0b3c22d8a0..05f808e801ee 100644 --- a/assets/images/box.svg +++ b/assets/images/box.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/product-illustrations/todd-with-phones.svg b/assets/images/product-illustrations/todd-with-phones.svg index 5992a4f408d7..21ee5a015820 100644 --- a/assets/images/product-illustrations/todd-with-phones.svg +++ b/assets/images/product-illustrations/todd-with-phones.svg @@ -1,667 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/advanced-approvals-icon-square.svg b/assets/images/simple-illustrations/advanced-approvals-icon-square.svg index 00f3de51bd42..bd71512a9846 100644 --- a/assets/images/simple-illustrations/advanced-approvals-icon-square.svg +++ b/assets/images/simple-illustrations/advanced-approvals-icon-square.svg @@ -1,86 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/emptystate__big-vault.svg b/assets/images/simple-illustrations/emptystate__big-vault.svg index 02606e39fafd..a1d18da1b117 100644 --- a/assets/images/simple-illustrations/emptystate__big-vault.svg +++ b/assets/images/simple-illustrations/emptystate__big-vault.svg @@ -1,378 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg b/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg new file mode 100644 index 000000000000..eb2bad31620d --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/user-check.svg b/assets/images/user-check.svg index 2da67de751f4..931f4e5f6a51 100644 --- a/assets/images/user-check.svg +++ b/assets/images/user-check.svg @@ -1,9 +1 @@ - - - - + \ No newline at end of file diff --git a/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md b/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md index f761e8dacd5e..f514360e9519 100644 --- a/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md +++ b/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md @@ -30,11 +30,11 @@ You can get support any time by locating your chat with Concierge in your chat i
  • Click Default Currency to set the currency for all expenses submitted under the workspace. Expensify automatically converts all other currencies to your default currency.
  • -![Click your profile photo or icon]({{site.url}}/assets/images/ExpensifyHelp_R1_CreateWorkspace_1.png){:width="100%"} +![Click your profile photo or icon]({{site.url}}/assets/images/ExpensifyHelp_CreateWorkspace_1.png){:width="100%"} -![Click Workspaces in the left menu and New Worksapce]({{site.url}}/assets/images/ExpensifyHelp_R1_CreateWorkspace_2.png){:width="100%"} +![Click Workspaces in the left menu and New Worksapce]({{site.url}}/assets/images/ExpensifyHelp_CreateWorkspace_2.png){:width="100%"} -![Options to make changes like a custom workspace name]({{site.url}}/assets/images/ExpensifyHelp_R1_CreateWorkspace_3.png){:width="100%"} +![Options to make changes like a custom workspace name]({{site.url}}/assets/images/ExpensifyHelp_CreateWorkspace_3.png){:width="100%"} # 3. Invite members @@ -52,11 +52,11 @@ Once the invite is accepted, the new members will appear in your members list. You can also invite members on the workspace’s Profile page by clicking **Share** to share the workspace’s URL or QR code. {% include end-info.html %} -![Click Members on the left and click Invite member]({{site.url}}/assets/images/ExpensifyHelp_R1_InviteMembers_1.png){:width="100%"} +![Click Members on the left and click Invite member]({{site.url}}/assets/images/ExpensifyHelp_InviteMembers_1.png){:width="100%"} -![Use the search field to find the individual by name, email, or phone number]({{site.url}}/assets/images/ExpensifyHelp_R1_InviteMembers_2.png){:width="100%"} +![Use the search field to find the individual by name, email, or phone number]({{site.url}}/assets/images/ExpensifyHelp_InviteMembers_2.png){:width="100%"} -![Enter a custom message into the Message field]({{site.url}}/assets/images/ExpensifyHelp_R1_InviteMembers_3.png){:width="100%"} +![Enter a custom message into the Message field]({{site.url}}/assets/images/ExpensifyHelp_InviteMembers_3.png){:width="100%"} # 4. Set admins @@ -114,5 +114,7 @@ To add more features, Easily monitor when your Free Trial starts and how many days are left on your Subscription page. We’ll also notify you when your trial starts and ends, at which point you’ll add a billing card to continue using all your favorite features! + +![Hightlight the free trial start and end date]({{site.url}}/assets/images/ExpensifyHelp-FreeTrial-1.png){:width="100%"} diff --git a/docs/assets/images/Duty-of-care.png b/docs/assets/images/Duty-of-care.png index f09f8254b478..8774c0983f3f 100644 Binary files a/docs/assets/images/Duty-of-care.png and b/docs/assets/images/Duty-of-care.png differ diff --git a/docs/assets/images/ExpensifyHelp-FreeTrial-1.png b/docs/assets/images/ExpensifyHelp-FreeTrial-1.png new file mode 100644 index 000000000000..28bfcaf15847 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-FreeTrial-1.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png index 3dcf92d028ab..e221b508a799 100644 Binary files a/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png index cafb106e897e..9953bfcbd281 100644 Binary files a/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png index 08b553857110..f30ec7f9c1e3 100644 Binary files a/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_1.png b/docs/assets/images/ExpensifyHelp_InviteMembers_1.png index cba73c2ce150..12b000fbd07c 100644 Binary files a/docs/assets/images/ExpensifyHelp_InviteMembers_1.png and b/docs/assets/images/ExpensifyHelp_InviteMembers_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_2.png b/docs/assets/images/ExpensifyHelp_InviteMembers_2.png index e09b8ac5b2b0..169013014ab5 100644 Binary files a/docs/assets/images/ExpensifyHelp_InviteMembers_2.png and b/docs/assets/images/ExpensifyHelp_InviteMembers_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_3.png b/docs/assets/images/ExpensifyHelp_InviteMembers_3.png index 999e6785ae5f..c8603b7a6b3c 100644 Binary files a/docs/assets/images/ExpensifyHelp_InviteMembers_3.png and b/docs/assets/images/ExpensifyHelp_InviteMembers_3.png differ diff --git a/docs/assets/images/Export-Expenses.png b/docs/assets/images/Export-Expenses.png index 37cabda7922e..8e73577bdf62 100644 Binary files a/docs/assets/images/Export-Expenses.png and b/docs/assets/images/Export-Expenses.png differ diff --git a/docs/assets/images/QBO_classic_edit_exports.png b/docs/assets/images/QBO_classic_edit_exports.png index 037d8ac4aafa..adff19e3e60a 100644 Binary files a/docs/assets/images/QBO_classic_edit_exports.png and b/docs/assets/images/QBO_classic_edit_exports.png differ diff --git a/docs/assets/images/QBO_classic_icon.png b/docs/assets/images/QBO_classic_icon.png index fccca3bf2bce..a557b2affdf5 100644 Binary files a/docs/assets/images/QBO_classic_icon.png and b/docs/assets/images/QBO_classic_icon.png differ diff --git a/docs/assets/images/QBO_classic_report_history.png b/docs/assets/images/QBO_classic_report_history.png index 47ef39845b47..9a732b058c72 100644 Binary files a/docs/assets/images/QBO_classic_report_history.png and b/docs/assets/images/QBO_classic_report_history.png differ diff --git a/docs/assets/images/QBO_classic_troubleshooting_billable.png b/docs/assets/images/QBO_classic_troubleshooting_billable.png index 82b91e904902..876fe33dbe66 100644 Binary files a/docs/assets/images/QBO_classic_troubleshooting_billable.png and b/docs/assets/images/QBO_classic_troubleshooting_billable.png differ diff --git a/docs/assets/images/QBO_classic_troubleshooting_billable_2.png b/docs/assets/images/QBO_classic_troubleshooting_billable_2.png index cda5424cd279..5a88c48a3e5c 100644 Binary files a/docs/assets/images/QBO_classic_troubleshooting_billable_2.png and b/docs/assets/images/QBO_classic_troubleshooting_billable_2.png differ diff --git a/docs/assets/images/Travel-Analytics.png b/docs/assets/images/Travel-Analytics.png index 27f696e28bcd..c4f969b6d51c 100644 Binary files a/docs/assets/images/Travel-Analytics.png and b/docs/assets/images/Travel-Analytics.png differ diff --git a/docs/assets/images/Xero_classic_Edit_exports.png b/docs/assets/images/Xero_classic_Edit_exports.png index 4bfc8dcf70cc..25bda4a9454a 100644 Binary files a/docs/assets/images/Xero_classic_Edit_exports.png and b/docs/assets/images/Xero_classic_Edit_exports.png differ diff --git a/docs/assets/images/Xero_classic_bank_transaction.png b/docs/assets/images/Xero_classic_bank_transaction.png index ff4080a3756a..b730e65569a6 100644 Binary files a/docs/assets/images/Xero_classic_bank_transaction.png and b/docs/assets/images/Xero_classic_bank_transaction.png differ diff --git a/docs/assets/images/Xero_classic_category_icon.png b/docs/assets/images/Xero_classic_category_icon.png index ecc1b51e2548..50360ecdae54 100644 Binary files a/docs/assets/images/Xero_classic_category_icon.png and b/docs/assets/images/Xero_classic_category_icon.png differ diff --git a/docs/assets/images/Xero_classic_copy.png b/docs/assets/images/Xero_classic_copy.png index 5734b57e216d..ff67fecad252 100644 Binary files a/docs/assets/images/Xero_classic_copy.png and b/docs/assets/images/Xero_classic_copy.png differ diff --git a/docs/assets/images/Xero_classic_new_connection.png b/docs/assets/images/Xero_classic_new_connection.png index a798572a1302..bb7183262e19 100644 Binary files a/docs/assets/images/Xero_classic_new_connection.png and b/docs/assets/images/Xero_classic_new_connection.png differ diff --git a/docs/assets/images/Xero_classic_troubleshoot_category.png b/docs/assets/images/Xero_classic_troubleshoot_category.png index fb1b25070521..af6b8c2ffa0a 100644 Binary files a/docs/assets/images/Xero_classic_troubleshoot_category.png and b/docs/assets/images/Xero_classic_troubleshoot_category.png differ diff --git a/docs/assets/images/Xero_classic_troubleshoot_payment.png b/docs/assets/images/Xero_classic_troubleshoot_payment.png index 7ce1bf1ef86f..441324ebf1b1 100644 Binary files a/docs/assets/images/Xero_classic_troubleshoot_payment.png and b/docs/assets/images/Xero_classic_troubleshoot_payment.png differ diff --git a/docs/assets/images/Xero_classic_troubleshoot_remove_redo.png b/docs/assets/images/Xero_classic_troubleshoot_remove_redo.png index c8a5dac029f5..2028457c9555 100644 Binary files a/docs/assets/images/Xero_classic_troubleshoot_remove_redo.png and b/docs/assets/images/Xero_classic_troubleshoot_remove_redo.png differ diff --git a/docs/assets/images/Xero_help_01.png b/docs/assets/images/Xero_help_01.png index ce05ea83c925..ce60204866f7 100644 Binary files a/docs/assets/images/Xero_help_01.png and b/docs/assets/images/Xero_help_01.png differ diff --git a/docs/assets/images/Xero_help_02.png b/docs/assets/images/Xero_help_02.png index c2d556c7aed0..8ed2f256cf0f 100644 Binary files a/docs/assets/images/Xero_help_02.png and b/docs/assets/images/Xero_help_02.png differ diff --git a/docs/assets/images/Xero_help_03.png b/docs/assets/images/Xero_help_03.png index 30616ffd3d64..318bb274b115 100644 Binary files a/docs/assets/images/Xero_help_03.png and b/docs/assets/images/Xero_help_03.png differ diff --git a/docs/assets/images/Xero_help_04.png b/docs/assets/images/Xero_help_04.png index d0e950d3968a..b0d29bc9fde6 100644 Binary files a/docs/assets/images/Xero_help_04.png and b/docs/assets/images/Xero_help_04.png differ diff --git a/docs/assets/images/Xero_help_05.png b/docs/assets/images/Xero_help_05.png index be65e9c62960..e8265e82b652 100644 Binary files a/docs/assets/images/Xero_help_05.png and b/docs/assets/images/Xero_help_05.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index 97d05fdc3248..f662533358cc 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -1,4 +1,5 @@ sourceURL,targetURL +https://community.expensify.com,https://help.expensify.com https://community.expensify.com/discussion/5634/deep-dive-how-long-will-it-take-for-me-to-receive-my-reimbursement,https://help.expensify.com/articles/expensify-classic/expenses/reports/Reimbursements https://community.expensify.com/discussion/4925/how-to-dispute-an-expensify-card-transaction,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction https://community.expensify.com/discussion/5184/faq-how-am-i-protected-from-fraud-using-the-expensify-card,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 29a93b364afb..bf8f74590335 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.21 + 9.0.22 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.21.1 + 9.0.22.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 01f3b9f2c22c..8ff30fd4822d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.21 + 9.0.22 CFBundleSignature ???? CFBundleVersion - 9.0.21.1 + 9.0.22.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 929c1e66e8d0..6b5aa2fd9e63 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.21 + 9.0.22 CFBundleVersion - 9.0.21.1 + 9.0.22.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index c1a5b32cf3c2..8754aea533e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.21-1", + "version": "9.0.22-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.21-1", + "version": "9.0.22-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -189,6 +189,7 @@ "@types/react-collapse": "^5.0.1", "@types/react-dom": "^18.2.4", "@types/react-is": "^18.3.0", + "@types/react-native-web": "^0.0.0", "@types/react-test-renderer": "^18.0.0", "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", @@ -213,7 +214,7 @@ "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^29.4.1", + "electron": "^29.4.6", "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", @@ -17851,6 +17852,16 @@ "react-native": "*" } }, + "node_modules/@types/react-native-web": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@types/react-native-web/-/react-native-web-0.0.0.tgz", + "integrity": "sha512-WeaDnb57Z60pUVu6FO6WybA+7BAbPz83otLVbOpcPvRN2f/PIDt/9ViiXJ989QFrLhdex/Jen15xMOyO2X2L2A==", + "dev": true, + "dependencies": { + "@types/react": "*", + "react-native": "*" + } + }, "node_modules/@types/react-redux": { "version": "7.1.27", "license": "MIT", @@ -24112,11 +24123,12 @@ } }, "node_modules/electron": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.1.tgz", - "integrity": "sha512-YQvMAtdmjMF1yGfQFuO/KOmy+04SKot85NalppK/8zxKwOKrrK6dJBp+nJWteqBwRAKiasSrC1lDalF6hZct/w==", + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.6.tgz", + "integrity": "sha512-fz8ndj8cmmf441t4Yh2FDP3Rn0JhLkVGvtUf2YVMbJ5SdJPlc0JWll9jYkhh60jDKVVCr/tBAmfxqRnXMWJpzg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", diff --git a/package.json b/package.json index dd93bd493fd4..a077eae54c23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.21-1", + "version": "9.0.22-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -245,6 +245,7 @@ "@types/react-collapse": "^5.0.1", "@types/react-dom": "^18.2.4", "@types/react-is": "^18.3.0", + "@types/react-native-web": "^0.0.0", "@types/react-test-renderer": "^18.0.0", "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", @@ -269,7 +270,7 @@ "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^29.4.1", + "electron": "^29.4.6", "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", diff --git a/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch b/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch index dc45a6758d5c..38871a2e7764 100644 --- a/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch +++ b/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch @@ -1,8 +1,87 @@ diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js -index e338d90..70a59bf 100644 +index e338d90..238989f 100644 --- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js -@@ -1219,7 +1219,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -89,6 +89,7 @@ type State = { + firstVisibleItemKey: ?string, + // When > 0 the scroll position available in JS is considered stale and should not be used. + pendingScrollUpdateCount: number, ++ lastItemCount: number, + }; + + function getScrollingThreshold(threshold: number, visibleLength: number) { +@@ -404,12 +405,13 @@ class VirtualizedList extends StateSafePureComponent { + + const minIndexForVisible = + this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const itemCount = this.props.getItemCount(this.props.data); + + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), + firstVisibleItemKey: +- this.props.getItemCount(this.props.data) > minIndexForVisible ++ itemCount > minIndexForVisible + ? VirtualizedList._getItemKey(this.props, minIndexForVisible) + : null, + // When we have a non-zero initialScrollIndex, we will receive a +@@ -420,6 +422,7 @@ class VirtualizedList extends StateSafePureComponent { + this.props.initialScrollIndex > 0 + ? 1 + : 0, ++ lastItemCount: itemCount, + }; + } + +@@ -701,16 +704,15 @@ class VirtualizedList extends StateSafePureComponent { + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + const itemCount = newProps.getItemCount(newProps.data); +- if (itemCount === prevState.renderMask.numCells()) { ++ if (itemCount === prevState.renderMask.numCells() && itemCount === prevState.lastItemCount) { + return prevState; + } +- + let maintainVisibleContentPositionAdjustment: ?number = null; + const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; + const minIndexForVisible = + newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; + const newFirstVisibleItemKey = +- newProps.getItemCount(newProps.data) > minIndexForVisible ++ itemCount > minIndexForVisible + ? VirtualizedList._getItemKey(newProps, minIndexForVisible) + : null; + if ( +@@ -758,6 +760,7 @@ class VirtualizedList extends StateSafePureComponent { + maintainVisibleContentPositionAdjustment != null + ? prevState.pendingScrollUpdateCount + 1 + : prevState.pendingScrollUpdateCount, ++ lastItemCount: itemCount, + }; + } + +@@ -1159,7 +1162,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + +- componentDidUpdate(prevProps: Props) { ++ componentDidUpdate(prevProps: Props, prevState: State) { + const {data, extraData} = this.props; + if (data !== prevProps.data || extraData !== prevProps.extraData) { + // clear the viewableIndices cache to also trigger +@@ -1181,6 +1184,11 @@ class VirtualizedList extends StateSafePureComponent { + if (hiPriInProgress) { + this._hiPriInProgress = false; + } ++ ++ if (this.state.cellsAroundViewport.first !== prevState.cellsAroundViewport.first || ++ this.state.cellsAroundViewport.last !== prevState.cellsAroundViewport.last) { ++ this._maybeCallOnEdgeReached(); ++ } + } + + _cellRefs: {[string]: null | CellRenderer} = {}; +@@ -1219,7 +1227,7 @@ class VirtualizedList extends StateSafePureComponent { zoomScale: 1, }; _scrollRef: ?React.ElementRef = null; @@ -11,7 +90,7 @@ index e338d90..70a59bf 100644 _sentEndForContentLength = 0; _updateCellsToRenderBatcher: Batchinator; _viewabilityTuples: Array = []; -@@ -1550,16 +1550,16 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1550,16 +1558,16 @@ class VirtualizedList extends StateSafePureComponent { onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && diff --git a/patches/react-native+0.73.4+016+iOSCoreAnimationBorderRendering.patch b/patches/react-native+0.73.4+016+iOSCoreAnimationBorderRendering.patch index b59729e79622..478e282e387d 100644 --- a/patches/react-native+0.73.4+016+iOSCoreAnimationBorderRendering.patch +++ b/patches/react-native+0.73.4+016+iOSCoreAnimationBorderRendering.patch @@ -1,22 +1,23 @@ diff --git a/node_modules/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/node_modules/react-native/React/Fabric/Mounting/RCTMountingManager.mm -index b4cfb3d..7aa00e5 100644 +index b4cfb3d..fdfae56 100644 --- a/node_modules/react-native/React/Fabric/Mounting/RCTMountingManager.mm +++ b/node_modules/react-native/React/Fabric/Mounting/RCTMountingManager.mm -@@ -49,6 +49,9 @@ static void RCTPerformMountInstructions( - { - SystraceSection s("RCTPerformMountInstructions"); +@@ -265,6 +265,9 @@ static void RCTPerformMountInstructions( + auto surfaceId = mountingCoordinator.getSurfaceId(); + + [CATransaction begin]; + [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; + - for (const auto &mutation : mutations) { - switch (mutation.type) { - case ShadowViewMutation::Create: { -@@ -147,6 +150,7 @@ static void RCTPerformMountInstructions( - } - } - } + mountingCoordinator.getTelemetryController().pullTransaction( + [&](const MountingTransaction &transaction, const SurfaceTelemetry &surfaceTelemetry) { + [self.delegate mountingManager:self willMountComponentsWithRootTag:surfaceId]; +@@ -278,6 +281,8 @@ static void RCTPerformMountInstructions( + _observerCoordinator.notifyObserversMountingTransactionDidMount(transaction, surfaceTelemetry); + [self.delegate mountingManager:self didMountComponentsWithRootTag:surfaceId]; + }); ++ + [CATransaction commit]; } - @implementation RCTMountingManager { + - (void)setIsJSResponder:(BOOL)isJSResponder diff --git a/patches/react-native+0.73.4+024+fixMVCPAndroid.patch b/patches/react-native+0.73.4+024+fixMVCPAndroid.patch new file mode 100644 index 000000000000..fe37e38c3040 --- /dev/null +++ b/patches/react-native+0.73.4+024+fixMVCPAndroid.patch @@ -0,0 +1,334 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java +index fff761f..2cebd6b 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java +@@ -82,6 +82,7 @@ public class MaintainVisibleScrollPositionHelper currentScroll || i == contentView.getChildCount() - 1) { +- mFirstVisibleView = new WeakReference<>(child); +- Rect frame = new Rect(); +- child.getHitRect(frame); +- mPrevFirstVisibleFrame = frame; +- break; ++ if ((position > currentScroll && position < firstVisibleViewPosition) || ++ (firstVisibleView == null && i == contentView.getChildCount() - 1)) { ++ firstVisibleView = child; ++ firstVisibleViewPosition = position; ++ } ++ } ++ mFirstVisibleView = new WeakReference<>(firstVisibleView); ++ } ++ ++ private View getFirstVisibleView() { ++ return mFirstVisibleView != null ? mFirstVisibleView.get() : null; ++ } ++ ++ private void willMountItemsInternal() { ++ View firstVisibleView = getFirstVisibleView(); ++ ++ // If we don't have a first visible view because no scroll happened call onScroll ++ // to update it. ++ if (firstVisibleView == null) { ++ onScroll(); ++ firstVisibleView = getFirstVisibleView(); ++ ++ // There are cases where it is possible for this to still be null so just bail out. ++ if (firstVisibleView == null) { ++ return; + } + } ++ Rect frame = new Rect(); ++ firstVisibleView.getHitRect(frame); ++ mPrevFirstVisibleFrame = frame; + } + + // UIManagerListener +@@ -177,19 +205,19 @@ public class MaintainVisibleScrollPositionHelper minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ firstVisibleItemKey: itemCount > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, + // When we have a non-zero initialScrollIndex, we will receive a + // scroll event later so this will prevent the window from updating + // until we get a valid offset. +- pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0, ++ lastItemCount: itemCount + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -919,13 +921,13 @@ class VirtualizedList extends StateSafePureComponent { + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); +- if (itemCount === prevState.renderMask.numCells()) { ++ if (itemCount === prevState.renderMask.numCells() && itemCount === prevState.lastItemCount) { + return prevState; + } + var maintainVisibleContentPositionAdjustment = null; + var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; + var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; +- var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ var newFirstVisibleItemKey = itemCount > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; + if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { + if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { + // Fast path if items were added at the start of the list. +@@ -944,7 +946,8 @@ class VirtualizedList extends StateSafePureComponent { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), + firstVisibleItemKey: newFirstVisibleItemKey, +- pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount, ++ lastItemCount: itemCount + }; + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -1220,7 +1223,7 @@ class VirtualizedList extends StateSafePureComponent { + return ret; + } + } +- componentDidUpdate(prevProps) { ++ componentDidUpdate(prevProps, prevState) { + var _this$props7 = this.props, + data = _this$props7.data, + extraData = _this$props7.extraData; +@@ -1244,6 +1247,11 @@ class VirtualizedList extends StateSafePureComponent { + if (hiPriInProgress) { + this._hiPriInProgress = false; + } ++ ++ if (this.state.cellsAroundViewport.first !== prevState.cellsAroundViewport.first || ++ this.state.cellsAroundViewport.last !== prevState.cellsAroundViewport.last) { ++ this._maybeCallOnEdgeReached(); ++ } + } + + // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex +@@ -1407,8 +1415,8 @@ class VirtualizedList extends StateSafePureComponent { // Next check if the user just scrolled within the start threshold // and call onStartReached only once for a given content length, // and only if onEndReached is not being executed @@ -22,7 +90,7 @@ index b05da08..80aea85 100644 onStartReached({ distanceFromStart }); -@@ -1407,7 +1407,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1417,7 +1425,7 @@ class VirtualizedList extends StateSafePureComponent { // If the user scrolls away from the start or end and back again, // cause onStartReached or onEndReached to be triggered again else { @@ -32,10 +100,80 @@ index b05da08..80aea85 100644 } } diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index 459f017..799a6ee 100644 +index 459f017..d20115c 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -1325,7 +1325,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -79,6 +79,7 @@ type State = { + firstVisibleItemKey: ?string, + // When > 0 the scroll position available in JS is considered stale and should not be used. + pendingScrollUpdateCount: number, ++ lastItemCount: number, + }; + + /** +@@ -453,12 +454,13 @@ class VirtualizedList extends StateSafePureComponent { + + const minIndexForVisible = + this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const itemCount = this.props.getItemCount(this.props.data); + + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), + firstVisibleItemKey: +- this.props.getItemCount(this.props.data) > minIndexForVisible ++ itemCount > minIndexForVisible + ? VirtualizedList._getItemKey(this.props, minIndexForVisible) + : null, + // When we have a non-zero initialScrollIndex, we will receive a +@@ -469,6 +471,7 @@ class VirtualizedList extends StateSafePureComponent { + this.props.initialScrollIndex > 0 + ? 1 + : 0, ++ lastItemCount: itemCount, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -809,16 +812,15 @@ class VirtualizedList extends StateSafePureComponent { + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + const itemCount = newProps.getItemCount(newProps.data); +- if (itemCount === prevState.renderMask.numCells()) { ++ if (itemCount === prevState.renderMask.numCells() && itemCount === prevState.lastItemCount) { + return prevState; + } +- + let maintainVisibleContentPositionAdjustment: ?number = null; + const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; + const minIndexForVisible = + newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; + const newFirstVisibleItemKey = +- newProps.getItemCount(newProps.data) > minIndexForVisible ++ itemCount > minIndexForVisible + ? VirtualizedList._getItemKey(newProps, minIndexForVisible) + : null; + if ( +@@ -866,6 +868,7 @@ class VirtualizedList extends StateSafePureComponent { + maintainVisibleContentPositionAdjustment != null + ? prevState.pendingScrollUpdateCount + 1 + : prevState.pendingScrollUpdateCount, ++ lastItemCount: itemCount, + }; + } + +@@ -1285,6 +1288,11 @@ class VirtualizedList extends StateSafePureComponent { + if (hiPriInProgress) { + this._hiPriInProgress = false; + } ++ ++ if (this.state.cellsAroundViewport.first !== prevState.cellsAroundViewport.first || ++ this.state.cellsAroundViewport.last !== prevState.cellsAroundViewport.last) { ++ this._maybeCallOnEdgeReached(); ++ } + } + + _averageCellLength = 0; +@@ -1325,7 +1333,7 @@ class VirtualizedList extends StateSafePureComponent { zoomScale: 1, }; _scrollRef: ?React.ElementRef = null; @@ -44,7 +182,7 @@ index 459f017..799a6ee 100644 _sentEndForContentLength = 0; _totalCellLength = 0; _totalCellsMeasured = 0; -@@ -1675,18 +1675,18 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1675,18 +1683,18 @@ class VirtualizedList extends StateSafePureComponent { onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && diff --git a/src/CONST.ts b/src/CONST.ts index a9160e606bd9..b31bcc424c84 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2131,6 +2131,12 @@ const CONST = { NETSUITE: 'netsuite', SAGE_INTACCT: 'intacct', }, + ROUTE: { + QBO: 'quickbooks-online', + XERO: 'xero', + NETSUITE: 'netsuite', + SAGE_INTACCT: 'sage-intacct', + }, NAME_USER_FRIENDLY: { netsuite: 'NetSuite', quickbooksOnline: 'Quickbooks Online', @@ -5244,8 +5250,6 @@ const CONST = { SEARCH: { RESULTS_PAGE_SIZE: 50, DATA_TYPES: { - TRANSACTION: 'transaction', - REPORT: 'report', EXPENSE: 'expense', INVOICE: 'invoice', TRIP: 'trip', @@ -5274,9 +5278,10 @@ const CONST = { STATUS: { EXPENSE: { ALL: 'all', - SHARED: 'shared', DRAFTS: 'drafts', - FINISHED: 'finished', + OUTSTANDING: 'outstanding', + APPROVED: 'approved', + PAID: 'paid', }, INVOICE: { ALL: 'all', @@ -5291,14 +5296,6 @@ const CONST = { PAID: 'paid', }, }, - TAB: { - EXPENSE: { - ALL: 'type:expense status:all', - SHARED: 'type:expense status:shared', - DRAFTS: 'type:expense status:drafts', - FINISHED: 'type:expense status:finished', - }, - }, TABLE_COLUMNS: { RECEIPT: 'receipt', DATE: 'date', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 23b56d921c4d..b7b6cf53a176 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -571,6 +571,10 @@ const ONYXKEYS = { REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccount', PERSONAL_BANK_ACCOUNT_FORM_DRAFT: 'personalBankAccountDraft', + DISABLE_AUTO_RENEW_SURVEY_FORM: 'disableAutoRenewSurveyForm', + DISABLE_AUTO_RENEW_SURVEY_FORM_DRAFT: 'disableAutoRenewSurveyFormDraft', + REQUEST_EARLY_CANCELLATION_FORM: 'requestEarlyCancellationForm', + REQUEST_EARLY_CANCELLATION_FORM_DRAFT: 'requestEarlyCancellationFormDraft', EXIT_SURVEY_REASON_FORM: 'exitSurveyReasonForm', EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft', EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm', @@ -647,6 +651,8 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.RoomSettingsForm; [ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.NewTaskForm; [ONYXKEYS.FORMS.EDIT_TASK_FORM]: FormTypes.EditTaskForm; + [ONYXKEYS.FORMS.DISABLE_AUTO_RENEW_SURVEY_FORM]: FormTypes.FeedbackSurveyForm; + [ONYXKEYS.FORMS.REQUEST_EARLY_CANCELLATION_FORM]: FormTypes.FeedbackSurveyForm; [ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM]: FormTypes.ExitSurveyReasonForm; [ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM]: FormTypes.ExitSurveyResponseForm; [ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM]: FormTypes.MoneyRequestDescriptionForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index de495568daa3..0ece7cc316aa 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -717,11 +717,12 @@ const ROUTES = { }, WORKSPACE_ACCOUNTING_CARD_RECONCILIATION: { route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation', - getRoute: (policyID: string, connection?: ConnectionName) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation` as const, + getRoute: (policyID: string, connection?: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation` as const, }, WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: { route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation/account', - getRoute: (policyID: string, connection?: ConnectionName) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation/account` as const, + getRoute: (policyID: string, connection?: ValueOf) => + `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation/account` as const, }, WORKSPACE_CATEGORIES: { route: 'settings/workspaces/:policyID/categories', diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx index f6096e3b55e8..f4e1b992437f 100644 --- a/src/components/BlockingViews/BlockingView.tsx +++ b/src/components/BlockingViews/BlockingView.tsx @@ -1,8 +1,9 @@ import type {ImageContentFit} from 'expo-image'; import React, {useMemo} from 'react'; -import type {ImageSourcePropType, StyleProp, TextStyle, ViewStyle, WebStyle} from 'react-native'; +import type {ImageSourcePropType, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {SvgProps} from 'react-native-svg'; +import type {WebStyle} from 'react-native-web'; import type {MergeExclusive} from 'type-fest'; import AutoEmailLink from '@components/AutoEmailLink'; import Icon from '@components/Icon'; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 126c81961cee..1441266c76de 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -29,6 +29,9 @@ type ButtonProps = Partial & { /** The fill color to pass into the icon. */ iconFill?: string; + /** The fill color to pass into the icon when the button is hovered. */ + iconHoverFill?: string; + /** Any additional styles to pass to the left icon container. */ iconStyles?: StyleProp; @@ -80,9 +83,15 @@ type ButtonProps = Partial & { /** Additional text styles */ textStyles?: StyleProp; + /** Additional text styles when the button is hovered */ + textHoverStyles?: StyleProp; + /** Whether we should use the default hover style */ shouldUseDefaultHover?: boolean; + /** Additional hover styles */ + hoverStyles?: StyleProp; + /** Whether we should use the success theme color */ success?: boolean; @@ -170,6 +179,7 @@ function Button( iconRight = Expensicons.ArrowRight, iconFill, + iconHoverFill, icon = null, iconStyles = [], iconRightStyles = [], @@ -194,8 +204,10 @@ function Button( style = [], innerStyles = [], textStyles = [], + textHoverStyles = [], shouldUseDefaultHover = true, + hoverStyles = undefined, success = false, danger = false, @@ -238,6 +250,7 @@ function Button( danger && styles.buttonDangerText, !!icon && styles.textAlignLeft, textStyles, + isHovered && textHoverStyles, link && styles.link, link && isHovered && StyleUtils.getColorStyle(theme.linkHover), link && styles.fontWeightNormal, @@ -259,7 +272,7 @@ function Button( ) : ( (''); const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); @@ -352,7 +352,7 @@ function Composer( opacity: 0, }} > - + {`${valueBeforeCaret} `} + = {borderColor: theme.border}; - const [reason, setReason] = useState