diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js index 8404a46acf95..b2c9a46df078 100644 --- a/.eslintrc.changed.js +++ b/.eslintrc.changed.js @@ -24,10 +24,9 @@ module.exports = { files: [ 'src/libs/actions/IOU.ts', 'src/libs/actions/Report.ts', - 'src/libs/OptionsListUtils.ts', - 'src/pages/home/ReportScreen.tsx', 'src/pages/workspace/WorkspaceInitialPage.tsx', 'src/pages/home/report/PureReportActionItem.tsx', + 'src/libs/SidebarUtils.ts', ], rules: { 'rulesdir/no-default-id-values': 'off', diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 20fdd2990005..94e7218eec0e 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -17570,7 +17570,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -17651,6 +17653,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index dad5dcb16461..e877b5fa0f24 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12574,7 +12574,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -12655,6 +12657,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/checkAndroidStatus/index.js b/.github/actions/javascript/checkAndroidStatus/index.js index fe21cbe0f2bd..bbecafc58d10 100644 --- a/.github/actions/javascript/checkAndroidStatus/index.js +++ b/.github/actions/javascript/checkAndroidStatus/index.js @@ -736967,7 +736967,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -737048,6 +737050,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index cd96c031fd44..f1c6f1c22d7d 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -11857,7 +11857,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -11938,6 +11940,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 6a762d1b19e0..7ce63fc6367b 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -12139,7 +12139,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -12220,6 +12222,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index b85a6784562d..f09866bb4356 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -11818,7 +11818,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -11899,6 +11901,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 4426cb35aa77..fe2bef28dd88 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -12180,7 +12180,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -12261,6 +12263,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index 0b1d55c62db9..fa7e7e00e137 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11920,7 +11920,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -12001,6 +12003,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index 557f124ce03a..7813dda33ad7 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -11818,7 +11818,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -11899,6 +11901,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 0f522564a56d..3921b3d33b24 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -13217,7 +13217,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -13298,6 +13300,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 8c0f1d03fb0e..2a19afe5aadd 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -11924,7 +11924,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -12005,6 +12007,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index f247d3e6e0d8..fa07a1063e2e 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -18401,7 +18401,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -18482,6 +18484,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 419bba9fe14c..8c7aa27fad55 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -11828,7 +11828,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -11909,6 +11911,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 9473228e7269..a847be766f9b 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -11920,7 +11920,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -12001,6 +12003,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index de282ff84740..f0c627baa434 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -11860,7 +11860,9 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`; // eslint-disable-next-line max-len - issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; @@ -11941,6 +11943,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow) { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index ae74621b356a..e463f0d60fd8 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -356,7 +356,11 @@ class GithubUtils { // eslint-disable-next-line max-len issueBody += `\r\n- [${ isFirebaseChecked ? 'x' : ' ' - }] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + }] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **this release version** and verified that this release does not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; + // eslint-disable-next-line max-len + issueBody += `\r\n- [${ + isFirebaseChecked ? 'x' : ' ' + }] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) for **the previous release version** and verified that the release did not introduce any new crashes. More detailed instructions on this verification can be found [here](https://stackoverflowteams.com/c/expensify/questions/15095/15096).`; // eslint-disable-next-line max-len issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; @@ -457,6 +461,7 @@ class GithubUtils { /** * Get the most recent workflow run for the given New Expensify workflow. */ + /* eslint-disable rulesdir/no-default-id-values */ static getLatestWorkflowRunID(workflow: string | number): Promise { console.log(`Fetching New Expensify workflow runs for ${workflow}...`); return this.octokit.actions diff --git a/.github/workflows/deployNewHelp.yml b/.github/workflows/deployNewHelp.yml index 8e455979a50e..f2e4aef67dc5 100644 --- a/.github/workflows/deployNewHelp.yml +++ b/.github/workflows/deployNewHelp.yml @@ -55,7 +55,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '20.18.0' + node-version: '20.18.1' # Wil install the _help/package.js - name: Install Node.js Dependencies diff --git a/.nvmrc b/.nvmrc index 2a393af592b8..d4b7699d36ca 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.18.0 +20.18.1 diff --git a/Mobile-Expensify b/Mobile-Expensify index 7375ec0adb37..a98104e1f8bf 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 7375ec0adb378cba10edde02c2ee0e57ca9785e0 +Subproject commit a98104e1f8bfe0c39bf11f1e91634b30d42d649c diff --git a/android/app/build.gradle b/android/app/build.gradle index c827bc60785e..bced66a46e08 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009008500 - versionName "9.0.85-0" + versionCode 1009008601 + versionName "9.0.86-1" // 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/product-illustrations/emptystate__holdexpense.svg b/assets/images/product-illustrations/emptystate__holdexpense.svg new file mode 100644 index 000000000000..d00738964047 --- /dev/null +++ b/assets/images/product-illustrations/emptystate__holdexpense.svg @@ -0,0 +1,1207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__realtimereports.svg b/assets/images/simple-illustrations/simple-illustration__realtimereports.svg new file mode 100644 index 000000000000..40fc3082a028 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__realtimereports.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__stopwatch.svg b/assets/images/simple-illustrations/simple-illustration__stopwatch.svg new file mode 100644 index 000000000000..c348fd73337b --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__stopwatch.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index c7f05e661bd2..4092029c3ca8 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -503,6 +503,8 @@ Here are some common cases you may face when fixing your code to remove the old/ #### **Case 1**: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. +##### 1. Utility function + ```diff -Report.getNewerActions(newestActionCurrentReport?.reportID ?? '-1', newestActionCurrentReport?.reportActionID ?? '-1'); +Report.getNewerActions(newestActionCurrentReport?.reportID, newestActionCurrentReport?.reportActionID); @@ -520,6 +522,50 @@ We need to change `Report.getNewerActions()` arguments to allow `undefined`. By + } ``` +##### 2. `ROUTES.ts`'s `getRoute()` function + +```diff +-Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policyID ?? '-1')) ++Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policyID)) +``` + +> error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. + +We need to change the `getRoute()` `policyID` argument type to allow `undefined` to fix the TS error. Besides that, we *must* add a warning log if a user tries to navigate without the `policyID`. The log will help to catch and investigate cases of navigation with invalid IDs in the future. + +```diff +WORKSPACE_PROFILE_ADDRESS: { + route: 'settings/workspaces/:policyID/profile/address', +- getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo), ++ getRoute: (policyID: string | undefined, backTo?: string) => { ++ if (!policyID) { ++ Log.warn("Invalid policyID is used to build the WORKSPACE_PROFILE_ADDRESS route") ++ } ++ return getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo); ++ }, +}, +``` + +##### Important Note: + +When you change the function id argument type to allow `undefined`, check if it makes sense to call the function without the argument. If so, you can type the argument as optional. Otherwise, please use explicit `undefined` typing to identify that the parameter should **always** be passed, even if it's `undefined`. + +``` ts +// BAD +-function getReport(reportID: string) { ++function getReport(reportID?: string) { // it doesn't makes sense to call getReport() without reportID, so it can't be optional + +-function findLastAccessedReport(excludeReportID: string) { ++function findLastAccessedReport(excludeReportID: string | undefined) { // you can call findLastAccessedReport() without excludeReportID, so it can't be the required param + +// GOOD +-function getReport(reportID: string) { ++function getReport(reportID: string | undefined) { + +-function findLastAccessedReport(excludeReportID: string) { ++function findLastAccessedReport(excludeReportID?: string) { +``` + #### **Case 2**: Type 'undefined' cannot be used as an index type. ```diff diff --git a/docs/articles/new-expensify/expenses-&-payments/Pay-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Pay-an-expense.md index 5d2b634e8032..6e1285be402a 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Pay-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Pay-an-expense.md @@ -28,6 +28,8 @@ To pay an expense, {% include end-option.html %} {% include end-selector.html %} +![Click Pay Button]({{site.url}}/assets/images/Reports_PayExpense_02.png){:width="100%"} + # Pay back friends and family You'll need to [set up your wallet](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Set-up-your-wallet) to send and receive personal payments within Expensify. The wallet is currently available to customers in the US-only. diff --git a/docs/assets/images/commfeed/commfeed-01.png b/docs/assets/images/commfeed/commfeed-01.png new file mode 100644 index 000000000000..554fb0466235 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-01.png differ diff --git a/docs/assets/images/commfeed/commfeed-02.png b/docs/assets/images/commfeed/commfeed-02.png new file mode 100644 index 000000000000..6de00be01943 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-02.png differ diff --git a/docs/assets/images/commfeed/commfeed-03.png b/docs/assets/images/commfeed/commfeed-03.png new file mode 100644 index 000000000000..505c48297f28 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-03.png differ diff --git a/docs/assets/images/commfeed/commfeed-04.png b/docs/assets/images/commfeed/commfeed-04.png new file mode 100644 index 000000000000..53ff5063462a Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-04.png differ diff --git a/docs/assets/images/commfeed/commfeed-05.png b/docs/assets/images/commfeed/commfeed-05.png new file mode 100644 index 000000000000..611bd1c071f3 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-05.png differ diff --git a/docs/assets/images/commfeed/commfeed-06.png b/docs/assets/images/commfeed/commfeed-06.png new file mode 100644 index 000000000000..05264ab38bb6 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-06.png differ diff --git a/docs/assets/images/commfeed/commfeed-07.png b/docs/assets/images/commfeed/commfeed-07.png new file mode 100644 index 000000000000..1b9c6e362c61 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-07.png differ diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 1131589c72ec..c8d825589bfb 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 7FD73CA22B23CE9500420AF3 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 59A21B2405370FDDD847C813 /* libPods-NewExpensify.a */; }; 9E17CB36A6B22BDD4BE53561 /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9196A72C11B91A52A43D6E8A /* libPods-NotificationServiceExtension.a */; }; + AC131FBB2CF634F20010CE80 /* BackgroundTasks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC131FBA2CF634F20010CE80 /* BackgroundTasks.framework */; }; ACA597C323AA39404655647F /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EF33B19FC6A7FE676839430D /* libPods-NewExpensify-NewExpensifyTests.a */; }; BDB853621F354EBB84E619C2 /* ExpensifyNewKansas-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */; }; D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; }; @@ -141,6 +142,7 @@ 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyNeue-BoldItalic.otf"; sourceTree = ""; }; 8EFE0319D586C1078DB926FD /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; 9196A72C11B91A52A43D6E8A /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + AC131FBA2CF634F20010CE80 /* BackgroundTasks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = BackgroundTasks.framework; path = System/Library/Frameworks/BackgroundTasks.framework; sourceTree = SDKROOT; }; BBE493797E97F2995E627244 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugadhoc.xcconfig"; sourceTree = ""; }; BCD444BEDDB0AF1745B39049 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-NewExpensify-NewExpensifyTests/ExpoModulesProvider.swift"; sourceTree = ""; }; BF6A4C5167244B9FB8E4D4E3 /* ExpensifyNeue-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Italic.otf"; path = "../assets/fonts/native/ExpensifyNeue-Italic.otf"; sourceTree = ""; }; @@ -180,6 +182,7 @@ 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */, E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, + AC131FBB2CF634F20010CE80 /* BackgroundTasks.framework in Frameworks */, 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -239,6 +242,7 @@ 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( + AC131FBA2CF634F20010CE80 /* BackgroundTasks.framework */, 383643672B6D4AE2005BB9AE /* DeviceCheck.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, diff --git a/ios/NewExpensify/AppDelegate.mm b/ios/NewExpensify/AppDelegate.mm index 5608c44823f4..5d419f5a623e 100644 --- a/ios/NewExpensify/AppDelegate.mm +++ b/ios/NewExpensify/AppDelegate.mm @@ -9,6 +9,8 @@ #import "RCTBootSplash.h" #import "RCTStartupTimer.h" #import +#import +#import @interface AppDelegate () @@ -49,6 +51,8 @@ - (BOOL)application:(UIApplication *)application [UIApplication sharedApplication].applicationIconBadgeNumber = 0; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isFirstRunComplete"]; } + + [RNBackgroundTaskManager setup]; return YES; } diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ccdfc4df2a9d..caa3981385d6 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + com.chat.expensify.backgroundTaskSync + CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -19,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.85 + 9.0.86 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +44,7 @@ CFBundleVersion - 9.0.85.0 + 9.0.86.1 FullStory OrgId @@ -97,6 +101,8 @@ UIBackgroundModes remote-notification + fetch + processing UIFileSharingEnabled diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0c15e3ac20b0..e34bc72ed760 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.85 + 9.0.86 CFBundleSignature ???? CFBundleVersion - 9.0.85.0 + 9.0.86.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 98698be4f00a..ef864d63538b 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.85 + 9.0.86 CFBundleVersion - 9.0.85.0 + 9.0.86.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f516193d5246..4ddb40b55336 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -35,6 +35,27 @@ PODS: - EXImageLoader (5.0.0): - ExpoModulesCore - React-Core + - expensify-react-native-background-task (0.0.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - Expo (52.0.14): - ExpoModulesCore - ExpoAsset (11.0.1): @@ -274,15 +295,15 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - MapboxCommon (23.10.1) - - MapboxCoreMaps (10.18.0): - - MapboxCommon (~> 23.10) - - MapboxMaps (10.18.2): - - MapboxCommon (= 23.10.1) - - MapboxCoreMaps (= 10.18.0) - - MapboxMobileEvents (= 1.0.10) + - MapboxCommon (23.11.2) + - MapboxCoreMaps (10.19.2): + - MapboxCommon (~> 23.11) + - MapboxMaps (10.19.1): + - MapboxCommon (= 23.11.2) + - MapboxCoreMaps (= 10.19.2) + - MapboxMobileEvents (= 2.0.0) - Turf (= 2.8.0) - - MapboxMobileEvents (1.0.10) + - MapboxMobileEvents (2.0.0) - nanopb (2.30908.0): - nanopb/decode (= 2.30908.0) - nanopb/encode (= 2.30908.0) @@ -2517,16 +2538,16 @@ PODS: - Yoga - RNLocalize (2.2.6): - React-Core - - rnmapbox-maps (10.1.30): - - MapboxMaps (~> 10.18.2) + - rnmapbox-maps (10.1.33): + - MapboxMaps (~> 10.19.0) - React - React-Core - - rnmapbox-maps/DynamicLibrary (= 10.1.30) + - rnmapbox-maps/DynamicLibrary (= 10.1.33) - Turf - - rnmapbox-maps/DynamicLibrary (10.1.30): + - rnmapbox-maps/DynamicLibrary (10.1.33): - DoubleConversion - hermes-engine - - MapboxMaps (~> 10.18.2) + - MapboxMaps (~> 10.19.0) - RCT-Folly - RCTRequired - RCTTypeSafety @@ -2813,6 +2834,7 @@ DEPENDENCIES: - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EXAV (from `../node_modules/expo-av/ios`) - EXImageLoader (from `../node_modules/expo-image-loader/ios`) + - "expensify-react-native-background-task (from `../node_modules/@expensify/react-native-background-task`)" - Expo (from `../node_modules/expo`) - ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoFont (from `../node_modules/expo-font/ios`) @@ -2982,6 +3004,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-av/ios" EXImageLoader: :path: "../node_modules/expo-image-loader/ios" + expensify-react-native-background-task: + :path: "../node_modules/@expensify/react-native-background-task" Expo: :path: "../node_modules/expo" ExpoAsset: @@ -3228,6 +3252,7 @@ SPEC CHECKSUMS: DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 EXAV: 9773c9799767c9925547b05e41a26a0240bb8ef2 EXImageLoader: 759063a65ab016b836f73972d3bb25404888713d + expensify-react-native-background-task: 6f797cf470b627912c246514b1631a205794775d Expo: 0e7b52be71a24a38d5e919e3040d8f51a8739cd0 ExpoAsset: 8138f2a9ec55ae1ad7c3871448379f7d97692d15 ExpoFont: 7522d869d84ee2ee8093ee997fef5b86f85d856b @@ -3260,10 +3285,10 @@ SPEC CHECKSUMS: libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 lottie-ios: 3d98679b41fa6fd6aff2352b3953dbd3df8a397e lottie-react-native: e50d25429207f95045253c19fa7463e288594f87 - MapboxCommon: 0ff437e44988da6856e280d00ffb266564ed0487 - MapboxCoreMaps: f1bd9405f5b9d3e343f2fe4138775299699a22fb - MapboxMaps: e76b14f52c54c40b76ddecd04f40448e6f35a864 - MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 + MapboxCommon: 873b75dd0e8c5d7029e0c849437eba365f4887e5 + MapboxCoreMaps: 35685edba03e44468aed57c3dfd7f8795edafda8 + MapboxMaps: 05822ab0ee74f7d626e6471572439afe35c1c116 + MapboxMobileEvents: d044b9edbe0ec7df60f6c2c9634fe9a7f449266b nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af onfido-react-native-sdk: 4ccfdeb10f9ccb4a5799d2555cdbc2a068a42c0d @@ -3360,7 +3385,7 @@ SPEC CHECKSUMS: RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 RNLiveMarkdown: 19826569be35bada5c0f21a0c48b5bc780051501 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 - rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 + rnmapbox-maps: d184c8d3213acf4c97ec71fbbb6f9d4954552d80 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5 RNReanimated: 1b622aa38321705e0865a0c156a2bdf0a85d7ebc diff --git a/ios/tmp.xcconfig b/ios/tmp.xcconfig new file mode 100644 index 000000000000..ee98b7b0bd8c --- /dev/null +++ b/ios/tmp.xcconfig @@ -0,0 +1,12 @@ +NEW_EXPENSIFY_URL=https:/$()/new.expensify.com/ +SECURE_EXPENSIFY_URL=https:/$()/secure.expensify.com/ +EXPENSIFY_URL=https:/$()/www.expensify.com/ +EXPENSIFY_PARTNER_NAME=chat-expensify-com +EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66 +PUSHER_APP_KEY=268df511a204fbb60884 +USE_WEB_PROXY=false +ENVIRONMENT=production +SEND_CRASH_REPORTS=true +FB_API_KEY=AIzaSyBrLKgCuo6Vem6Xi5RPokdumssW8HaWBow +FB_APP_ID=1:1008697809946:web:08de4ecb7656b7235445a3 +FB_PROJECT_ID=expensify-mobile-app diff --git a/jest/setup.ts b/jest/setup.ts index c575054f7dac..4db3d945ad8f 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -83,6 +83,11 @@ jest.mock('@src/libs/actions/Timing', () => ({ end: jest.fn(), })); +jest.mock('../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ + defineTask: jest.fn(), + onBackgroundTaskExecution: jest.fn(), +})); + // This makes FlatList render synchronously for easier testing. jest.mock( '@react-native/virtualized-lists/Interaction/Batchinator', diff --git a/modules/background-task/expensify-react-native-background-task.podspec b/modules/background-task/expensify-react-native-background-task.podspec new file mode 100644 index 000000000000..207cd239c463 --- /dev/null +++ b/modules/background-task/expensify-react-native-background-task.podspec @@ -0,0 +1,20 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' + +Pod::Spec.new do |s| + s.name = "expensify-react-native-background-task" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => ".git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,cpp}" + + install_modules_dependencies(s) +end diff --git a/modules/background-task/ios/RNBackgroundTaskManager.h b/modules/background-task/ios/RNBackgroundTaskManager.h new file mode 100644 index 000000000000..38a8a88e734d --- /dev/null +++ b/modules/background-task/ios/RNBackgroundTaskManager.h @@ -0,0 +1,15 @@ +#import +#import +#import + +@interface RNBackgroundTaskManager : NSObject + +@property (nonatomic, copy) void (^ _Nullable taskHandler)(BGTask * _Nonnull); + ++ (instancetype _Nullable )shared; ++ (void)setup; +- (void)setHandlerForIdentifier:(NSString *_Nullable)identifier + completion:(void (^_Nullable)(BGTask * _Nonnull))handler; +- (void (^_Nullable)(BGTask * _Nonnull))handlerForIdentifier:(NSString *_Nullable)identifier; + +@end diff --git a/modules/background-task/ios/RNBackgroundTaskManager.m b/modules/background-task/ios/RNBackgroundTaskManager.m new file mode 100644 index 000000000000..f0bc85dfaa9f --- /dev/null +++ b/modules/background-task/ios/RNBackgroundTaskManager.m @@ -0,0 +1,52 @@ +#import + +@implementation RNBackgroundTaskManager : NSObject { + NSMutableDictionary *_handlers; +} + ++ (instancetype)shared { + static RNBackgroundTaskManager *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[RNBackgroundTaskManager alloc] init]; + }); + return instance; +} + +- (instancetype)init { + if (self = [super init]) { + _handlers = [NSMutableDictionary new]; + } + return self; +} + +- (void)setHandlerForIdentifier:(NSString *)identifier + completion:(void (^)(BGTask * _Nonnull))handler { + _handlers[identifier] = handler; +} + +- (void (^)(BGTask * _Nonnull))handlerForIdentifier:(NSString *)identifier { + return _handlers[identifier]; +} + ++ (void)setup { + NSArray *backgroundIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; + + if (!backgroundIdentifiers || ![backgroundIdentifiers isKindOfClass:[NSArray class]]) { + NSLog(@"[ReactNativeBackgroundTask] No background identifiers found or invalid format"); + } else { + for (NSString *identifier in backgroundIdentifiers) { + [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:identifier + usingQueue:nil + launchHandler:^(BGTask * _Nonnull task) { + NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", task.identifier); + void (^handler)(BGTask * _Nonnull) = [[RNBackgroundTaskManager shared] handlerForIdentifier:task.identifier]; + if (handler) { + handler(task); + } + }]; + } + } +} + +@end diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.h b/modules/background-task/ios/ReactNativeBackgroundTask.h new file mode 100644 index 000000000000..93d1e83a6878 --- /dev/null +++ b/modules/background-task/ios/ReactNativeBackgroundTask.h @@ -0,0 +1,18 @@ +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNReactNativeBackgroundTaskSpec.h" +#import + +@interface ReactNativeBackgroundTask : NativeReactNativeBackgroundTaskSpecBase +#else +#import +#import + +@interface ReactNativeBackgroundTask : NSObject +#endif + +- (void)defineTask:(NSString *)taskName + taskExecutor:(RCTResponseSenderBlock)taskExecutor + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; + +@end diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm new file mode 100644 index 000000000000..f963c928a41e --- /dev/null +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -0,0 +1,117 @@ +#import "ReactNativeBackgroundTask.h" +#import +#import +#import "RNBackgroundTaskManager.h" + +@implementation ReactNativeBackgroundTask { + NSMutableDictionary *_taskExecutors; +} + +RCT_EXPORT_MODULE() + +- (instancetype)init { + if (self = [super init]) { + _taskExecutors = [NSMutableDictionary new]; + } + return self; +} + +- (BOOL)scheduleNewBackgroundTask:(NSString *)identifier error:(NSError **)outError { + BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:identifier]; + + // Set earliest begin date to some time in the future + request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now + + NSError *error = nil; + BOOL success = [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]; + + if (!success) { + NSLog(@"[ReactNativeBackgroundTask] Failed to schedule task: %@", error.localizedDescription); + if (outError != nil) { + *outError = error; + } + } + + return success; +} + +RCT_EXPORT_METHOD(defineTask:(NSString *)taskName + taskExecutor:(RCTResponseSenderBlock)taskExecutor + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + if (taskName == nil) { + NSLog(@"[ReactNativeBackgroundTask] Failed to define task: taskName is nil"); + reject(@"ERR_INVALID_TASK_NAME", @"Task name must be provided", nil); + return; + } + + if (taskExecutor == nil) { + NSLog(@"[ReactNativeBackgroundTask] Failed to define task: taskExecutor is nil"); + reject(@"ERR_INVALID_TASK_EXECUTOR", @"Task executor must be provided", nil); + return; + } + + NSArray *backgroundIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; + + + if (!backgroundIdentifiers || ![backgroundIdentifiers isKindOfClass:[NSArray class]]) { + NSLog(@"[ReactNativeBackgroundTask] No background identifiers found or invalid format"); + reject(@"ERR_INVALID_TASK_SCHEDULER_IDENTIFIER", @"No background identifiers found or invalid format", nil); + return; + } + + NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); + + BOOL allSuccess = YES; + NSError *taskError = nil; + + for (NSString *identifier in backgroundIdentifiers) { + [[RNBackgroundTaskManager shared] setHandlerForIdentifier:identifier completion:^(BGTask * _Nonnull task) { + NSLog(@"[ReactNativeBackgroundTask] Executing background task's handler"); + + // Execute all registered tasks + [self->_taskExecutors enumerateKeysAndObjectsUsingBlock:^(NSString *taskName, RCTResponseSenderBlock executor, BOOL *stop) { + NSLog(@"[ReactNativeBackgroundTask] Executing task: %@", taskName); + [self emitOnBackgroundTaskExecution:(taskName)]; + }]; + + NSError *scheduleError = nil; + [self scheduleNewBackgroundTask:identifier error:&scheduleError]; + + [task setTaskCompletedWithSuccess:YES]; + }]; + + NSError *scheduleError = nil; + BOOL success = [self scheduleNewBackgroundTask:identifier error:&scheduleError]; + + if (success) { + _taskExecutors[taskName] = taskExecutor; + } else { + allSuccess = NO; + taskError = scheduleError; + break; + } + } + + if (allSuccess) { + resolve(@YES); + } else { + reject(@"ERR_SCHEDULE_TASK_FAILED", + taskError.localizedDescription ?: @"Failed to schedule initial background task", + taskError); + } + + _taskExecutors[taskName] = taskExecutor; +} + +// Don't compile this code when we build for the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif + +@end diff --git a/modules/background-task/package.json b/modules/background-task/package.json new file mode 100644 index 000000000000..92d464777bae --- /dev/null +++ b/modules/background-task/package.json @@ -0,0 +1,19 @@ +{ + "name": "@expensify/react-native-background-task", + "version": "0.0.0", + "description": "Execute tasks in background", + "main": "src/index", + "codegenConfig": { + "name": "RNReactNativeBackgroundTaskSpec", + "type": "modules", + "jsSrcsDir": "src" + }, + "author": " <> ()", + "license": "UNLICENSED", + "homepage": "#readme", + "create-react-native-library": { + "type": "module-mixed", + "languages": "kotlin-objc", + "version": "0.44.1" + } +} diff --git a/modules/background-task/react-native.config.js b/modules/background-task/react-native.config.js new file mode 100644 index 000000000000..d532440e69b0 --- /dev/null +++ b/modules/background-task/react-native.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('@react-native-community/cli-types').UserDependencyConfig} + */ +module.exports = { + dependency: { + platforms: { + android: null, + }, + }, +}; diff --git a/modules/background-task/src/NativeReactNativeBackgroundTask.ts b/modules/background-task/src/NativeReactNativeBackgroundTask.ts new file mode 100644 index 000000000000..792fe2850552 --- /dev/null +++ b/modules/background-task/src/NativeReactNativeBackgroundTask.ts @@ -0,0 +1,12 @@ +import type {TurboModule} from 'react-native'; +import {TurboModuleRegistry} from 'react-native'; +import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes'; + +// We need to export the interface inline for proper TypeScript type inference with TurboModules +// eslint-disable-next-line rulesdir/no-inline-named-export, @typescript-eslint/consistent-type-definitions +export interface Spec extends TurboModule { + defineTask(taskName: string, taskExecutor: (data: unknown) => void | Promise): Promise; + readonly onBackgroundTaskExecution: EventEmitter; +} + +export default TurboModuleRegistry.getEnforcing('ReactNativeBackgroundTask'); diff --git a/modules/background-task/src/index.ts b/modules/background-task/src/index.ts new file mode 100644 index 000000000000..d3afe75a85bc --- /dev/null +++ b/modules/background-task/src/index.ts @@ -0,0 +1,35 @@ +import NativeReactNativeBackgroundTask from './NativeReactNativeBackgroundTask'; + +type TaskManagerTaskExecutor = (data: T) => void | Promise; + +const taskExecutors = new Map(); + +NativeReactNativeBackgroundTask.onBackgroundTaskExecution((taskName) => { + const executor = taskExecutors.get(taskName); + + if (executor) { + executor(taskName); + } +}); + +const TaskManager = { + /** + * Defines a task that can be executed in the background. + * @param taskName - Name of the task. Must be unique and match the name used when registering the task. + * @param taskExecutor - Function that will be executed when the task runs. + */ + defineTask: (taskName: string, taskExecutor: TaskManagerTaskExecutor): Promise => { + if (typeof taskName !== 'string' || taskName.length === 0) { + throw new Error('Task name must be a string'); + } + if (typeof taskExecutor !== 'function') { + throw new Error('Task executor must be a function'); + } + + taskExecutors.set(taskName, taskExecutor); + + return NativeReactNativeBackgroundTask.defineTask(taskName, taskExecutor); + }, +}; + +export default TaskManager; diff --git a/package-lock.json b/package-lock.json index 3a973b9150b6..682c46047e00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "new.expensify", - "version": "9.0.85-0", + "version": "9.0.86-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.85-0", + "version": "9.0.86-1", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", + "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-live-markdown": "0.1.210", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", @@ -40,7 +41,7 @@ "@react-navigation/native-stack": "^6.9.26", "@react-navigation/stack": "6.3.29", "@react-ng/bounds-observer": "^0.2.1", - "@rnmapbox/maps": "10.1.30", + "@rnmapbox/maps": "10.1.33", "@shopify/flash-list": "1.7.1", "@ua/react-native-airship": "19.2.1", "awesome-phonenumber": "^5.4.0", @@ -97,7 +98,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.87", + "react-native-onyx": "2.0.89", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -281,10 +282,15 @@ "xlsx": "file:vendor/xlsx-0.20.3.tgz" }, "engines": { - "node": "20.18.0", + "node": "20.18.1", "npm": "10.8.2" } }, + "modules/background-task": { + "name": "@expensify/react-native-background-task", + "version": "0.0.0", + "license": "UNLICENSED" + }, "node_modules/@0no-co/graphql.web": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.0.11.tgz", @@ -3630,6 +3636,10 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@expensify/react-native-background-task": { + "resolved": "modules/background-task", + "link": true + }, "node_modules/@expensify/react-native-live-markdown": { "version": "0.1.210", "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.210.tgz", @@ -9759,9 +9769,10 @@ } }, "node_modules/@rnmapbox/maps": { - "version": "10.1.30", - "resolved": "https://registry.npmjs.org/@rnmapbox/maps/-/maps-10.1.30.tgz", - "integrity": "sha512-3yl043+mpBldIHxTMMBU6Rdka6IjSww3kaIngltsUBTtnQI9NE1Yv3msC1X10E5bcfLHrhLxkiMSRhckCKBkPA==", + "version": "10.1.33", + "resolved": "https://registry.npmjs.org/@rnmapbox/maps/-/maps-10.1.33.tgz", + "integrity": "sha512-+J1aJnZ6q0N3044doCb6To/Eftpsn3Eq78AYPMa/Ig9x/u1eWpmbt5WbXJCehkOltGsjvTCenn+/gZKsXMZfww==", + "license": "MIT", "dependencies": { "@turf/along": "6.5.0", "@turf/distance": "6.5.0", @@ -32302,9 +32313,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.87", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.87.tgz", - "integrity": "sha512-TMeDGIFlCj1RFb38z8PoMMKbGBnVUja5dQ5niia4cGgcYJEfDX+i9skaS+UxJn5AUggwUd0TSxyjNjomQj/ERQ==", + "version": "2.0.89", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.89.tgz", + "integrity": "sha512-JzXjas0UNnYqTH4XD2Qfs64kBJBvHQ7HIIglieL1+Gto7eANyFRUpr0uRM3BlONinSPD/1xWZIurYAJtHuM5dg==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -32316,7 +32327,7 @@ "underscore": "^1.13.6" }, "engines": { - "node": ">=20.18.0", + "node": ">=20.18.1", "npm": ">=10.8.2" }, "peerDependencies": { diff --git a/package.json b/package.json index 31eca1f887f4..65a50ac02b48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.85-0", + "version": "9.0.86-1", "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.", @@ -77,6 +77,7 @@ "dependencies": { "@dotlottie/react-player": "^1.6.3", "@expensify/react-native-live-markdown": "0.1.210", + "@expensify/react-native-background-task": "file:./modules/background-task", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -105,7 +106,7 @@ "@react-navigation/native-stack": "^6.9.26", "@react-navigation/stack": "6.3.29", "@react-ng/bounds-observer": "^0.2.1", - "@rnmapbox/maps": "10.1.30", + "@rnmapbox/maps": "10.1.33", "@shopify/flash-list": "1.7.1", "@ua/react-native-airship": "19.2.1", "awesome-phonenumber": "^5.4.0", @@ -162,7 +163,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.87", + "react-native-onyx": "2.0.89", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -391,7 +392,7 @@ ] }, "engines": { - "node": "20.18.0", + "node": "20.18.1", "npm": "10.8.2" } } diff --git a/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch b/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch deleted file mode 100644 index b840e3da7b12..000000000000 --- a/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch +++ /dev/null @@ -1,83 +0,0 @@ -diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt -index 5bebc1b..80a4be4 100644 ---- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt -+++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt -@@ -5,6 +5,8 @@ import com.facebook.react.bridge.ReactApplicationContext - import com.facebook.react.uimanager.ThemedReactContext - import com.facebook.react.uimanager.annotations.ReactProp - import com.facebook.react.viewmanagers.RNMBXRasterSourceManagerInterface -+import com.rnmapbox.rnmbx.events.constants.EventKeys -+import com.rnmapbox.rnmbx.events.constants.eventMapOf - import javax.annotation.Nonnull - - class RNMBXRasterSourceManager(reactApplicationContext: ReactApplicationContext) : -@@ -26,7 +28,10 @@ class RNMBXRasterSourceManager(reactApplicationContext: ReactApplicationContext) - } - - override fun customEvents(): Map? { -- return null -+ return eventMapOf( -+ EventKeys.RASTER_SOURCE_LAYER_CLICK to "onMapboxRasterSourcePress", -+ EventKeys.MAP_ANDROID_CALLBACK to "onAndroidCallback" -+ ) - } - - companion object { -diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt -index d059b2c..3882f1e 100644 ---- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt -+++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt -@@ -4,35 +4,37 @@ private fun ns(name: String): String { - val namespace = "rct.mapbox" - return String.format("%s.%s", namespace, name) - } -+ - enum class EventKeys(val value: String) { - // map events -- MAP_CLICK(ns("map.press")), -- MAP_LONG_CLICK(ns("map.longpress")), -- MAP_ONCHANGE(ns("map.change")), -- MAP_ON_LOCATION_CHANGE(ns("map.location.change")), -- MAP_ANDROID_CALLBACK(ns("map.androidcallback")), -- MAP_USER_TRACKING_MODE_CHANGE(ns("map.usertrackingmodechange")), -+ MAP_CLICK("topPress"), -+ MAP_LONG_CLICK("topLongPress"), -+ MAP_ONCHANGE("topMapChange"), -+ MAP_ON_LOCATION_CHANGE("topLocationChange"), -+ MAP_ANDROID_CALLBACK("topAndroidCallback"), -+ MAP_USER_TRACKING_MODE_CHANGE("topUserTrackingModeChange"), - - // point annotation events -- POINT_ANNOTATION_SELECTED(ns("pointannotation.selected")), -- POINT_ANNOTATION_DESELECTED(ns("pointannotation.deselected")), -- POINT_ANNOTATION_DRAG_START(ns("pointannotation.dragstart")), -- POINT_ANNOTATION_DRAG(ns("pointannotation.drag")), -- POINT_ANNOTATION_DRAG_END(ns("pointannotation.dragend")), -+ POINT_ANNOTATION_SELECTED("topMapboxPointAnnotationSelected"), -+ POINT_ANNOTATION_DESELECTED("topMapboxPointAnnotationDeselected"), -+ POINT_ANNOTATION_DRAG_START("topMapboxPointAnnotationDragStart"), -+ POINT_ANNOTATION_DRAG("topMapboxPointAnnotationDrag"), -+ POINT_ANNOTATION_DRAG_END("topMapboxPointAnnotationDragEnd"), - - // source events -- SHAPE_SOURCE_LAYER_CLICK(ns("shapesource.layer.pressed")), -- VECTOR_SOURCE_LAYER_CLICK(ns("vectorsource.layer.pressed")), -- RASTER_SOURCE_LAYER_CLICK(ns("rastersource.layer.pressed")), -+ SHAPE_SOURCE_LAYER_CLICK("topMapboxShapeSourcePress"), -+ VECTOR_SOURCE_LAYER_CLICK("topMapboxVectorSourcePress"), -+ RASTER_SOURCE_LAYER_CLICK("topMapboxRasterSourcePress"), - - // images event -- IMAGES_MISSING(ns("images.missing")), -+ IMAGES_MISSING("topImageMissing"), - - // location events -+ // TODO: not sure about this one since it is not registered anywhere - USER_LOCATION_UPDATE(ns("user.location.update")), - - // viewport events -- VIEWPORT_STATUS_CHANGE(ns("viewport.statuschange")) -+ VIEWPORT_STATUS_CHANGE("topStatusChanged") - } - - fun eventMapOf(vararg values: Pair): Map { diff --git a/patches/@rnmapbox+maps+10.1.30+002+fix-nullable-types-0.76.patch b/patches/@rnmapbox+maps+10.1.30+002+fix-nullable-types-0.76.patch deleted file mode 100644 index 36c58b2db2e8..000000000000 --- a/patches/@rnmapbox+maps+10.1.30+002+fix-nullable-types-0.76.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImagesManager.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImagesManager.kt -index a9e2b68..1e08fdf 100644 ---- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImagesManager.kt -+++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImagesManager.kt -@@ -233,7 +233,7 @@ class RNMBXImagesManager(private val mContext: ReactApplicationContext) : - parent.mImageViews.remove(view) - } - -- override fun removeAllViews(parent: RNMBXImages) { -+ override fun removeAllViews(parent: RNMBXImages?) { - if (parent == null) { - Logger.e("RNMBXImages", "removeAllViews parent is null") - return -diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/utils/ViewTagResolver.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/utils/ViewTagResolver.kt -index f45cc25..5b0bb86 100644 ---- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/utils/ViewTagResolver.kt -+++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/utils/ViewTagResolver.kt -@@ -30,7 +30,7 @@ open class ViewTagResolver(val context: ReactApplicationContext) { - if (list != null) { - context.runOnUiQueueThread { - try { -- val view = manager.resolveView(viewTag) -+ val view = manager.resolveView(viewTag)!! - - list.forEach { it.fn(view) } - } catch (err: IllegalViewOperationException) { diff --git a/patches/focus-trap+7.5.4.patch b/patches/focus-trap+7.5.4.patch index c7b2aef2b51f..19ac4432dfaf 100644 --- a/patches/focus-trap+7.5.4.patch +++ b/patches/focus-trap+7.5.4.patch @@ -1,7 +1,22 @@ diff --git a/node_modules/focus-trap/dist/focus-trap.esm.js b/node_modules/focus-trap/dist/focus-trap.esm.js -index 10d56db..a6d76d8 100644 +index 10d56db..975151c 100644 --- a/node_modules/focus-trap/dist/focus-trap.esm.js +++ b/node_modules/focus-trap/dist/focus-trap.esm.js +@@ -71,12 +71,12 @@ var activeFocusTraps = { + trapStack.push(trap); + } + }, +- deactivateTrap: function deactivateTrap(trapStack, trap) { ++ deactivateTrap: function deactivateTrap(trapStack, trap, unpauseOnDeactivate) { + var trapIndex = trapStack.indexOf(trap); + if (trapIndex !== -1) { + trapStack.splice(trapIndex, 1); + } +- if (trapStack.length > 0) { ++ if (trapStack.length > 0 && unpauseOnDeactivate) { + trapStack[trapStack.length - 1].unpause(); + } + } @@ -100,8 +100,8 @@ var isKeyForward = function isKeyForward(e) { var isKeyBackward = function isKeyBackward(e) { return isTabEvent(e) && e.shiftKey; @@ -13,7 +28,15 @@ index 10d56db..a6d76d8 100644 }; // Array.find/findIndex() are not supported on IE; this replicates enough -@@ -283,7 +283,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { +@@ -153,6 +153,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { + var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document; + var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack; + var config = _objectSpread2({ ++ unpauseOnDeactivate: true, + returnFocusOnDeactivate: true, + escapeDeactivates: true, + delayInitialFocus: true, +@@ -283,7 +284,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { return node; }; var getInitialFocusNode = function getInitialFocusNode() { @@ -22,7 +45,7 @@ index 10d56db..a6d76d8 100644 // false explicitly indicates we want no initialFocus at all if (node === false) { -@@ -744,7 +744,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { +@@ -744,7 +745,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { // that caused the focus trap activation. state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () { tryFocus(getInitialFocusNode()); @@ -31,7 +54,16 @@ index 10d56db..a6d76d8 100644 doc.addEventListener('focusin', checkFocusIn, true); doc.addEventListener('mousedown', checkPointerDown, { capture: true, -@@ -880,7 +880,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { +@@ -868,7 +869,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { + state.active = false; + state.paused = false; + updateObservedNodes(); +- activeFocusTraps.deactivateTrap(trapStack, trap); ++ activeFocusTraps.deactivateTrap(trapStack, trap, config.unpauseOnDeactivate); + var onDeactivate = getOption(options, 'onDeactivate'); + var onPostDeactivate = getOption(options, 'onPostDeactivate'); + var checkCanReturnFocus = getOption(options, 'checkCanReturnFocus'); +@@ -880,7 +881,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)); } onPostDeactivate === null || onPostDeactivate === void 0 || onPostDeactivate(); @@ -41,7 +73,7 @@ index 10d56db..a6d76d8 100644 if (returnFocus && checkCanReturnFocus) { checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation); diff --git a/node_modules/focus-trap/index.d.ts b/node_modules/focus-trap/index.d.ts -index 400db1b..69f4b94 100644 +index 400db1b..78b38d9 100644 --- a/node_modules/focus-trap/index.d.ts +++ b/node_modules/focus-trap/index.d.ts @@ -16,7 +16,7 @@ declare module 'focus-trap' { @@ -53,7 +85,19 @@ index 400db1b..69f4b94 100644 type MouseEventToBoolean = (event: MouseEvent | TouchEvent) => boolean; type KeyboardEventToBoolean = (event: KeyboardEvent) => boolean; -@@ -185,7 +185,7 @@ declare module 'focus-trap' { +@@ -135,6 +135,11 @@ declare module 'focus-trap' { + * in the trap. + */ + fallbackFocus?: FocusTarget; ++ /** ++ * By default, the previous focus trap on the stack will be unpaused ++ * when the current active trap is deactivated. ++ */ ++ unpauseOnDeactivate?: boolean; + /** + * Default: `true`. If `false`, when the trap is deactivated, + * focus will *not* return to the element that had focus before activation. +@@ -185,7 +190,7 @@ declare module 'focus-trap' { * This prevents elements within the focusable element from capturing * the event that triggered the focus trap activation. */ diff --git a/src/App.tsx b/src/App.tsx index 1883e3a28c3a..b822618b93a9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import CustomStatusBarAndBackgroundContextProvider from './components/CustomStat import ErrorBoundary from './components/ErrorBoundary'; import HTMLEngineProvider from './components/HTMLEngineProvider'; import InitialURLContextProvider from './components/InitialURLContextProvider'; +import {InputBlurContextProvider} from './components/InputBlurContext'; import KeyboardProvider from './components/KeyboardProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; @@ -37,6 +38,7 @@ import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; +import './setup/backgroundTask'; import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; type AppProps = { @@ -93,6 +95,7 @@ function App({url}: AppProps) { KeyboardProvider, SearchRouterContextProvider, ProductTrainingContextProvider, + InputBlurContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 544b23bbbf2a..12e92c26479f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -62,6 +62,9 @@ const chatTypes = { // Explicit type annotation is required const cardActiveStates: number[] = [2, 3, 4, 7]; +// Hide not issued or not activated cards (states 2 and 4) from card filter options in search, as no transactions can be made on cards in these states +const cardHiddenFromSearchStates: number[] = [2, 4]; + const selectableOnboardingChoices = { PERSONAL_SPEND: 'newDotPersonalSpend', MANAGE_TEAM: 'newDotManageTeam', @@ -120,7 +123,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = { 'Here’s how to submit an expense:\n' + '\n' + '1. Click the green *+* button.\n' + - '2. Choose *Submit expense*.\n' + + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + '\n' + @@ -2803,6 +2806,7 @@ const CONST = { STATE_SUSPENDED: 7, }, ACTIVE_STATES: cardActiveStates, + HIDDEN_FROM_SEARCH_STATES: cardHiddenFromSearchStates, LIMIT_TYPES: { SMART: 'smart', MONTHLY: 'monthly', @@ -4507,6 +4511,7 @@ const CONST = { TRAVEL_DOT_URL: 'https://travel.expensify.com', STAGING_TRAVEL_DOT_URL: 'https://staging.travel.expensify.com', TRIP_ID_PATH: (tripID?: string) => (tripID ? `trips/${tripID}` : undefined), + TRIP_SUPPORT: '/support', SPOTNANA_TMC_ID: '8e8e7258-1cf3-48c0-9cd1-fe78a6e31eed', STAGING_SPOTNANA_TMC_ID: '7a290c6e-5328-4107-aff6-e48765845b81', SCREEN_READER_STATES: { @@ -4957,45 +4962,6 @@ const CONST = { ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes}, ONBOARDING_COMPANY_SIZE: {...onboardingCompanySize}, ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?', - ONBOARDING_CONCIERGE: { - [onboardingChoices.EMPLOYER]: - '# Expensify is the fastest way to get paid back!\n' + - '\n' + - 'To submit expenses for reimbursement:\n' + - '1. From the home screen, click the green + button > *Request money*.\n' + - "2. Enter an amount or scan a receipt, then input your boss's email.\n" + - '\n' + - "That'll send a request to get you paid back. Let me know if you have any questions!", - [onboardingChoices.MANAGE_TEAM]: - "# Let's start managing your team's expenses!\n" + - '\n' + - "To manage your team's expenses, create a workspace to keep everything in one place. Here's how:\n" + - '1. From the home screen, click the green + button > *New Workspace*\n' + - '2. Give your workspace a name (e.g. "Sales team expenses").\n' + - '\n' + - 'Then, invite your team to your workspace via the Members pane and [connect a business bank account](https://help.expensify.com/articles/new-expensify/bank-accounts/Connect-a-Bank-Account) to reimburse them. Let me know if you have any questions!', - [onboardingChoices.PERSONAL_SPEND]: - "# Let's start tracking your expenses! \n" + - '\n' + - "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + - '1. From the home screen, click the green + button > *New Workspace*\n' + - '2. Give your workspace a name (e.g. "My expenses").\n' + - '\n' + - 'Then, add expenses to your workspace:\n' + - '1. Find your workspace using the search field.\n' + - '2. Click the gray + button next to the message field.\n' + - '3. Click Request money, then add your expense type.\n' + - '\n' + - "We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!", - [onboardingChoices.CHAT_SPLIT]: - '# Splitting the bill is as easy as a conversation!\n' + - '\n' + - 'To split an expense:\n' + - '1. From the home screen, click the green + button > *Request money*.\n' + - '2. Enter an amount or scan a receipt, then choose who you want to split it with.\n' + - '\n' + - "We'll send a request to each person so they can pay you back. Let me know if you have any questions!", - }, ONBOARDING_ACCOUNTING_MAPPING: { quickbooksOnline: 'QuickBooks Online', xero: 'Xero', @@ -5020,7 +4986,7 @@ const CONST = { type: 'createWorkspace', autoCompleted: true, title: 'Create a workspace', - description: + description: ({workspaceSettingsLink}) => '*Create a workspace* to track expenses, scan receipts, chat, and more.\n' + '\n' + 'Here’s how to create a workspace:\n' + @@ -5028,7 +4994,7 @@ const CONST = { '1. Click the settings tab.\n' + '2. Click *Workspaces* > *New workspace*.\n' + '\n' + - '*Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.*', + `*Your new workspace is ready!* [Check it out](${workspaceSettingsLink}).`, }, selfGuidedTourTask, { @@ -5232,7 +5198,7 @@ const CONST = { 'Here’s how to submit an expense:\n' + '\n' + '1. Click the green *+* button.\n' + - '2. Choose *Submit expense*.\n' + + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + '\n' + diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fa40bfad4e63..2be9368a10b3 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -297,7 +297,10 @@ const ROUTES = { REPORT: 'r', REPORT_WITH_ID: { route: 'r/:reportID?/:reportActionID?', - getRoute: (reportID: string, reportActionID?: string, referrer?: string) => { + getRoute: (reportID: string | undefined, reportActionID?: string, referrer?: string) => { + if (!reportID) { + Log.warn('Invalid reportID is used to build the REPORT_WITH_ID route'); + } const baseRoute = reportActionID ? (`r/${reportID}/${reportActionID}` as const) : (`r/${reportID}` as const); const referrerParam = referrer ? `?referrer=${encodeURIComponent(referrer)}` : ''; return `${baseRoute}${referrerParam}` as const; @@ -363,7 +366,12 @@ const ROUTES = { }, REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', - getRoute: (reportID: string | undefined, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details`, backTo), + getRoute: (reportID: string | undefined, backTo?: string) => { + if (!reportID) { + Log.warn('Invalid reportID is used to build the REPORT_WITH_ID_DETAILS route'); + } + return getUrlWithBackToParam(`r/${reportID}/details`, backTo); + }, }, REPORT_WITH_ID_DETAILS_EXPORT: { route: 'r/:reportID/details/export/:connectionName', @@ -399,7 +407,12 @@ const ROUTES = { }, REPORT_DESCRIPTION: { route: 'r/:reportID/description', - getRoute: (reportID: string | undefined, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/description` as const, backTo), + getRoute: (reportID: string | undefined, backTo?: string) => { + if (!reportID) { + Log.warn('Invalid reportID is used to build the REPORT_DESCRIPTION route'); + } + return getUrlWithBackToParam(`r/${reportID}/description` as const, backTo); + }, }, TASK_ASSIGNEE: { route: 'r/:reportID/assignee', @@ -896,7 +909,12 @@ const ROUTES = { }, WORKSPACE_PROFILE_DESCRIPTION: { route: 'settings/workspaces/:policyID/profile/description', - getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/profile/description` as const, + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the WORKSPACE_PROFILE_DESCRIPTION route'); + } + return `settings/workspaces/${policyID}/profile/description` as const; + }, }, WORKSPACE_PROFILE_SHARE: { route: 'settings/workspaces/:policyID/profile/share', diff --git a/src/components/DisplayNames/index.native.tsx b/src/components/DisplayNames/index.native.tsx index ceee34586e8b..3ca810aa4d36 100644 --- a/src/components/DisplayNames/index.native.tsx +++ b/src/components/DisplayNames/index.native.tsx @@ -11,6 +11,7 @@ function DisplayNames({accessibilityLabel, fullTitle, textStyles = [], numberOfL accessibilityLabel={accessibilityLabel} style={textStyles} numberOfLines={numberOfLines} + testID={DisplayNames.displayName} > {fullTitle || translate('common.hidden')} {renderAdditionalText?.()} diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 1ae76f72ccef..5c9263e911b0 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -1,18 +1,23 @@ import type {VideoReadyForDisplayEvent} from 'expo-av'; +import type {ImageContentFit} from 'expo-image'; import React, {useCallback, useEffect, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; +import type {MergeExclusive} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; -import * as User from '@userActions/User'; +import {dismissTrackTrainingModal} from '@userActions/User'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import CheckboxWithLabel from './CheckboxWithLabel'; +import ImageSVG from './ImageSVG'; import Lottie from './Lottie'; import LottieAnimations from './LottieAnimations'; import type DotLottieAnimation from './LottieAnimations/types'; @@ -36,26 +41,18 @@ type VideoLoadedEventType = { type VideoStatus = 'video' | 'animation'; -type FeatureTrainingModalProps = { - /** Animation to show when video is unavailable. Useful when app is offline */ - animation?: DotLottieAnimation; +type BaseFeatureTrainingModalProps = { + /** The aspect ratio to preserve for the icon, video or animation */ + illustrationAspectRatio?: number; /** Style for the inner container of the animation */ - animationInnerContainerStyle?: StyleProp; + illustrationInnerContainerStyle?: StyleProp; /** Style for the outer container of the animation */ - animationOuterContainerStyle?: StyleProp; - - /** Additional styles for the animation */ - animationStyle?: StyleProp; - - /** URL for the video */ - videoURL: string; - - videoAspectRatio?: number; + illustrationOuterContainerStyle?: StyleProp; /** Title for the modal */ - title?: string; + title?: string | React.ReactNode; /** Describe what is showing */ description?: string; @@ -81,9 +78,6 @@ type FeatureTrainingModalProps = { /** Link to navigate to when user wants to learn more */ onHelp?: () => void; - /** Children to render */ - children?: React.ReactNode; - /** Styles for the content container */ contentInnerContainerStyles?: StyleProp; @@ -92,15 +86,46 @@ type FeatureTrainingModalProps = { /** Styles for the modal inner container */ modalInnerContainerStyle?: ViewStyle; + + /** Children to show below title and description and above buttons */ + children?: React.ReactNode; + + /** Modal width */ + width?: number; +}; + +type FeatureTrainingModalVideoProps = { + /** Animation to show when video is unavailable. Useful when app is offline */ + animation?: DotLottieAnimation; + + /** Additional styles for the animation */ + animationStyle?: StyleProp; + + /** URL for the video */ + videoURL?: string; +}; + +type FeatureTrainingModalSVGProps = { + /** Expensicon for the page */ + image: IconAsset; + + /** Determines how the image should be resized to fit its container */ + contentFitImage?: ImageContentFit; }; +// This page requires either an icon or a video/animation, but not both +type FeatureTrainingModalProps = BaseFeatureTrainingModalProps & MergeExclusive; + function FeatureTrainingModal({ animation, animationStyle, - animationInnerContainerStyle, - animationOuterContainerStyle, + illustrationInnerContainerStyle, + illustrationOuterContainerStyle, videoURL, - videoAspectRatio: videoAspectRatioProp, + illustrationAspectRatio: illustrationAspectRatioProp, + image, + contentFitImage, + width = variables.onboardingModalWidth, title = '', description = '', secondaryDescription = '', @@ -116,13 +141,14 @@ function FeatureTrainingModal({ modalInnerContainerStyle, }: FeatureTrainingModalProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout(); const [isModalVisible, setIsModalVisible] = useState(false); const [willShowAgain, setWillShowAgain] = useState(true); const [videoStatus, setVideoStatus] = useState('video'); const [isVideoStatusLocked, setIsVideoStatusLocked] = useState(false); - const [videoAspectRatio, setVideoAspectRatio] = useState(videoAspectRatioProp ?? VIDEO_ASPECT_RATIO); + const [illustrationAspectRatio, setIllustrationAspectRatio] = useState(illustrationAspectRatioProp ?? VIDEO_ASPECT_RATIO); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isOffline} = useNetwork(); @@ -149,14 +175,14 @@ function FeatureTrainingModal({ } if ('naturalSize' in event) { - setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height); + setIllustrationAspectRatio(event.naturalSize.width / event.naturalSize.height); } else { - setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); + setIllustrationAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); } }; const renderIllustration = useCallback(() => { - const aspectRatio = videoAspectRatio || VIDEO_ASPECT_RATIO; + const aspectRatio = illustrationAspectRatio || VIDEO_ASPECT_RATIO; return ( - {!!videoURL && videoStatus === 'video' ? ( + {!!image && ( + + )} + {!!videoURL && videoStatus === 'video' && ( - ) : ( + )} + {((!videoURL && !image) || (!!videoURL && videoStatus === 'animation')) && ( ); }, [ - videoAspectRatio, + image, + contentFitImage, + illustrationAspectRatio, styles.w100, styles.onboardingVideoPlayer, styles.flex1, @@ -208,14 +243,14 @@ function FeatureTrainingModal({ animationStyle, animation, shouldUseNarrowLayout, - animationInnerContainerStyle, + illustrationInnerContainerStyle, ]); const toggleWillShowAgain = useCallback(() => setWillShowAgain((prevWillShowAgain) => !prevWillShowAgain), []); const closeModal = useCallback(() => { if (!willShowAgain) { - User.dismissTrackTrainingModal(); + dismissTrackTrainingModal(); } setIsModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -238,7 +273,6 @@ function FeatureTrainingModal({ onClose={closeModal} innerContainerStyle={{ boxShadow: 'none', - borderRadius: 16, paddingBottom: 20, paddingTop: onboardingIsMediumOrLargerScreenWidth ? undefined : MODAL_PADDING, ...(onboardingIsMediumOrLargerScreenWidth @@ -252,14 +286,14 @@ function FeatureTrainingModal({ ...modalInnerContainerStyle, }} > - - + + {renderIllustration()} {!!title && !!description && ( - {title} + {typeof title === 'string' ? {title} : title} {description} {secondaryDescription.length > 0 && {secondaryDescription}} {children} diff --git a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx index 63a33899822c..a9407cccc00a 100644 --- a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx @@ -15,6 +15,7 @@ function FocusTrapForModal({children, active, initialFocus = false}: FocusTrapFo clickOutsideDeactivates: true, initialFocus, fallbackFocus: document.body, + unpauseOnDeactivate: false, setReturnFocus: (element) => { if (ReportActionComposeFocusManager.isFocused()) { return false; diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx index 21d5991c8005..706c46254436 100644 --- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -50,6 +50,7 @@ function FocusTrapForScreen({children, focusTrapSettings}: FocusTrapProps) { delayInitialFocus: CONST.ANIMATED_TRANSITION, initialFocus: false, setReturnFocus: false, + unpauseOnDeactivate: false, ...(focusTrapSettings?.focusTrapOptions ?? {}), }} > diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 2731d6bd1f98..e135fddd93ea 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -4,8 +4,10 @@ import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'rea import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import {useInputBlurContext} from '@components/InputBlurContext'; import useDebounceNonReactive from '@hooks/useDebounceNonReactive'; import useLocalize from '@hooks/useLocalize'; +import * as Browser from '@libs/Browser'; import * as ValidationUtils from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; @@ -95,6 +97,7 @@ function FormProvider( const [inputValues, setInputValues] = useState
(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); + const {setIsBlurred} = useInputBlurContext(); const onValidate = useCallback( (values: FormOnyxValues, shouldClearServerError = true) => { @@ -371,6 +374,9 @@ function FormProvider( }, VALIDATE_DELAY); } inputProps.onBlur?.(event); + if (Browser.isSafari()) { + setIsBlurred(true); + } }, onInputChange: (value, key) => { const inputKey = key ?? inputID; @@ -393,7 +399,7 @@ function FormProvider( }, }; }, - [draftValues, inputValues, formState?.errorFields, errors, submit, setTouchedInput, shouldValidateOnBlur, onValidate, hasServerError, formID, shouldValidateOnChange], + [draftValues, inputValues, formState?.errorFields, errors, submit, setTouchedInput, shouldValidateOnBlur, onValidate, hasServerError, setIsBlurred, formID, shouldValidateOnChange], ); const value = useMemo(() => ({registerInput}), [registerInput]); diff --git a/src/components/HoldMenuSectionList.tsx b/src/components/HoldMenuSectionList.tsx index 4ffdfa1bd60e..180b801c3f3c 100644 --- a/src/components/HoldMenuSectionList.tsx +++ b/src/components/HoldMenuSectionList.tsx @@ -16,9 +16,6 @@ type HoldMenuSection = { /** Translation key for the title */ titleTranslationKey: TranslationPaths; - - /** Translation key for the description */ - descriptionTranslationKey: TranslationPaths; }; function HoldMenuSectionList() { @@ -27,19 +24,12 @@ function HoldMenuSectionList() { const holdMenuSections: HoldMenuSection[] = [ { - icon: Illustrations.Hourglass, - titleTranslationKey: 'iou.whatIsHoldTitle', - descriptionTranslationKey: 'iou.whatIsHoldExplain', - }, - { - icon: Illustrations.CommentBubbles, - titleTranslationKey: 'iou.holdIsTemporaryTitle', - descriptionTranslationKey: 'iou.holdIsTemporaryExplain', + icon: Illustrations.Stopwatch, + titleTranslationKey: 'iou.holdIsLeftBehind', }, { - icon: Illustrations.TrashCan, - titleTranslationKey: 'iou.deleteHoldTitle', - descriptionTranslationKey: 'iou.deleteHoldExplain', + icon: Illustrations.RealtimeReport, + titleTranslationKey: 'iou.unholdWhenReady', }, ]; @@ -49,17 +39,16 @@ function HoldMenuSectionList() { {translate(section.titleTranslationKey)} - {translate(section.descriptionTranslationKey)} ))} diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 0debd4585e7b..fd5eb9f6fd58 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -38,6 +38,7 @@ import ConciergeBlue from '@assets/images/product-illustrations/concierge--blue. import ConciergeExclamation from '@assets/images/product-illustrations/concierge--exclamation.svg'; import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg'; import EmptyStateExpenses from '@assets/images/product-illustrations/emptystate__expenses.svg'; +import HoldExpense from '@assets/images/product-illustrations/emptystate__holdexpense.svg'; import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg'; import FolderWithPapers from '@assets/images/product-illustrations/folder-with-papers.svg'; import GpsTrackOrange from '@assets/images/product-illustrations/gps-track--orange.svg'; @@ -121,6 +122,7 @@ import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__ import Pillow from '@assets/images/simple-illustrations/simple-illustration__pillow.svg'; import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; +import RealtimeReport from '@assets/images/simple-illustrations/simple-illustration__realtimereports.svg'; import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; import ReceiptLocationMarker from '@assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg'; import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg'; @@ -131,6 +133,7 @@ import SendMoney from '@assets/images/simple-illustrations/simple-illustration__ import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg'; import SmallRocket from '@assets/images/simple-illustrations/simple-illustration__smallrocket.svg'; import SplitBill from '@assets/images/simple-illustrations/simple-illustration__splitbill.svg'; +import Stopwatch from '@assets/images/simple-illustrations/simple-illustration__stopwatch.svg'; import SubscriptionAnnual from '@assets/images/simple-illustrations/simple-illustration__subscription-annual.svg'; import SubscriptionPPU from '@assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg'; import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg'; @@ -223,6 +226,8 @@ export { LockClosed, Gears, QRCode, + RealtimeReport, + HoldExpense, ReceiptEnvelope, Approval, WalletAlt, @@ -251,6 +256,7 @@ export { ReceiptLocationMarker, Lightbulb, EmptyStateTravel, + Stopwatch, SubscriptionAnnual, SubscriptionPPU, ExpensifyApprovedLogo, diff --git a/src/components/InputBlurContext/index.tsx b/src/components/InputBlurContext/index.tsx new file mode 100644 index 000000000000..949c7029d849 --- /dev/null +++ b/src/components/InputBlurContext/index.tsx @@ -0,0 +1,32 @@ +import React, {useContext, useMemo, useState} from 'react'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type InputBlurContextType = { + isBlurred: boolean; // Boolean state to track blur + setIsBlurred: React.Dispatch>; // Function to update the state +}; + +const InputBlurContext = React.createContext({ + isBlurred: true, + setIsBlurred: () => {}, +}); + +function InputBlurContextProvider({children}: ChildrenProps) { + const [isBlurred, setIsBlurred] = useState(false); + + const contextValue = useMemo( + () => ({ + isBlurred, + setIsBlurred, + }), + [isBlurred], + ); + + return {children}; +} + +function useInputBlurContext() { + return useContext(InputBlurContext); +} + +export {InputBlurContext, useInputBlurContext, InputBlurContextProvider}; diff --git a/src/components/MigratedUserWelcomeModal.tsx b/src/components/MigratedUserWelcomeModal.tsx index d097e3095298..312f6e0f77d0 100644 --- a/src/components/MigratedUserWelcomeModal.tsx +++ b/src/components/MigratedUserWelcomeModal.tsx @@ -4,7 +4,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Welcome from '@libs/actions/Welcome'; +import {dismissProductTraining} from '@libs/actions/Welcome'; import convertToLTR from '@libs/convertToLTR'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -45,11 +45,11 @@ function OnboardingWelcomeVideo() { confirmText={translate('migratedUserWelcomeModal.confirmText')} animation={LottieAnimations.WorkspacePlanet} onClose={() => { - Welcome.dismissProductTraining(CONST.MIGRATED_USER_WELCOME_MODAL); + dismissProductTraining(CONST.MIGRATED_USER_WELCOME_MODAL); }} animationStyle={[styles.emptyWorkspaceIllustrationStyle]} - animationInnerContainerStyle={[StyleUtils.getBackgroundColorStyle(LottieAnimations.WorkspacePlanet.backgroundColor), styles.cardSectionIllustration]} - animationOuterContainerStyle={styles.p0} + illustrationInnerContainerStyle={[StyleUtils.getBackgroundColorStyle(LottieAnimations.WorkspacePlanet.backgroundColor), styles.cardSectionIllustration]} + illustrationOuterContainerStyle={styles.p0} contentInnerContainerStyles={[styles.mb5, styles.gap2]} contentOuterContainerStyles={!shouldUseNarrowLayout && [styles.mt8, styles.mh8]} modalInnerContainerStyle={{...styles.pt0, ...(shouldUseNarrowLayout ? {} : styles.pb8)}} diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 6c7a43758374..1654314e576d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -9,15 +9,44 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCurrentUserAccountID} from '@libs/actions/Report'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {getConnectedIntegration, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOriginalMessage, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; +import { + canBeExported, + canDeleteTransaction, + getArchiveReason, + getBankAccountRoute, + getMoneyRequestSpendBreakdown, + getNonHeldAndFullAmount, + getTransactionsWithReceipts, + hasHeldExpenses as hasHeldExpensesReportUtils, + hasOnlyHeldExpenses as hasOnlyHeldExpensesReportUtils, + hasUpdatedTotal, + isAllowedToApproveExpenseReport, + isAllowedToSubmitDraftExpenseReport, + isArchivedReport as isArchivedReportUtils, + isClosedExpenseReportWithNoExpenses, + isCurrentUserSubmitter, + isInvoiceReport, + isOpenExpenseReport, + navigateBackOnDeleteTransaction, +} from '@libs/ReportUtils'; +import { + allHavePendingRTERViolation, + getAllReportTransactions, + isDuplicate as isDuplicateTransactionUtils, + isExpensifyCardTransaction, + isOnHold as isOnHoldTransactionUtils, + isPayAtEndExpense as isPayAtEndExpenseTransactionUtils, + isPending, + isReceiptBeingScanned, + shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, +} from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import * as IOU from '@userActions/IOU'; -import * as TransactionActions from '@userActions/Transaction'; +import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidAction, deleteMoneyRequest, deleteTrackExpense, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU'; +import {markAsCash as markAsCashAction} from '@userActions/Transaction'; import CONST from '@src/CONST'; import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -40,7 +69,6 @@ import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusB import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import type {ActionHandledType} from './ProcessMoneyReportHoldMenu'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; -import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; import ExportWithDropdownMenu from './ReportActionItem/ExportWithDropdownMenu'; import SettlementButton from './SettlementButton'; @@ -67,8 +95,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const route = useRoute(); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`); - const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID || CONST.DEFAULT_NUMBER_ID}`); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID || CONST.DEFAULT_NUMBER_ID}`); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); const [session] = useOnyx(ONYXKEYS.SESSION); const requestParentReportAction = useMemo(() => { @@ -81,49 +111,45 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true}); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const transaction = - transactions?.[ - `${ONYXKEYS.COLLECTION.TRANSACTION}${ - ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) && ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID - }` - ] ?? undefined; + transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${isMoneyRequestAction(requestParentReportAction) && getOriginalMessage(requestParentReportAction)?.IOUTransactionID}`] ?? + undefined; const styles = useThemeStyles(); const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); - const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport); - const isOnHold = TransactionUtils.isOnHold(transaction); - const isDeletedParentAction = !!requestParentReportAction && ReportActionsUtils.isDeletedAction(requestParentReportAction); - const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID); + const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport); + const isOnHold = isOnHoldTransactionUtils(transaction); + const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction); + const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID); // Only the requestor can delete the request, admins can only edit it. const isActionOwner = typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID; - const canDeleteRequest = isActionOwner && ReportUtils.canDeleteTransaction(moneyRequestReport) && !isDeletedParentAction; + const canDeleteRequest = isActionOwner && canDeleteTransaction(moneyRequestReport) && !isDeletedParentAction; const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [paymentType, setPaymentType] = useState(); const [requestType, setRequestType] = useState(); - const allTransactions = useMemo(() => TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID, transactions), [moneyRequestReport?.reportID, transactions]); - const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy); + const allTransactions = useMemo(() => getAllReportTransactions(moneyRequestReport?.reportID, transactions), [moneyRequestReport?.reportID, transactions]); + const canAllowSettlement = hasUpdatedTotal(moneyRequestReport, policy); const policyType = policy?.type; - const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); - const connectedIntegration = PolicyUtils.getConnectedIntegration(policy); + const isDraft = isOpenExpenseReport(moneyRequestReport); + const connectedIntegration = getConnectedIntegration(policy); const navigateBackToAfterDelete = useRef(); - const hasHeldExpenses = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID); - const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t)); - const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t)); + const hasHeldExpenses = hasHeldExpensesReportUtils(moneyRequestReport?.reportID); + const hasScanningReceipt = getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => isReceiptBeingScanned(t)); + const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); const transactionIDs = allTransactions.map((t) => t.transactionID); - const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID]); - const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, moneyRequestReport, policy); - const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport?.reportID); - const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction); - const isArchivedReport = ReportUtils.isArchivedReport(moneyRequestReport); - const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: ReportUtils.getArchiveReason}); + const hasAllPendingRTERViolations = allHavePendingRTERViolation([transaction?.transactionID]); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction?.transactionID, moneyRequestReport, policy); + const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID); + const isPayAtEndExpense = isPayAtEndExpenseTransactionUtils(transaction); + const isArchivedReport = isArchivedReportUtils(moneyRequestReport); + const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: getArchiveReason}); const getCanIOUBePaid = useCallback( - (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere), + (onlyShowPayElsewhere = false) => canIOUBePaidAction(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere), [moneyRequestReport, chatReport, policy, transaction], ); const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]); @@ -131,13 +157,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]); const shouldShowMarkAsCashButton = - hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(moneyRequestReport?.reportID))); + hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(moneyRequestReport?.reportID))); const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere; - const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions, [moneyRequestReport, policy, hasOnlyPendingTransactions]); + const shouldShowApproveButton = useMemo(() => canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions, [moneyRequestReport, policy, hasOnlyPendingTransactions]); - const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); + const shouldDisableApproveButton = shouldShowApproveButton && !isAllowedToApproveExpenseReport(moneyRequestReport); const currentUserAccountID = getCurrentUserAccountID(); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; @@ -151,16 +177,16 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea !shouldShowBrokenConnectionViolation && (moneyRequestReport?.ownerAccountID === currentUserAccountID || isAdmin || moneyRequestReport?.managerID === currentUserAccountID); - const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(moneyRequestReport); + const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && canBeExported(moneyRequestReport); const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !hasAllPendingRTERViolations && !shouldShowExportIntegrationButton && !shouldShowBrokenConnectionViolation; - const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); + const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowStatusBar = hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions; - const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar; + const shouldShowNextStep = !isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar; const shouldShowAnyButton = isDuplicate || shouldShowSettlementButton || @@ -169,10 +195,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea shouldShowNextStep || shouldShowMarkAsCashButton || shouldShowExportIntegrationButton; - const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency); - const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton); - const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID); + const bankAccountRoute = getBankAccountRoute(chatReport); + const formattedAmount = convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency); + const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton); + const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID); const displayedAmount = isAnyTransactionOnHold && canAllowSettlement && hasValidNonHeldAmount ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); const {isDelegateAccessRestricted} = useDelegateUserDetails(); @@ -192,10 +218,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setIsNoDelegateAccessMenuVisible(true); } else if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); - } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) { - IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); + } else if (isInvoiceReport(moneyRequestReport)) { + payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); } else { - IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true); + payMoneyRequest(type, chatReport, moneyRequestReport, true); } }, [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport], @@ -208,19 +234,17 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea } else if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); } else { - IOU.approveMoneyRequest(moneyRequestReport, true); + approveMoneyRequest(moneyRequestReport, true); } }; const deleteTransaction = useCallback(() => { if (requestParentReportAction) { - const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) - ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID - : undefined; - if (ReportActionsUtils.isTrackExpenseAction(requestParentReportAction)) { - navigateBackToAfterDelete.current = IOU.deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, true); + const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined; + if (isTrackExpenseAction(requestParentReportAction)) { + navigateBackToAfterDelete.current = deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, true); } else { - navigateBackToAfterDelete.current = IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, true); + navigateBackToAfterDelete.current = deleteMoneyRequest(iouTransactionID, requestParentReportAction, true); } } @@ -231,15 +255,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (!requestParentReportAction) { return; } - const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) - ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID - : undefined; + const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined; const reportID = transactionThreadReport?.reportID; if (!iouTransactionID || !reportID) { return; } - TransactionActions.markAsCash(iouTransactionID, reportID); + markAsCashAction(iouTransactionID, reportID); }, [requestParentReportAction, transactionThreadReport?.reportID]); const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => ( @@ -304,30 +326,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea ); useEffect(() => { - if (isLoadingHoldUseExplained) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (isLoadingHoldUseExplained || dismissedHoldUseExplanation || !isOnHold) { return; } - setShouldShowHoldMenu(isOnHold && !dismissedHoldUseExplanation); + Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute())); }, [dismissedHoldUseExplanation, isLoadingHoldUseExplained, isOnHold]); - useEffect(() => { - if (!shouldShowHoldMenu) { - return; - } - - if (isSmallScreenWidth) { - if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD.route) { - Navigation.goBack(); - } - } else { - Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute())); - } - }, [isSmallScreenWidth, shouldShowHoldMenu]); - - const handleHoldRequestClose = () => { - IOU.dismissHoldUseExplanation(); - }; - useEffect(() => { if (canDeleteRequest) { return; @@ -400,7 +405,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea success={isWaitingForSubmissionFromCurrentUser} text={translate('common.submit')} style={[styles.mnw120, styles.pv2, styles.pr0]} - onPress={() => IOU.submitReport(moneyRequestReport)} + onPress={() => submitReport(moneyRequestReport)} isDisabled={shouldDisableSubmitButton} /> @@ -462,7 +467,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea success={isWaitingForSubmissionFromCurrentUser} text={translate('common.submit')} style={[styles.flex1, styles.pr0]} - onPress={() => IOU.submitReport(moneyRequestReport)} + onPress={() => submitReport(moneyRequestReport)} isDisabled={shouldDisableSubmitButton} /> )} @@ -507,20 +512,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea isVisible={isDeleteRequestModalVisible} onConfirm={deleteTransaction} onCancel={() => setIsDeleteRequestModalVisible(false)} - onModalHide={() => ReportUtils.navigateBackOnDeleteTransaction(navigateBackToAfterDelete.current)} + onModalHide={() => navigateBackOnDeleteTransaction(navigateBackToAfterDelete.current)} prompt={translate('iou.deleteConfirmation', {count: 1})} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} danger shouldEnableNewFocusManagement /> - {isSmallScreenWidth && shouldShowHoldMenu && ( - - )} ); } diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index c470f0c50806..54e55b064341 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -814,6 +814,9 @@ function MoneyRequestConfirmationList({ return; } Log.info(`[IOU] Sending money via: ${paymentMethod}`); + if (shouldPlaySound) { + playSound(SOUNDS.DONE); + } onSendMoney?.(paymentMethod); } }, diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index f253c757050f..32480e37d8ee 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,6 +1,6 @@ import {useRoute} from '@react-navigation/native'; import type {ReactNode} from 'react'; -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -9,13 +9,24 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {isCurrentUserSubmitter} from '@libs/ReportUtils'; +import { + allHavePendingRTERViolation, + getTransactionViolations, + hasPendingRTERViolation, + hasReceipt, + isDuplicate as isDuplicateTransactionUtils, + isExpensifyCardTransaction, + isOnHold as isOnHoldTransactionUtils, + isPending, + isReceiptBeingScanned, + shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, +} from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import * as IOU from '@userActions/IOU'; -import * as TransactionActions from '@userActions/Transaction'; +import {markAsCash as markAsCashAction} from '@userActions/Transaction'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -29,7 +40,6 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; -import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; type MoneyRequestHeaderProps = { /** The report currently being looked at */ @@ -50,10 +60,10 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const route = useRoute(); - const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); const [transaction] = useOnyx( `${ONYXKEYS.COLLECTION.TRANSACTION}${ - ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? -1 : -1 + isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID : CONST.DEFAULT_NUMBER_ID }`, ); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); @@ -62,26 +72,24 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); - const isOnHold = TransactionUtils.isOnHold(transaction); - const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? ''); + const isOnHold = isOnHoldTransactionUtils(transaction); + const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID); const reportID = report?.reportID; const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; - const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']); + const hasAllPendingRTERViolations = allHavePendingRTERViolation([transaction?.transactionID]); - const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', parentReport, policy); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction?.transactionID, parentReport, policy); - const shouldShowMarkAsCashButton = - hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(parentReport?.reportID ?? ''))); + const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(parentReport?.reportID))); const markAsCash = useCallback(() => { - TransactionActions.markAsCash(transaction?.transactionID ?? '-1', reportID ?? ''); + markAsCashAction(transaction?.transactionID, reportID); }, [reportID, transaction?.transactionID]); - const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); + const isScanning = hasReceipt(transaction) && isReceiptBeingScanned(transaction); const getStatusIcon: (src: IconAsset) => ReactNode = (src) => ( ), }; } - if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) { + if (hasPendingRTERViolation(getTransactionViolations(transaction?.transactionID, transactionViolations))) { return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; } if (isScanning) { @@ -123,106 +131,90 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const statusBarProps = getStatusBarProps(); useEffect(() => { - if (isLoadingHoldUseExplained) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (isLoadingHoldUseExplained || dismissedHoldUseExplanation || !isOnHold) { return; } - setShouldShowHoldMenu(isOnHold && !dismissedHoldUseExplanation); + Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute())); }, [dismissedHoldUseExplanation, isLoadingHoldUseExplained, isOnHold]); - useEffect(() => { - if (!shouldShowHoldMenu) { - return; - } - - if (isSmallScreenWidth) { - if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD.route) { - Navigation.goBack(); - } - } else { - Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute())); - } - }, [isSmallScreenWidth, shouldShowHoldMenu]); - - const handleHoldRequestClose = () => { - IOU.dismissHoldUseExplanation(); - }; - return ( - <> - - - {shouldShowMarkAsCashButton && !shouldUseNarrowLayout && ( -