diff --git a/.storybook/public/favicon.svg b/.storybook/public/favicon.svg index 6bc34f89282e..726791b58cfb 100644 --- a/.storybook/public/favicon.svg +++ b/.storybook/public/favicon.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/README.md b/README.md index 29a9e9b8ffdc..6544e0e95486 100644 --- a/README.md +++ b/README.md @@ -663,7 +663,38 @@ Sometimes it might be beneficial to generate a local production version instead In order to generate a production web build, run `npm run build`, this will generate a production javascript build in the `dist/` folder. #### Local production build of the MacOS desktop app -In order to compile a production desktop build, run `npm run desktop-build`, this will generate a production app in the `dist/Mac` folder named `Chat.app`. +The commands used to compile a production or staging desktop build are `npm run desktop-build` and `npm run desktop-build-staging`, respectively. These will product an app in the `dist/Mac` folder named NewExpensify.dmg that you can install like a normal app. + +HOWEVER, by default those commands will try to notarize the build (signing it as Expensify) and publish it to the S3 bucket where it's hosted for users. In most cases you won't actually need or want to do that for your local testing. To get around that and disable those behaviors for your local build, apply the following diff: + +```diff +diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js +index e4ed685f65..4c7c1b3667 100644 +--- a/config/electronBuilder.config.js ++++ b/config/electronBuilder.config.js +@@ -42,9 +42,6 @@ module.exports = { + entitlements: 'desktop/entitlements.mac.plist', + entitlementsInherit: 'desktop/entitlements.mac.plist', + type: 'distribution', +- notarize: { +- teamId: '368M544MTT', +- }, + }, + dmg: { + title: 'New Expensify', +diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh +index 791f59d733..526306eec1 100755 +--- a/scripts/build-desktop.sh ++++ b/scripts/build-desktop.sh +@@ -35,4 +35,4 @@ npx webpack --config config/webpack/webpack.desktop.ts --env file=$ENV_FILE + title "Building Desktop App Archive Using Electron" + info "" + shift 1 +-npx electron-builder --config config/electronBuilder.config.js --publish always "$@" ++npx electron-builder --config config/electronBuilder.config.js --publish never "$@" +``` + +There may be some cases where you need to test a signed and published build, such as when testing the update flows. Instructions on setting that up can be found in [Testing Electron Auto-Update](https://github.com/Expensify/App/blob/main/desktop/README.md#testing-electron-auto-update). Good luck 🙃 #### Local production build the iOS app In order to compile a production iOS build, run `npm run ios-build`, this will generate a `Chat.ipa` in the root directory of this project. diff --git a/android/app/build.gradle b/android/app/build.gradle index d78c4b107e84..fb2791852d51 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001047700 - versionName "1.4.77-0" + versionCode 1001047803 + versionName "1.4.78-3" // 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/all.svg b/assets/images/all.svg index d1a833d280ce..f6d9f46fc92e 100644 --- a/assets/images/all.svg +++ b/assets/images/all.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/arrow-down-long.svg b/assets/images/arrow-down-long.svg new file mode 100644 index 000000000000..cbf6e7e5ad2f --- /dev/null +++ b/assets/images/arrow-down-long.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/arrow-right.svg b/assets/images/arrow-right.svg index 8d2ded92e791..649582544847 100644 --- a/assets/images/arrow-right.svg +++ b/assets/images/arrow-right.svg @@ -1,10 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/arrow-up-long.svg b/assets/images/arrow-up-long.svg new file mode 100644 index 000000000000..13d7a0c2d67e --- /dev/null +++ b/assets/images/arrow-up-long.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/avatars/fallback-avatar.svg b/assets/images/avatars/fallback-avatar.svg index 69293d72aed9..4a7fecf967db 100644 --- a/assets/images/avatars/fallback-avatar.svg +++ b/assets/images/avatars/fallback-avatar.svg @@ -1,10 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_1.svg b/assets/images/avatars/group/default-avatar_1.svg index 5d97c5bf855b..1edcaa33a8aa 100644 --- a/assets/images/avatars/group/default-avatar_1.svg +++ b/assets/images/avatars/group/default-avatar_1.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_10.svg b/assets/images/avatars/group/default-avatar_10.svg index 12c9dd76ae31..62e818cb3e45 100644 --- a/assets/images/avatars/group/default-avatar_10.svg +++ b/assets/images/avatars/group/default-avatar_10.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_11.svg b/assets/images/avatars/group/default-avatar_11.svg index 97f17f30f3a7..2f976b05519d 100644 --- a/assets/images/avatars/group/default-avatar_11.svg +++ b/assets/images/avatars/group/default-avatar_11.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_12.svg b/assets/images/avatars/group/default-avatar_12.svg index f917fb136582..c29992aa1793 100644 --- a/assets/images/avatars/group/default-avatar_12.svg +++ b/assets/images/avatars/group/default-avatar_12.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_13.svg b/assets/images/avatars/group/default-avatar_13.svg index 9e59fb9123a5..5f6b69f01fe3 100644 --- a/assets/images/avatars/group/default-avatar_13.svg +++ b/assets/images/avatars/group/default-avatar_13.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_14.svg b/assets/images/avatars/group/default-avatar_14.svg index ca071e488416..27096ffd77d7 100644 --- a/assets/images/avatars/group/default-avatar_14.svg +++ b/assets/images/avatars/group/default-avatar_14.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_15.svg b/assets/images/avatars/group/default-avatar_15.svg index f227cc0717be..7cae7b1e6562 100644 --- a/assets/images/avatars/group/default-avatar_15.svg +++ b/assets/images/avatars/group/default-avatar_15.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_16.svg b/assets/images/avatars/group/default-avatar_16.svg index efbb85f0b13d..1c02725ba669 100644 --- a/assets/images/avatars/group/default-avatar_16.svg +++ b/assets/images/avatars/group/default-avatar_16.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_17.svg b/assets/images/avatars/group/default-avatar_17.svg index 25c015c595ca..58a5014fae68 100644 --- a/assets/images/avatars/group/default-avatar_17.svg +++ b/assets/images/avatars/group/default-avatar_17.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_18.svg b/assets/images/avatars/group/default-avatar_18.svg index a58ee6e66eff..43eeffb3db8d 100644 --- a/assets/images/avatars/group/default-avatar_18.svg +++ b/assets/images/avatars/group/default-avatar_18.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_2.svg b/assets/images/avatars/group/default-avatar_2.svg index ff1cc3e6dd2d..f67a49d28cd2 100644 --- a/assets/images/avatars/group/default-avatar_2.svg +++ b/assets/images/avatars/group/default-avatar_2.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_3.svg b/assets/images/avatars/group/default-avatar_3.svg index dde31b5d02a0..471d3a348b4a 100644 --- a/assets/images/avatars/group/default-avatar_3.svg +++ b/assets/images/avatars/group/default-avatar_3.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_4.svg b/assets/images/avatars/group/default-avatar_4.svg index f6d02801bc6b..46e22d28b6df 100644 --- a/assets/images/avatars/group/default-avatar_4.svg +++ b/assets/images/avatars/group/default-avatar_4.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_5.svg b/assets/images/avatars/group/default-avatar_5.svg index fdabd36e2058..a81471170e23 100644 --- a/assets/images/avatars/group/default-avatar_5.svg +++ b/assets/images/avatars/group/default-avatar_5.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_6.svg b/assets/images/avatars/group/default-avatar_6.svg index 6f1c6b80eda6..71da5e5631f3 100644 --- a/assets/images/avatars/group/default-avatar_6.svg +++ b/assets/images/avatars/group/default-avatar_6.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_7.svg b/assets/images/avatars/group/default-avatar_7.svg index 62d9a8b76bb8..080426ca0454 100644 --- a/assets/images/avatars/group/default-avatar_7.svg +++ b/assets/images/avatars/group/default-avatar_7.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_8.svg b/assets/images/avatars/group/default-avatar_8.svg index 206b10c2322b..b6b2d98579eb 100644 --- a/assets/images/avatars/group/default-avatar_8.svg +++ b/assets/images/avatars/group/default-avatar_8.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_9.svg b/assets/images/avatars/group/default-avatar_9.svg index ffbe02ce57e8..14885d4c401c 100644 --- a/assets/images/avatars/group/default-avatar_9.svg +++ b/assets/images/avatars/group/default-avatar_9.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/back-left.svg b/assets/images/back-left.svg index 2ddd554e9720..2c709401916f 100644 --- a/assets/images/back-left.svg +++ b/assets/images/back-left.svg @@ -1,10 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/coins.svg b/assets/images/coins.svg index aa3c68e72ea8..164fa84388f5 100644 --- a/assets/images/coins.svg +++ b/assets/images/coins.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/comment-bubbles.svg b/assets/images/comment-bubbles.svg new file mode 100644 index 000000000000..1277b8958c94 --- /dev/null +++ b/assets/images/comment-bubbles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/connection-complete.svg b/assets/images/connection-complete.svg index fbfb2b041358..d864d9a33626 100644 --- a/assets/images/connection-complete.svg +++ b/assets/images/connection-complete.svg @@ -1,330 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/credit-card-hourglass.svg b/assets/images/credit-card-hourglass.svg index 2acd013fbe59..28ffe766b597 100644 --- a/assets/images/credit-card-hourglass.svg +++ b/assets/images/credit-card-hourglass.svg @@ -1,19 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/document-plus.svg b/assets/images/document-plus.svg index cce2e3027cea..729bc98d4f8a 100644 --- a/assets/images/document-plus.svg +++ b/assets/images/document-plus.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/assets/images/document-slash.svg b/assets/images/document-slash.svg index ebb183142e40..e8a0ff20702e 100644 --- a/assets/images/document-slash.svg +++ b/assets/images/document-slash.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/integrationicons/qbo-icon-square.svg b/assets/images/integrationicons/qbo-icon-square.svg index a8ce3468ffbf..e297b597f980 100644 --- a/assets/images/integrationicons/qbo-icon-square.svg +++ b/assets/images/integrationicons/qbo-icon-square.svg @@ -1,14 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/integrationicons/xero-icon-square.svg b/assets/images/integrationicons/xero-icon-square.svg index 94b79bb3533d..43774919c92c 100644 --- a/assets/images/integrationicons/xero-icon-square.svg +++ b/assets/images/integrationicons/xero-icon-square.svg @@ -1,32 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/invoice-generic.svg b/assets/images/invoice-generic.svg index d0e2662c4084..251918c4cff4 100644 --- a/assets/images/invoice-generic.svg +++ b/assets/images/invoice-generic.svg @@ -1,15 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/money-waving.svg b/assets/images/money-waving.svg index 5242e31092a0..e68744d595be 100644 --- a/assets/images/money-waving.svg +++ b/assets/images/money-waving.svg @@ -1,81 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/new-expensify-adhoc.svg b/assets/images/new-expensify-adhoc.svg index b3dd92fbbaae..8da6331c8c94 100644 --- a/assets/images/new-expensify-adhoc.svg +++ b/assets/images/new-expensify-adhoc.svg @@ -1,31 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/new-expensify-dev.svg b/assets/images/new-expensify-dev.svg index 316da6b5aa4d..fcb371f586b6 100644 --- a/assets/images/new-expensify-dev.svg +++ b/assets/images/new-expensify-dev.svg @@ -1,27 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/new-expensify-stg.svg b/assets/images/new-expensify-stg.svg index 1a1994c7a9fd..d536257fc880 100644 --- a/assets/images/new-expensify-stg.svg +++ b/assets/images/new-expensify-stg.svg @@ -1,35 +1 @@ - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/play.svg b/assets/images/play.svg index 5f7e14969529..98a8c00520fc 100644 --- a/assets/images/play.svg +++ b/assets/images/play.svg @@ -1,6 +1 @@ - - - - - + \ No newline at end of file diff --git a/assets/images/qrcode.svg b/assets/images/qrcode.svg index 42c49c958246..47d61d7dd47c 100644 --- a/assets/images/qrcode.svg +++ b/assets/images/qrcode.svg @@ -1,14 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/receipt-plus.svg b/assets/images/receipt-plus.svg index 3907da65c472..ca4d96b3dfa5 100644 --- a/assets/images/receipt-plus.svg +++ b/assets/images/receipt-plus.svg @@ -1,12 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/receipt-scan.svg b/assets/images/receipt-scan.svg index c93986de3c9b..f7c164c948c8 100644 --- a/assets/images/receipt-scan.svg +++ b/assets/images/receipt-scan.svg @@ -1,14 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__abacus.svg b/assets/images/simple-illustrations/simple-illustration__abacus.svg index df94ab653982..6dac0e9009b1 100644 --- a/assets/images/simple-illustrations/simple-illustration__abacus.svg +++ b/assets/images/simple-illustrations/simple-illustration__abacus.svg @@ -1,43 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__accounting.svg b/assets/images/simple-illustrations/simple-illustration__accounting.svg index f7634141e966..3213b4f93856 100644 --- a/assets/images/simple-illustrations/simple-illustration__accounting.svg +++ b/assets/images/simple-illustrations/simple-illustration__accounting.svg @@ -1,32 +1 @@ - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__alert.svg b/assets/images/simple-illustrations/simple-illustration__alert.svg index 2e7bca02f5e3..cbf70b7655a7 100644 --- a/assets/images/simple-illustrations/simple-illustration__alert.svg +++ b/assets/images/simple-illustrations/simple-illustration__alert.svg @@ -1,15 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__binoculars.svg b/assets/images/simple-illustrations/simple-illustration__binoculars.svg index 381be8988873..5abacd359464 100644 --- a/assets/images/simple-illustrations/simple-illustration__binoculars.svg +++ b/assets/images/simple-illustrations/simple-illustration__binoculars.svg @@ -1,50 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__car-ice.svg b/assets/images/simple-illustrations/simple-illustration__car-ice.svg index ba2b79bca6aa..9da1b844c101 100644 --- a/assets/images/simple-illustrations/simple-illustration__car-ice.svg +++ b/assets/images/simple-illustrations/simple-illustration__car-ice.svg @@ -1,53 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__car.svg b/assets/images/simple-illustrations/simple-illustration__car.svg index 2d420be6c3a9..9da1b844c101 100644 --- a/assets/images/simple-illustrations/simple-illustration__car.svg +++ b/assets/images/simple-illustrations/simple-illustration__car.svg @@ -1,25 +1 @@ - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__coins.svg b/assets/images/simple-illustrations/simple-illustration__coins.svg index 5350886402c6..5caa1c0635d5 100644 --- a/assets/images/simple-illustrations/simple-illustration__coins.svg +++ b/assets/images/simple-illustrations/simple-illustration__coins.svg @@ -1,26 +1 @@ - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__company-card.svg b/assets/images/simple-illustrations/simple-illustration__company-card.svg index 4121bbeeb205..1f4e43dbc047 100644 --- a/assets/images/simple-illustrations/simple-illustration__company-card.svg +++ b/assets/images/simple-illustrations/simple-illustration__company-card.svg @@ -1,38 +1 @@ - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__lightbulb.svg b/assets/images/simple-illustrations/simple-illustration__lightbulb.svg index 1dc359764147..62a9cb0c3b76 100644 --- a/assets/images/simple-illustrations/simple-illustration__lightbulb.svg +++ b/assets/images/simple-illustrations/simple-illustration__lightbulb.svg @@ -1,33 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__pencil.svg b/assets/images/simple-illustrations/simple-illustration__pencil.svg index 8d9f06991612..d3eaf8771021 100644 --- a/assets/images/simple-illustrations/simple-illustration__pencil.svg +++ b/assets/images/simple-illustrations/simple-illustration__pencil.svg @@ -1,20 +1 @@ - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__piggybank.svg b/assets/images/simple-illustrations/simple-illustration__piggybank.svg index be87ff34752a..ab1f73113f18 100644 --- a/assets/images/simple-illustrations/simple-illustration__piggybank.svg +++ b/assets/images/simple-illustrations/simple-illustration__piggybank.svg @@ -1,50 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__receiptupload.svg b/assets/images/simple-illustrations/simple-illustration__receiptupload.svg index b8fe5101715f..efff624f481f 100644 --- a/assets/images/simple-illustrations/simple-illustration__receiptupload.svg +++ b/assets/images/simple-illustrations/simple-illustration__receiptupload.svg @@ -1,22 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__splitbill.svg b/assets/images/simple-illustrations/simple-illustration__splitbill.svg index dfed7535ee90..1390a7cf9205 100644 --- a/assets/images/simple-illustrations/simple-illustration__splitbill.svg +++ b/assets/images/simple-illustrations/simple-illustration__splitbill.svg @@ -1,55 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__tag.svg b/assets/images/simple-illustrations/simple-illustration__tag.svg index 0cac51679a5e..0a93014d11b3 100644 --- a/assets/images/simple-illustrations/simple-illustration__tag.svg +++ b/assets/images/simple-illustrations/simple-illustration__tag.svg @@ -1,33 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__teachers-unite.svg b/assets/images/simple-illustrations/simple-illustration__teachers-unite.svg index b4edd9513722..27ce709889dd 100644 --- a/assets/images/simple-illustrations/simple-illustration__teachers-unite.svg +++ b/assets/images/simple-illustrations/simple-illustration__teachers-unite.svg @@ -1,49 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__workflows.svg b/assets/images/simple-illustrations/simple-illustration__workflows.svg index b684c58126f7..c11d2663997f 100644 --- a/assets/images/simple-illustrations/simple-illustration__workflows.svg +++ b/assets/images/simple-illustrations/simple-illustration__workflows.svg @@ -1,153 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/stopwatch.svg b/assets/images/stopwatch.svg index 0f26af219e04..b8ca46fd1fa1 100644 --- a/assets/images/stopwatch.svg +++ b/assets/images/stopwatch.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/suitcase.svg b/assets/images/suitcase.svg index 97036db6b5ac..452c44f73e22 100644 --- a/assets/images/suitcase.svg +++ b/assets/images/suitcase.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/tag.svg b/assets/images/tag.svg index f5e13b8135cb..f25bcbe47f71 100644 --- a/assets/images/tag.svg +++ b/assets/images/tag.svg @@ -1,12 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/thread.svg b/assets/images/thread.svg index 3b8f334fafdd..9f01ce7b2c06 100644 --- a/assets/images/thread.svg +++ b/assets/images/thread.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/x-circle.svg b/assets/images/x-circle.svg index c186e41c4244..5fa5f3741567 100644 --- a/assets/images/x-circle.svg +++ b/assets/images/x-circle.svg @@ -1,12 +1 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md index 08a444a6b8e4..cc3e256be399 100644 --- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md +++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md @@ -96,6 +96,79 @@ These steps are covered in more detail in the "testing" section below. Due to some technical constraints, Apple and Google sign-in may require additional configuration to be able to work in the development environment as expected. This document describes any additional steps for each platform. +## Show Apple / Google SSO buttons development environment + +The Apple/Google Sign In button renders differently in development mode. To prevent confusion +for developers about a possible regression, we decided to not render third party buttons in +development mode. + +To re-enable the SSO buttons in development mode, remove this [condition](https://github.com/Expensify/App/blob/c2a718c9100e704c89ad9564301348bc53a49777/src/pages/signin/LoginForm/BaseLoginForm.tsx#L300) so that we always render the SSO button components: + +```diff +diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx +index 4286a26033..850f8944ca 100644 +--- a/src/pages/signin/LoginForm/BaseLoginForm.tsx ++++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx +@@ -288,7 +288,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false + // for developers about possible regressions, we won't render buttons in development mode. + // For more information about these differences and how to test in development mode, + // see`Expensify/App/contributingGuides/APPLE_GOOGLE_SIGNIN.md` +- CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.DEV && ( ++ ( + + `Swift Default Apps` => `URI Schemes` => `new-expensify` and select `New Expensify.app` +4. Note that a dev build of the desktop app will not work. You'll create and install a local staging build: + 1. Update `build-desktop.sh` replacing `--publish always` with `--publish never`. + 2. Run `npm run desktop-build-staging` and install the locally-generated desktop app to test. +5. (Google only) apply the following diff: + + ```diff + diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx + index 765fbab038..4318528b4c 100644 + --- a/src/components/DeeplinkWrapper/index.website.tsx + +++ b/src/components/DeeplinkWrapper/index.website.tsx + @@ -63,14 +63,7 @@ function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWra + const isUnsupportedDeeplinkRoute = routeRegex.test(window.location.pathname); + + // Making a few checks to exit early before checking authentication status + - if ( + - !isMacOSWeb() || + - isUnsupportedDeeplinkRoute || + - hasShownPrompt || + - CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV || + - autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || + - Session.isAnonymousUser() + - ) { + + if (!isMacOSWeb() || isUnsupportedDeeplinkRoute || hasShownPrompt || autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || Session.isAnonymousUser()) { + return; + } + // We want to show the prompt immediately if the user is already authenticated. + diff --git a/src/libs/Navigation/linkingConfig/prefixes.ts b/src/libs/Navigation/linkingConfig/prefixes.ts + index ca2da6f56b..2c191598f0 100644 + --- a/src/libs/Navigation/linkingConfig/prefixes.ts + +++ b/src/libs/Navigation/linkingConfig/prefixes.ts + @@ -8,6 +8,7 @@ const prefixes: LinkingOptions['prefixes'] = [ + 'https://www.expensify.cash', + 'https://staging.expensify.cash', + 'https://dev.new.expensify.com', + + 'http://localhost', + CONST.NEW_EXPENSIFY_URL, + CONST.STAGING_NEW_EXPENSIFY_URL, + ]; + ``` + +6. Run `npm run web` + ## Apple #### Port requirements @@ -193,57 +266,11 @@ This is required because the desktop app needs to know the address of the web ap Note that changing this value to a domain that isn't configured for use with Expensify will cause Android to break, as it is still using the real client ID, but now has an incorrect value for `redirectURI`. -#### Set Environment to something other than "Development" - -The `DeepLinkWrapper` component will not handle deep links in the development environment. To be able to test deep linking, you must set the environment to something other than "Development". - -Within the `.env` file, set `envName` to something other than "Development", for example: - -``` -envName=Staging -``` - -Alternatively, within the `DeepLinkWrapper/index.website.js` file, you can set the `CONFIG.ENVIRONMENT` to something other than "Development". +## Google -#### Handle deep links in dev on MacOS +Unlike with Apple, to test Google Sign-In we don't need to set up any http/ssh tunnels. We can just use `localhost`. But we need to set up the web and desktop environments to use `localhost` instead of `dev.new.expensify.com` -If developing on MacOS, the development desktop app can't handle deeplinks correctly. To be able to test deeplinking back to the app, follow these steps: - -1. Create a "real" build of the desktop app, which can handle deep links, open the build folder, and install the dmg there: - -```shell -npm run desktop-build -open desktop-build -# Then double-click "NewExpensify.dmg" in Finder window -``` - -2. Even with this build, the deep link may not be handled by the correct app, as the development Electron config seems to intercept it sometimes. To manage this, install [SwiftDefaultApps](https://github.com/Lord-Kamina/SwiftDefaultApps), which adds a preference pane that can be used to configure which app should handle deep links. - -### Test the Apple / Google SSO buttons in development environment - -The Apple/Google Sign In button renders differently in development mode. To prevent confusion -for developers about a possible regression, we decided to not render third party buttons in -development mode. - -Here's how you can re-enable the SSO buttons in development mode: - -- Remove this [condition](https://github.com/Expensify/App/blob/c2a718c9100e704c89ad9564301348bc53a49777/src/pages/signin/LoginForm/BaseLoginForm.tsx#L300) so that we always render the SSO button components - ```diff - diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx - index 4286a26033..850f8944ca 100644 - --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx - +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx - @@ -288,7 +288,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false - // for developers about possible regressions, we won't render buttons in development mode. - // For more information about these differences and how to test in development mode, - // see`Expensify/App/contributingGuides/APPLE_GOOGLE_SIGNIN.md` - - CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.DEV && ( - + ( - - { + @@ -246,7 +246,7 @@ const mainWindow = (): Promise => { + let deeplinkUrl: string; + let browserWindow: BrowserWindow; + + - const loadURL = __DEV__ ? (win: BrowserWindow): Promise => win.loadURL(`https://dev.new.expensify.com:${port}`) : serve({directory: `${__dirname}/www`}); + + const loadURL = __DEV__ ? (win: BrowserWindow): Promise => win.loadURL(`http://localhost:${port}`) : serve({directory: `${__dirname}/www`}); + + // Prod and staging set the icon in the electron-builder config, so only update it here for dev + if (__DEV__) { + ``` -The DeepLinkWrapper component will not handle deep links in the development environment. To be able to test deep linking, you must set the environment to something other than "Development". diff --git a/docs/Hidden/Instructions b/docs/Hidden/Instructions new file mode 100644 index 000000000000..940c7ab60d10 --- /dev/null +++ b/docs/Hidden/Instructions @@ -0,0 +1 @@ +This folder is used to house articles that should not be live articles on the helpsite. diff --git a/docs/articles/new-expensify/expensify-card/Dispute-Expensify-Card-transaction.md b/docs/articles/new-expensify/expensify-card/Dispute-Expensify-Card-transaction.md new file mode 100644 index 000000000000..5bd23cd53730 --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Dispute-Expensify-Card-transaction.md @@ -0,0 +1,73 @@ +--- +title: Dispute Expensify Card transaction +description: Dispute an unrecognized, unauthorized, or fraudulent charge +--- +
+ +When using your Expensify Visa® Commercial Card, you may come across transactions that you want to dispute, including: + +- Unrecognized, unauthorized, or fraudulent charges + - Charges made with your card after it was lost or stolen + - Unauthorized charges while your card is still in your possession + - Continued charges for a canceled recurring subscription +- Service disputes + - Damaged or defective merchandise + - Charges for merchandise that was never received + - Duplicate charges for a single transaction + - Transactions of an incorrect amount + - Refund not received after a return + +# Dispute a transaction + +If you spot a transaction error on your Expensify Card, + +1. Contact the merchant. They can often address the issue promptly. +2. If you are unable to resolve the issue with the merchant, contact us immediately by opening your chat with Expensify Concierge in your Expensify Chat inbox, or by emailing concierge@expensify.com to start the dispute process. Provide the following information: + - Details about the disputed charge, including why you’re disputing it, what occurred, and any steps you’ve taken to address the issue + - Supporting documentation like receipts or cancellation confirmations +3. If you suspect fraud on your Expensify Card, immediately deactivate your card: + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +
    +
  1. Click your profile image or icon in the bottom left menu.
  2. +
  3. Click Wallet in the left menu.
  4. +
  5. Click your Expensify Card.
  6. +
  7. Click Report card fraud.
  8. +
  9. Follow the prompts to deactivate your card and request a new one.
  10. +
+{% include end-option.html %} + +{% include option.html value="mobile" %} +
    +
  1. Tap your profile image or icon in the bottom menu.
  2. +
  3. Tap Wallet.
  4. +
  5. Tap your Expensify Card.
  6. +
  7. Tap Report card fraud.
  8. +
  9. Follow the prompts to deactivate your card and request a new one.
  10. +
+{% include end-option.html %} + +{% include end-selector.html %} + +{:start="4"} +4. [Enable Two-Factor Authentication (2FA)](https://help.expensify.com/articles/new-expensify/settings/Enable-Two-Factor-Authentication) to add an additional layer of security to your account. + +{% include faq-begin.md %} + +**How am I protected from fraud using the Expensify Card?** + +Expensify leverages sophisticated algorithms to detect and/or block unusual card activity. You can also enable real-time notifications to receive alerts each time your card is charged. + +**How long does the dispute process take?** + +The dispute process can take up to 90 days. + +**Can I cancel a dispute?** + +You can cancel a filed dispute by using your Expensify Chat thread with Concierge or by emailing concierge@expensify.com. + +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/expensify-card/Update-your-Expensify-Card-mailing-address.md b/docs/articles/new-expensify/expensify-card/Update-your-Expensify-Card-mailing-address.md new file mode 100644 index 000000000000..6ce53b6a359a --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Update-your-Expensify-Card-mailing-address.md @@ -0,0 +1,29 @@ +--- +title: Update your Expensify Card mailing address +description: Change your mailing address for your Expensify Card +--- +
+ +1. Hover over Settings, then click **Account**. +2. Click the **Credit Card Import** tab. +3. Click **Request a New Card** on your physical card pending activation. +4. Select **I lost my card**. + +{% include info.html %} +If you’re updating your address to receive your new Expensify Visa® Commercial Card, you’ll still select **I lost my card** even though you have not lost a card. +{% include end-info.html %} + +{:start="5"} +5. Confirm your details and click **Continue**. +6. Update your address and click **Continue**. + +{% include info.html %} +If you’re updating your address to receive your new Expensify Visa® Commercial Card, you can click the X in the right corner to end the process here if the new card has not been shipped out to you yet. However, if the new card has already been shipped out to an incorrect address, proceed to the next step to resend the card to the newly updated address. +{% include end-info.html %} + +{:start="7"} +7. Proceed with the card replacement. + +Your new card will arrive in 2-3 business days. + +
diff --git a/docs/assets/images/AdminissuedVirtualCards.png b/docs/assets/images/AdminissuedVirtualCards.png index 88df9b2f3fec..9c44763f5840 100644 Binary files a/docs/assets/images/AdminissuedVirtualCards.png and b/docs/assets/images/AdminissuedVirtualCards.png differ diff --git a/docs/assets/images/domains.svg b/docs/assets/images/domains.svg index 3a3c95604b79..12c0a0a0792b 100644 --- a/docs/assets/images/domains.svg +++ b/docs/assets/images/domains.svg @@ -1,44 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/assets/images/plane.svg b/docs/assets/images/plane.svg index 0295aa3c66c0..bd7fceba3607 100644 --- a/docs/assets/images/plane.svg +++ b/docs/assets/images/plane.svg @@ -1,34 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/redirects.csv b/docs/redirects.csv index 5e4d06619653..3042dc79085c 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -26,10 +26,31 @@ https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vac https://community.expensify.com/discussion/5274/how-to-set-up-an-adp-indirect-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/expenses/Distance-Tracking https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings -https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://community.expensify.com/discussion/3498/how-do-i-invite-users-in-my-company,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/6015/tutorial,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/2596/setting-up-accounts,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/7665/how-do-i-add-another-person-to-my-account-to-keep-track-of-there-expenses,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/7456/how-do-i-submit-an-expense-for-reimbursement,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/1460/schedule-a-demo,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/835/what-is-the-difference-between-a-category-and-a-tag,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/7703/getting-started-video,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/1845/how-to-set-up-account-and-add-users,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/8629/employee-training-e-learning-program,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/1607/on-demand-webinars,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5444/admin-onboarding-webinar-faqs,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5417/employee-training-webinar-faqs,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5885/overview-the-employee-training-webinar,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5854/overview-the-expensify-admin-onboarding-webinar,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/4699/how-to-download-the-mobile-app,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/4524/how-to-set-up-the-uber-integration,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5212/how-to-connect-your-policy-to-netsuite-token-based-authentication,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Add-profile-photo +https://community.expensify.com/discussion/5922/deep-dive-day-1-with-expensify-for-submitters,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5934/day-1-with-expensify-admins-and-accountants,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5694/deep-dive-admin-training-and-setup-resources,https://help.expensify.com/expensify-classic/hubs/getting-started https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Perks.html,https://use.expensify.com/company-credit-card https://help.expensify.com/articles/expensify-classic/expensify-partner-program/How-to-Join-the-ExpensifyApproved!-Partner-Program.html,https://use.expensify.com/accountants-program https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners, https://use.expensify.com/blog/maximizing-rewards-expensifyapproved-accounting-partners-now-earn-0-5-revenue-share @@ -176,4 +197,5 @@ https://help.expensify.com/articles/new-expensify/workspaces/The-Free-Plan,https https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account https://help.expensify.com/articles/new-expensify/settings/Security,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security https://help.expensify.com/articles/expensify-classic/workspaces/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency +https://help.expensify.com/articles/new-expensify/chat/Expensify-Chat-For-Admins,https://help.expensify.com/new-expensify/hubs/chat/ https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.html,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9c95f45f3ddd..8337041077be 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.77 + 1.4.78 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.77.0 + 1.4.78.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e88964613a93..7b076baeb35a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.77 + 1.4.78 CFBundleSignature ???? CFBundleVersion - 1.4.77.0 + 1.4.78.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 311360dd247f..014ab9966e82 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.77 + 1.4.78 CFBundleVersion - 1.4.77.0 + 1.4.78.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 3275ee64d44f..4b013d48a02d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.77-0", + "version": "1.4.78-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.77-0", + "version": "1.4.78-3", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.76", + "@expensify/react-native-live-markdown": "0.1.70", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -59,7 +59,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#18fa764be9d68f72b48d238dcc20f2b0ca8f1147", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -73,7 +73,6 @@ "mapbox-gl": "^2.15.0", "onfido-sdk-ui": "14.15.0", "process": "^0.11.10", - "prop-types": "^15.7.2", "pusher-js": "8.3.0", "react": "18.2.0", "react-beautiful-dnd": "^13.1.1", @@ -124,7 +123,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", "react-native-vision-camera": "^4.0.0-beta.13", - "react-native-web": "^0.19.9", + "react-native-web": "^0.19.12", "react-native-web-linear-gradient": "^1.1.2", "react-native-web-sound": "^0.1.3", "react-native-webview": "13.6.4", @@ -3559,14 +3558,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.76", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.76.tgz", - "integrity": "sha512-JUXiLg0Y2FJiVOfZKRgoOP1no8ThPaJ6MBc122UsW6SG53OvS12MTHfgfKHjXRH1nIGro/p9ekYb8GAzpp+kdw==", - "workspaces": [ - "parser", - "example", - "WebExample" - ], + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.70.tgz", + "integrity": "sha512-HyqBtZyvuJFB4gIUECKIMxWCnTPlPj+GPWmw80VzMBRFV9QiFRKUKRWefNEJ1cXV5hl8a6oOWDQla+dCnjCzOQ==", "engines": { "node": ">= 18.0.0" }, @@ -20342,8 +20336,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#18fa764be9d68f72b48d238dcc20f2b0ca8f1147", - "integrity": "sha512-AbeXop0pAVnkOJ7uVShqF7q9xwOYADW1mit0kK73ADkNuuQuHCYTqQSsQDuLaG80c5N96h+NZF/9LvcrhU2aFw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", + "integrity": "sha512-uy1+axUTTuPKwAR06xNG/tGIJ+uaavmSQgKiNU7pQVR94ibNzDD2WESn2E7OEP9/QrHa61lfFlluTjFvvz5I8Q==", "license": "MIT", "dependencies": { "classnames": "2.5.0", @@ -31788,11 +31782,12 @@ } }, "node_modules/react-native-web": { - "version": "0.19.9", - "license": "MIT", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.12.tgz", + "integrity": "sha512-o2T0oztoVDQjztt4YksO9S1XRjoH/AqcSvifgWLrPJgGVbMWsfhILgl6lfUdEamVZzZSVV/2gqDVMAk/qq7mZw==", "dependencies": { "@babel/runtime": "^7.18.6", - "@react-native/normalize-color": "^2.1.0", + "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^6.0.1", "memoize-one": "^6.0.0", @@ -31822,6 +31817,11 @@ "react-native-web": "*" } }, + "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { + "version": "0.74.81", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.81.tgz", + "integrity": "sha512-g3YvkLO7UsSWiDfYAU+gLhRHtEpUyz732lZB+N8IlLXc5MnfXHC8GKneDGY3Mh52I3gBrs20o37D5viQX9E1CA==" + }, "node_modules/react-native-web/node_modules/memoize-one": { "version": "6.0.0", "license": "MIT" diff --git a/package.json b/package.json index b3c7267abe73..f139d469d944 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.77-0", + "version": "1.4.78-3", "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.", @@ -65,7 +65,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.76", + "@expensify/react-native-live-markdown": "0.1.70", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -111,7 +111,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#18fa764be9d68f72b48d238dcc20f2b0ca8f1147", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -125,7 +125,6 @@ "mapbox-gl": "^2.15.0", "onfido-sdk-ui": "14.15.0", "process": "^0.11.10", - "prop-types": "^15.7.2", "pusher-js": "8.3.0", "react": "18.2.0", "react-beautiful-dnd": "^13.1.1", @@ -176,7 +175,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", "react-native-vision-camera": "^4.0.0-beta.13", - "react-native-web": "^0.19.9", + "react-native-web": "^0.19.12", "react-native-web-linear-gradient": "^1.1.2", "react-native-web-sound": "^0.1.3", "react-native-webview": "13.6.4", diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.12+001+initial.patch similarity index 98% rename from patches/react-native-web+0.19.9+001+initial.patch rename to patches/react-native-web+0.19.12+001+initial.patch index 91ba6bfd59c0..c77cfc7829ed 100644 --- a/patches/react-native-web+0.19.9+001+initial.patch +++ b/patches/react-native-web+0.19.12+001+initial.patch @@ -1,9 +1,9 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index c879838..0c9dfcb 100644 +index e137def..c3e5054 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js @@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[missing-local-annot] + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. constructor(_props) { - var _this$props$updateCel; @@ -243,7 +243,7 @@ index c879838..0c9dfcb 100644 }); this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1307,8 +1360,12 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1317,8 +1370,12 @@ class VirtualizedList extends StateSafePureComponent { onStartReached = _this$props8.onStartReached, onStartReachedThreshold = _this$props8.onStartReachedThreshold, onEndReached = _this$props8.onEndReached, @@ -258,7 +258,7 @@ index c879838..0c9dfcb 100644 var _this$_scrollMetrics2 = this._scrollMetrics, contentLength = _this$_scrollMetrics2.contentLength, visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1348,16 +1405,10 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1358,16 +1415,10 @@ class VirtualizedList extends StateSafePureComponent { // and call onStartReached only once for a given content length, // and only if onEndReached is not being executed else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { @@ -279,9 +279,9 @@ index c879838..0c9dfcb 100644 } // If the user scrolls away from the start or end and back again, -@@ -1412,6 +1463,11 @@ class VirtualizedList extends StateSafePureComponent { - } - } +@@ -1433,6 +1484,11 @@ class VirtualizedList extends StateSafePureComponent { + */ + _updateViewableItems(props, cellsAroundViewport) { + // If we have any pending scroll updates it means that the scroll metrics + // are out of date and we should not call any of the visibility callbacks. @@ -292,7 +292,7 @@ index c879838..0c9dfcb 100644 tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); }); diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index c7d68bb..43f9653 100644 +index c7d68bb..459f017 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js @@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { diff --git a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch b/patches/react-native-web+0.19.12+002+fixLastSpacer.patch similarity index 100% rename from patches/react-native-web+0.19.9+004+fixLastSpacer.patch rename to patches/react-native-web+0.19.12+002+fixLastSpacer.patch diff --git a/patches/react-native-web+0.19.9+005+image-header-support.patch b/patches/react-native-web+0.19.12+003+image-header-support.patch similarity index 95% rename from patches/react-native-web+0.19.9+005+image-header-support.patch rename to patches/react-native-web+0.19.12+003+image-header-support.patch index 4652e22662f0..6652f0345cc4 100644 --- a/patches/react-native-web+0.19.9+005+image-header-support.patch +++ b/patches/react-native-web+0.19.12+003+image-header-support.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js -index 95355d5..19109fc 100644 +index 9649d27..3281cc8 100644 --- a/node_modules/react-native-web/dist/exports/Image/index.js +++ b/node_modules/react-native-web/dist/exports/Image/index.js @@ -135,7 +135,22 @@ function resolveAssetUri(source) { @@ -23,10 +23,10 @@ index 95355d5..19109fc 100644 + return a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers); +} +var BaseImage = /*#__PURE__*/React.forwardRef((props, ref) => { - var ariaLabel = props['aria-label'], + var _ariaLabel = props['aria-label'], + accessibilityLabel = props.accessibilityLabel, blurRadius = props.blurRadius, - defaultSource = props.defaultSource, -@@ -236,16 +251,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { +@@ -238,16 +253,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { } }, function error() { updateState(ERRORED); @@ -47,7 +47,7 @@ index 95355d5..19109fc 100644 }); } function abortPendingRequest() { -@@ -277,10 +286,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { +@@ -279,10 +288,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { suppressHydrationWarning: true }), hiddenImage, createTintColorSVG(tintColor, filterRef.current)); }); @@ -129,7 +129,7 @@ index 95355d5..19109fc 100644 ImageLoader.getSize(uri, success, failure); }; diff --git a/node_modules/react-native-web/dist/modules/ImageLoader/index.js b/node_modules/react-native-web/dist/modules/ImageLoader/index.js -index bc06a87..e309394 100644 +index bc06a87..5a22819 100644 --- a/node_modules/react-native-web/dist/modules/ImageLoader/index.js +++ b/node_modules/react-native-web/dist/modules/ImageLoader/index.js @@ -76,7 +76,7 @@ var ImageLoader = { diff --git a/patches/react-native-web+0.19.9+006+fixPointerEventDown.patch b/patches/react-native-web+0.19.12+004+fixPointerEventDown.patch similarity index 100% rename from patches/react-native-web+0.19.9+006+fixPointerEventDown.patch rename to patches/react-native-web+0.19.12+004+fixPointerEventDown.patch diff --git a/patches/react-native-web+0.19.9+007+osr-improvement.patch b/patches/react-native-web+0.19.12+005+osr-improvement.patch similarity index 100% rename from patches/react-native-web+0.19.9+007+osr-improvement.patch rename to patches/react-native-web+0.19.12+005+osr-improvement.patch diff --git a/patches/react-native-web+0.19.9+002+measureInWindow.patch b/patches/react-native-web+0.19.9+002+measureInWindow.patch deleted file mode 100644 index f41b4b3b48cb..000000000000 --- a/patches/react-native-web+0.19.9+002+measureInWindow.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/exports/UIManager/index.js b/node_modules/react-native-web/dist/exports/UIManager/index.js -index 15b71d5..46b9e01 100644 ---- a/node_modules/react-native-web/dist/exports/UIManager/index.js -+++ b/node_modules/react-native-web/dist/exports/UIManager/index.js -@@ -77,7 +77,7 @@ var UIManager = { - measureInWindow(node, callback) { - if (node) { - setTimeout(() => { -- var _getRect2 = getRect(node), -+ var _getRect2 = node.getBoundingClientRect(), - height = _getRect2.height, - left = _getRect2.left, - top = _getRect2.top, diff --git a/src/CONST.ts b/src/CONST.ts index 601258890e33..de4e3305eddc 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -73,7 +73,6 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { - RECENT_WAYPOINTS_NUMBER: 20, DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], // Note: Group and Self-DM excluded as these are not tied to a Workspace @@ -152,6 +151,7 @@ const CONST = { DISPLAY_NAME: { MAX_LENGTH: 50, RESERVED_NAMES: ['Expensify', 'Concierge'], + EXPENSIFY_CONCIERGE: 'Expensify Concierge', }, GPS: { @@ -661,9 +661,9 @@ const CONST = { DELETED_ACCOUNT: 'DELETEDACCOUNT', // OldDot Action DISMISSED_VIOLATION: 'DISMISSEDVIOLATION', DONATION: 'DONATION', // OldDot Action - EXPORTED_TO_CSV: 'EXPORTEDTOCSV', // OldDot Action - EXPORTED_TO_INTEGRATION: 'EXPORTEDTOINTEGRATION', // OldDot Action - EXPORTED_TO_QUICK_BOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action + EXPORTED_TO_CSV: 'EXPORTCSV', // OldDot Action + EXPORTED_TO_INTEGRATION: 'EXPORTINTEGRATION', // OldDot Action + EXPORTED_TO_QUICK_BOOKS: 'EXPORTED', // OldDot Action FORWARDED: 'FORWARDED', // OldDot Action HOLD: 'HOLD', HOLD_COMMENT: 'HOLDCOMMENT', @@ -685,6 +685,7 @@ const CONST = { REIMBURSEMENT_DEQUEUED: 'REIMBURSEMENTDEQUEUED', REIMBURSEMENT_REQUESTED: 'REIMBURSEMENTREQUESTED', // OldDot Action REIMBURSEMENT_SETUP: 'REIMBURSEMENTSETUP', // OldDot Action + REIMBURSEMENT_SETUP_REQUESTED: 'REIMBURSEMENTSETUPREQUESTED', // OldDot Action RENAMED: 'RENAMED', REPORT_PREVIEW: 'REPORTPREVIEW', SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action @@ -931,6 +932,7 @@ const CONST = { RECEIPT: 'receipt', DATE: 'date', MERCHANT: 'merchant', + DESCRIPTION: 'description', FROM: 'from', TO: 'to', CATEGORY: 'category', @@ -1184,6 +1186,10 @@ const CONST = { WEBP: 'image/webp', JPEG: 'image/jpeg', }, + ATTACHMENT_TYPE: { + REPORT: 'r', + NOTE: 'n', + }, IMAGE_OBJECT_POSITION: { TOP: 'top', @@ -1303,12 +1309,13 @@ const CONST = { SYNC: 'sync', ENABLE_NEW_CATEGORIES: 'enableNewCategories', EXPORT: 'export', + TENANT_ID: 'tenantID', IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', INVOICE_STATUS: { - AWAITING_PAYMENT: 'AWT_PAYMENT', DRAFT: 'DRAFT', AWAITING_APPROVAL: 'AWT_APPROVAL', + AWAITING_PAYMENT: 'AWT_PAYMENT', }, IMPORT_TRACKING_CATEGORIES: 'importTrackingCategories', MAPPINGS: 'mappings', @@ -1593,6 +1600,9 @@ const CONST = { ACCOUNTANT: 'accountant', }, }, + ACCESS_VARIANTS: { + CREATE: 'create', + }, }, GROWL: { @@ -1773,7 +1783,8 @@ const CONST = { XERO: 'xero', }, SYNC_STAGE_NAME: { - STARTING_IMPORT: 'startingImport', + STARTING_IMPORT_QBO: 'startingImportQBO', + STARTING_IMPORT_XERO: 'startingImportXero', QBO_IMPORT_MAIN: 'quickbooksOnlineImportMain', QBO_IMPORT_CUSTOMERS: 'quickbooksOnlineImportCustomers', QBO_IMPORT_EMPLOYEES: 'quickbooksOnlineImportEmployees', @@ -1919,8 +1930,8 @@ const CONST = { // Extract attachment's source from the data's html string ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g, - EMOJI_NAME: /:[\w+-]+:/g, - EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/, + EMOJI_NAME: /:[\p{L}0-9_+-]+:/gu, + EMOJI_SUGGESTIONS: /:[\p{L}0-9_+-]{1,40}$/u, AFTER_FIRST_LINE_BREAK: /\n.*/g, LINE_BREAK: /\r\n|\r|\n/g, CODE_2FA: /^\d{6}$/, @@ -2083,7 +2094,6 @@ const CONST = { INFO: 'info', }, REPORT_DETAILS_MENU_ITEM: { - SHARE_CODE: 'shareCode', MEMBERS: 'member', INVITE: 'invite', SETTINGS: 'settings', @@ -3734,7 +3744,7 @@ const CONST = { ONBOARDING_INTRODUCTION: 'Let’s get you set up 🔧', ONBOARDING_CHOICES: {...onboardingChoices}, - + 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' + @@ -4776,6 +4786,11 @@ const CONST = { REFERRER: { NOTIFICATION: 'notification', }, + + SORT_ORDER: { + ASC: 'asc', + DESC: 'desc', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 7a6203a44068..ddc4b5f88a69 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -269,6 +269,8 @@ function Expensify({ ); } +Expensify.displayName = 'Expensify'; + export default withOnyx({ isCheckingPublicRoom: { key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a027a8493b41..86a4a0a31716 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -133,9 +133,6 @@ const ONYXKEYS = { /** This NVP holds to most recent waypoints that a person has used when creating a distance expense */ NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints', - /** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */ - NVP_HAS_DISMISSED_IDLE_PANEL: 'nvp_hasDismissedIdlePanel', - /** This NVP contains the choice that the user made on the engagement modal */ NVP_INTRO_SELECTED: 'nvp_introSelected', @@ -589,7 +586,10 @@ type OnyxValuesMapping = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; + + // NVP_ONBOARDING is an array for old users. [ONYXKEYS.NVP_ONBOARDING]: Onboarding | []; + [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; @@ -628,7 +628,6 @@ type OnyxValuesMapping = { [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; - [ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean; [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected; [ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2bc04c4a99ea..5947b7b03a95 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2,6 +2,8 @@ import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; +import type {CentralPaneNavigatorParamList} from './libs/Navigation/types'; +import type {SearchQuery} from './types/onyx/SearchResults'; import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; // This is a file containing constants for all the routes we want to be able to go to @@ -14,10 +16,20 @@ function getUrlWithBackToParam(url: TUrl, backTo?: string): return `${url}${backToParam}` as const; } -const ROUTES = { +const PUBLIC_SCREENS_ROUTES = { // If the user opens this route, we'll redirect them to the path saved in the last visited path or to the home page if the last visited path is empty. ROOT: '', + TRANSITION_BETWEEN_APPS: 'transition', + CONNECTION_COMPLETE: 'connection-complete', + VALIDATE_LOGIN: 'v/:accountID/:validateCode', + UNLINK_LOGIN: 'u/:accountID/:validateCode', + APPLE_SIGN_IN: 'sign-in-with-apple', + GOOGLE_SIGN_IN: 'sign-in-with-google', + SAML_SIGN_IN: 'sign-in-with-saml', +} as const; +const ROUTES = { + ...PUBLIC_SCREENS_ROUTES, // This route renders the list of reports. HOME: 'home', @@ -25,7 +37,15 @@ const ROUTES = { SEARCH: { route: '/search/:query', - getRoute: (query: string) => `search/${query}` as const, + getRoute: (searchQuery: SearchQuery, queryParams?: CentralPaneNavigatorParamList['Search_Central_Pane']) => { + const {sortBy, sortOrder} = queryParams ?? {}; + + if (!sortBy && !sortOrder) { + return `search/${searchQuery}` as const; + } + + return `search/${searchQuery}?sortBy=${sortBy}&sortOrder=${sortOrder}` as const; + }, }, SEARCH_REPORT: { @@ -53,18 +73,11 @@ const ROUTES = { getRoute: (accountID: string | number) => `a/${accountID}/avatar` as const, }, - TRANSITION_BETWEEN_APPS: 'transition', - VALIDATE_LOGIN: 'v/:accountID/:validateCode', - CONNECTION_COMPLETE: 'connection-complete', GET_ASSISTANCE: { route: 'get-assistance/:taskID', getRoute: (taskID: string, backTo: string) => getUrlWithBackToParam(`get-assistance/${taskID}`, backTo), }, - UNLINK_LOGIN: 'u/:accountID/:validateCode', - APPLE_SIGN_IN: 'sign-in-with-apple', - GOOGLE_SIGN_IN: 'sign-in-with-google', DESKTOP_SIGN_IN_REDIRECT: 'desktop-signin-redirect', - SAML_SIGN_IN: 'sign-in-with-saml', // This is a special validation URL that will take the user to /workspace/new after validation. This is used // when linking users from e.com in order to share a session in this app. @@ -88,6 +101,7 @@ const ROUTES = { SETTINGS_TIMEZONE_SELECT: 'settings/profile/timezone/select', SETTINGS_PRONOUNS: 'settings/profile/pronouns', SETTINGS_PREFERENCES: 'settings/preferences', + SETTINGS_SUBSCRIPTION: 'settings/subscription', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', SETTINGS_THEME: 'settings/preferences/theme', @@ -182,7 +196,10 @@ const ROUTES = { SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date', SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time', SETTINGS_TROUBLESHOOT: 'settings/troubleshoot', - SETTINGS_CONSOLE: 'settings/troubleshoot/console', + SETTINGS_CONSOLE: { + route: 'settings/troubleshoot/console', + getRoute: (backTo?: string) => getUrlWithBackToParam(`settings/troubleshoot/console`, backTo), + }, SETTINGS_SHARE_LOG: { route: 'settings/troubleshoot/console/share-log', getRoute: (source: string) => `settings/troubleshoot/console/share-log?source=${encodeURI(source)}` as const, @@ -234,9 +251,10 @@ const ROUTES = { route: 'r/:reportID/details/shareCode', getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const, }, - REPORT_ATTACHMENTS: { - route: 'r/:reportID/attachment', - getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURIComponent(source)}` as const, + ATTACHMENTS: { + route: 'attachment', + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number) => + `attachment?source=${encodeURIComponent(url)}&type=${type}${reportID ? `&reportID=${reportID}` : ''}${accountID ? `&accountID=${accountID}` : ''}` as const, }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -262,13 +280,9 @@ const ROUTES = { route: 'r/:reportID/settings', getRoute: (reportID: string) => `r/${reportID}/settings` as const, }, - REPORT_SETTINGS_ROOM_NAME: { - route: 'r/:reportID/settings/room-name', - getRoute: (reportID: string) => `r/${reportID}/settings/room-name` as const, - }, - REPORT_SETTINGS_GROUP_NAME: { - route: 'r/:reportID/settings/group-name', - getRoute: (reportID: string) => `r/${reportID}/settings/group-name` as const, + REPORT_SETTINGS_NAME: { + route: 'r/:reportID/settings/name', + getRoute: (reportID: string) => `r/${reportID}/settings/name` as const, }, REPORT_SETTINGS_NOTIFICATION_PREFERENCES: { route: 'r/:reportID/settings/notification-preferences', @@ -672,12 +686,12 @@ const ROUTES = { getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tags/${orderWeight}/edit` as const, }, WORKSPACE_TAG_EDIT: { - route: 'settings/workspace/:policyID/tag/:tagName/edit', - getRoute: (policyID: string, tagName: string) => `settings/workspace/${policyID}/tag/${encodeURIComponent(tagName)}/edit` as const, + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/edit', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/edit` as const, }, WORKSPACE_TAG_SETTINGS: { - route: 'settings/workspaces/:policyID/tag/:tagName', - getRoute: (policyID: string, tagName: string) => `settings/workspaces/${policyID}/tag/${encodeURIComponent(tagName)}` as const, + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}` as const, }, WORKSPACE_TAG_LIST_VIEW: { route: 'settings/workspaces/:policyID/tag-list/:orderWeight', @@ -795,17 +809,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories` as const, }, - POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS: { - route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/cost-centers', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/cost-centers` as const, - }, - POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION: { - route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/region', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/region` as const, + POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP: { + route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/mapping/:categoryId/:categoryName', + getRoute: (policyID: string, categoryId: string, categoryName: string) => + `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/mapping/${categoryId}/${encodeURIComponent(categoryName)}` as const, }, POLICY_ACCOUNTING_XERO_CUSTOMER: { - route: '/settings/workspaces/:policyID/accounting/xero/import/customers', - getRoute: (policyID: string) => `/settings/workspaces/${policyID}/accounting/xero/import/customers` as const, + route: 'settings/workspaces/:policyID/accounting/xero/import/customers', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/customers` as const, }, POLICY_ACCOUNTING_XERO_TAXES: { route: 'settings/workspaces/:policyID/accounting/xero/import/taxes', @@ -816,8 +827,8 @@ const ROUTES = { getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export` as const, }, POLICY_ACCOUNTING_XERO_PREFERRED_EXPORTER_SELECT: { - route: '/settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', - getRoute: (policyID: string) => `/settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, + route: 'settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, }, POLICY_ACCOUNTING_XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: { route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-date-select', @@ -831,6 +842,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/advanced', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced` as const, }, + POLICY_ACCOUNTING_XERO_BILL_STATUS_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-status-selector', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export/purchase-bill-status-selector` as const, + }, POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR: { route: 'settings/workspaces/:policyID/accounting/xero/advanced/invoice-account-selector', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced/invoice-account-selector` as const, @@ -875,7 +890,7 @@ const HYBRID_APP_ROUTES = { MONEY_REQUEST_SUBMIT_CREATE: '/submit/new/scan', } as const; -export {HYBRID_APP_ROUTES, getUrlWithBackToParam}; +export {HYBRID_APP_ROUTES, getUrlWithBackToParam, PUBLIC_SCREENS_ROUTES}; export default ROUTES; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f74002312623..427f25a32ac5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -7,7 +7,7 @@ import type DeepValueOf from './types/utils/DeepValueOf'; const PROTECTED_SCREENS = { HOME: 'Home', CONCIERGE: 'Concierge', - REPORT_ATTACHMENTS: 'ReportAttachments', + ATTACHMENTS: 'Attachments', } as const; const SCREENS = { @@ -103,6 +103,10 @@ const SCREENS = { RESPONSE: 'Settings_ExitSurvey_Response', CONFIRM: 'Settings_ExitSurvey_Confirm', }, + + SUBSCRIPTION: { + ROOT: 'Settings_Subscription', + }, }, SAVE_THE_WORLD: { ROOT: 'SaveTheWorld_Root', @@ -184,8 +188,7 @@ const SCREENS = { REPORT_SETTINGS: { ROOT: 'Report_Settings_Root', - ROOM_NAME: 'Report_Settings_Room_Name', - GROUP_NAME: 'Report_Settings_Group_Name', + NAME: 'Report_Settings_Name', NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', WRITE_CAPABILITY: 'Report_Settings_Write_Capability', VISIBILITY: 'Report_Settings_Visibility', @@ -244,11 +247,11 @@ const SCREENS = { XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', XERO_TRACKING_CATEGORIES: 'Policy_Accounting_Xero_Tracking_Categories', - XERO_MAP_COST_CENTERS: 'Policy_Accounting_Xero_Map_Cost_Centers', - XERO_MAP_REGION: 'Policy_Accounting_Xero_Map_Region', + XERO_MAP_TRACKING_CATEGORY: 'Policy_Accounting_Xero_Map_Tracking_Category', XERO_EXPORT: 'Policy_Accounting_Xero_Export', XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: 'Policy_Accounting_Xero_Export_Purchase_Bill_Date_Select', XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced', + XERO_BILL_STATUS_SELECTOR: 'Policy_Accounting_Xero_Export_Bill_Status_Selector', XERO_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Invoice_Account_Selector', XERO_EXPORT_PREFERRED_EXPORTER_SELECT: 'Workspace_Accounting_Xero_Export_Preferred_Exporter_Select', XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index d1dc42bb4678..17a2f6212447 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -23,25 +23,7 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import listViewOverflow from './listViewOverflow'; -import type {AddressSearchProps, PredefinedPlace} from './types'; - -/** - * Check if the place matches the search by the place name or description. - * @param search The search string for a place - * @param place The place to check for a match on the search - * @returns true if search is related to place, otherwise it returns false. - */ -function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean { - if (!search) { - return true; - } - if (!place) { - return false; - } - const fullSearchSentence = `${place.name ?? ''} ${place.description}`; - return search.split(' ').every((searchTerm) => !searchTerm || (searchTerm && fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()))); -} +import type {AddressSearchProps} from './types'; // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the @@ -59,7 +41,6 @@ function AddressSearch( isLimitedToUSA = false, label, maxInputLength, - onFocus, onBlur, onInputChange, onPress, @@ -317,16 +298,10 @@ function AddressSearch( }; }, []); - const filteredPredefinedPlaces = useMemo(() => { - if (!isOffline || !searchValue) { - return predefinedPlaces ?? []; - } - return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? []; - }, [isOffline, predefinedPlaces, searchValue]); - const listEmptyComponent = useCallback( - () => (!isTyping ? null : {translate('common.noResultsFound')}), - [isTyping, styles, translate], + () => + !!isOffline || !isTyping ? null : {translate('common.noResultsFound')}, + [isOffline, isTyping, styles, translate], ); const listLoader = useCallback( @@ -363,10 +338,11 @@ function AddressSearch( ref={containerRef} > - {!!title && {title}} + {!!title && {title}} {subtitle}
); @@ -409,7 +385,6 @@ function AddressSearch( shouldSaveDraft, onFocus: () => { setIsFocused(true); - onFocus?.(); }, onBlur: (event) => { if (!isCurrentTargetInsideContainer(event, containerRef)) { @@ -439,18 +414,10 @@ function AddressSearch( }} styles={{ textInputContainer: [styles.flexColumn], - listView: [ - StyleUtils.getGoogleListViewStyle(displayListViewBorder), - listViewOverflow, - styles.borderLeft, - styles.borderRight, - styles.flexGrow0, - !isFocused && styles.h0, - ], + listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.overflowAuto, styles.borderLeft, styles.borderRight, !isFocused && {height: 0}], row: [styles.pv4, styles.ph3, styles.overflowAuto], description: [styles.googleSearchText], separator: [styles.googleSearchSeparator], - container: [styles.mh100], }} numberOfLines={2} isRowScrollable={false} @@ -474,13 +441,11 @@ function AddressSearch( ) } placeholder="" - listViewDisplayed - > - setLocationErrorCode(null)} - locationErrorCode={locationErrorCode} - /> - + /> + setLocationErrorCode(null)} + locationErrorCode={locationErrorCode} + />
{isFetchingCurrentLocation && } diff --git a/src/components/AddressSearch/listViewOverflow/index.native.ts b/src/components/AddressSearch/listViewOverflow/index.native.ts deleted file mode 100644 index 36b9f4005376..000000000000 --- a/src/components/AddressSearch/listViewOverflow/index.native.ts +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import {defaultStyles} from '@styles/index'; - -export default defaultStyles.overflowHidden; diff --git a/src/components/AddressSearch/listViewOverflow/index.ts b/src/components/AddressSearch/listViewOverflow/index.ts deleted file mode 100644 index ae8bf35cc80c..000000000000 --- a/src/components/AddressSearch/listViewOverflow/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import {defaultStyles} from '@styles/index'; - -export default defaultStyles.overflowAuto; diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 22cc3834b7a9..bc7acf3f7e40 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -24,10 +24,6 @@ type StreetValue = { street: string; }; -type PredefinedPlace = Place & { - name?: string; -}; - type AddressSearchProps = { /** The ID used to uniquely identify the input in a Form */ inputID?: string; @@ -35,9 +31,6 @@ type AddressSearchProps = { /** Saves a draft of the input value when used in a form */ shouldSaveDraft?: boolean; - /** Callback that is called when the text input is focused */ - onFocus?: () => void; - /** Callback that is called when the text input is blurred */ onBlur?: () => void; @@ -72,7 +65,7 @@ type AddressSearchProps = { canUseCurrentLocation?: boolean; /** A list of predefined places that can be shown when the user isn't searching for something */ - predefinedPlaces?: PredefinedPlace[] | null; + predefinedPlaces?: Place[] | null; /** A map of inputID key names */ renamedInputKeys?: Address; @@ -92,4 +85,4 @@ type AddressSearchProps = { type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean; -export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue, PredefinedPlace}; +export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue}; diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index 99a0ee3bf683..2212e7460a2a 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -6,9 +6,9 @@ import {StyleSheet} from 'react-native'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; @@ -30,7 +30,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', [], ); - const {isSmallScreenWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const linkProps: LinkProps = {}; if (onPress) { @@ -38,7 +38,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', } else { linkProps.href = href; } - const defaultTextStyle = DeviceCapabilities.canUseTouchScreen() || isSmallScreenWidth ? {} : {...styles.userSelectText, ...styles.cursorPointer}; + const defaultTextStyle = DeviceCapabilities.canUseTouchScreen() || shouldUseNarrowLayout ? {} : {...styles.userSelectText, ...styles.cursorPointer}; const isEmail = Str.isValidEmail(href.replace(/mailto:/i, '')); return ( diff --git a/src/components/AttachmentContext.ts b/src/components/AttachmentContext.ts new file mode 100644 index 000000000000..4ed6bdc9084f --- /dev/null +++ b/src/components/AttachmentContext.ts @@ -0,0 +1,22 @@ +import {createContext} from 'react'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type AttachmentContextProps = { + type?: ValueOf; + reportID?: string; + accountID?: number; +}; + +const AttachmentContext = createContext({ + type: undefined, + reportID: undefined, + accountID: undefined, +}); + +AttachmentContext.displayName = 'AttachmentContext'; + +export { + // eslint-disable-next-line import/prefer-default-export + AttachmentContext, +}; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index fb6a8e911e87..d1c027378563 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -5,8 +5,10 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import {useSharedValue} from 'react-native-reanimated'; +import type {ValueOf} 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 useWindowDimensions from '@hooks/useWindowDimensions'; @@ -100,6 +102,12 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { /** The report that has this attachment */ report?: OnyxEntry | EmptyObject; + /** The type of the attachment */ + type?: ValueOf; + + /** If the attachment originates from a note, the accountID will represent the author of that note. */ + accountID?: number; + /** Optional callback to fire when we want to do something after modal show. */ onModalShow?: () => void; @@ -155,6 +163,8 @@ function AttachmentModal({ onModalClose = () => {}, isLoading = false, shouldShowNotFoundPage = false, + type = undefined, + accountID = undefined, }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -170,8 +180,9 @@ function AttachmentModal({ const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1)); const [isDownloadButtonReadyToBeShown, setIsDownloadButtonReadyToBeShown] = React.useState(true); + const {windowWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const nope = useSharedValue(false); - const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]); @@ -451,7 +462,7 @@ function AttachmentModal({ let shouldShowThreeDotsButton = false; if (!isEmptyObject(report)) { headerTitleNew = translate(isReceiptAttachment ? 'common.receipt' : 'common.attachment'); - shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !isReceiptAttachment && !isOffline; + shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isReceiptAttachment && !isOffline; shouldShowThreeDotsButton = isReceiptAttachment && isModalOpen && threeDotsMenuItems.length !== 0; } const context = useMemo( @@ -486,14 +497,14 @@ function AttachmentModal({ propagateSwipe > - {isSmallScreenWidth && } + {shouldUseNarrowLayout && } downloadAttachment()} - shouldShowCloseButton={!isSmallScreenWidth} - shouldShowBackButton={isSmallScreenWidth} + shouldShowCloseButton={!shouldUseNarrowLayout} + shouldShowBackButton={shouldUseNarrowLayout} onBackButtonPress={closeModal} onCloseButtonPress={closeModal} shouldShowThreeDotsButton={shouldShowThreeDotsButton} @@ -515,35 +526,37 @@ function AttachmentModal({ onLinkPress={() => Navigation.dismissModal()} /> )} - {!isEmptyObject(report) && !isReceiptAttachment ? ( - - ) : ( - !!sourceForAttachmentView && - shouldLoadAttachment && - !isLoading && - !shouldShowNotFoundPage && ( - - - - ) - )} + {!shouldShowNotFoundPage && + (!isEmptyObject(report) && !isReceiptAttachment ? ( + + ) : ( + !!sourceForAttachmentView && + shouldLoadAttachment && + !isLoading && ( + + + + ) + ))} {/* If we have an onConfirm method show a confirmation button */} {!!onConfirm && ( @@ -553,7 +566,7 @@ function AttachmentModal({ - )} - {/** + + {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')} + + + )} + {/** These are the actionable buttons that appear at the bottom of a Concierge message for example: Invite a user mentioned but not a member of the room https://github.com/Expensify/App/issues/32741 */} - {actionableItemButtons.length > 0 && ( - - )} - - ) : ( - - )} + {actionableItemButtons.length > 0 && ( + + )} + + ) : ( + + )} + ); } diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index 82b49d1e260c..c537fedfe994 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -1,33 +1,40 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; import type {Attachment} from '@components/Attachments/types'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import Navigation from '@libs/Navigation/Navigation'; import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type ReportAttachmentsProps = StackScreenProps; +type ReportAttachmentsProps = StackScreenProps; function ReportAttachments({route}: ReportAttachmentsProps) { const reportID = route.params.reportID; + const type = route.params.type; + const accountID = route.params.accountID; const report = ReportUtils.getReport(reportID); + const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); // In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource const source = Number(route.params.source) || route.params.source; const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, String(attachment.source)); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID)); Navigation.navigate(routeToNavigate); }, - [reportID], + [reportID, accountID, type], ); return ( ); } diff --git a/src/pages/home/report/reportActionFragmentPropTypes.js b/src/pages/home/report/reportActionFragmentPropTypes.js deleted file mode 100644 index 5d2e3b951a1d..000000000000 --- a/src/pages/home/report/reportActionFragmentPropTypes.js +++ /dev/null @@ -1,32 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** The type of the action item fragment. Used to render a corresponding component */ - type: PropTypes.string.isRequired, - - /** The text content of the fragment. */ - text: PropTypes.string.isRequired, - - /** Used to apply additional styling. Style refers to a predetermined constant and not a class name. e.g. 'normal' - * or 'strong' - */ - style: PropTypes.string, - - /** ID of a report */ - reportID: PropTypes.string, - - /** ID of a policy */ - policyID: PropTypes.string, - - /** The target of a link fragment e.g. '_blank' */ - target: PropTypes.string, - - /** The destination of a link fragment e.g. 'https://www.expensify.com' */ - href: PropTypes.string, - - /** An additional avatar url - not the main avatar url but used within a message. */ - iconUrl: PropTypes.string, - - /** Fragment edited flag */ - isEdited: PropTypes.bool, -}); diff --git a/src/pages/home/report/reportActionPropTypes.js b/src/pages/home/report/reportActionPropTypes.js deleted file mode 100644 index 5f9643571e54..000000000000 --- a/src/pages/home/report/reportActionPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; -import reportActionFragmentPropTypes from './reportActionFragmentPropTypes'; - -export default { - /** The ID of the reportAction. It is the string representation of the a 64-bit integer. */ - reportActionID: PropTypes.string, - - /** Name of the action e.g. ADD_COMMENT */ - actionName: PropTypes.string, - - /** Person who created the action */ - person: PropTypes.arrayOf(reportActionFragmentPropTypes), - - /** ISO-formatted datetime */ - created: PropTypes.string, - - /** report action message */ - message: PropTypes.arrayOf(reportActionFragmentPropTypes), - - /** Original message associated with this action */ - originalMessage: PropTypes.shape({ - // The ID of the iou transaction - IOUTransactionID: PropTypes.string, - - /** accountIDs of the people to which the whisper was sent to (if any). Returns empty array if it is not a whisper */ - whisperedTo: PropTypes.arrayOf(PropTypes.number), - }), - - /** Error message that's come back from the server. */ - error: PropTypes.string, -}; diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 1bbf0d02a941..c1d55516b433 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -3,7 +3,6 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -13,13 +12,12 @@ import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as IOUUtils from '@libs/IOUUtils'; import * as KeyDownPressListener from '@libs/KeyboardShortcut/KeyDownPressListener'; import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as IOU from '@userActions/IOU'; import type {IOURequestType} from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -105,9 +103,6 @@ function IOURequestStartPage({ const isExpenseReport = ReportUtils.isExpenseReport(report); const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT; - // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense - const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || PolicyUtils.canSendInvoice(allPolicies); - const navigateBack = () => { Navigation.closeRHPFlow(); }; @@ -126,15 +121,21 @@ function IOURequestStartPage({ } return ( - - {({safeAreaPaddingBottomStyle}) => ( - + + {({safeAreaPaddingBottomStyle}) => ( - - )} - + )} + + ); } diff --git a/src/pages/iou/request/step/IOURequestStepDate.tsx b/src/pages/iou/request/step/IOURequestStepDate.tsx index 2fea4c0d52e1..3a221b9a56db 100644 --- a/src/pages/iou/request/step/IOURequestStepDate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDate.tsx @@ -176,8 +176,8 @@ const IOURequestStepDateWithOnyx = withOnyx { + HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); IOU.setMoneyRequestParticipants(transactionID, val); const rateID = DistanceRequestUtils.getCustomUnitRateID(val[0]?.reportID ?? ''); IOU.setCustomUnitRateID(transactionID, rateID); diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index 3bbff502d917..74a1370fb00c 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -8,6 +8,7 @@ import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {sortWorkspacesBySelected} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -41,21 +42,24 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte const workspaceOptions: WorkspaceListItem[] = useMemo(() => { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); - return activeAdminWorkspaces.map((policy) => ({ - text: policy.name, - value: policy.id, - keyForList: policy.id, - icons: [ - { - id: policy.id, - source: policy?.avatarURL ? policy.avatarURL : ReportUtils.getDefaultWorkspaceAvatar(policy.name), - fallbackIcon: Expensicons.FallbackWorkspaceAvatar, - name: policy.name, - type: CONST.ICON_TYPE_WORKSPACE, - }, - ], - isSelected: selectedWorkspace?.policyID === policy.id, - })); + + return activeAdminWorkspaces + .sort((policy1, policy2) => sortWorkspacesBySelected({policyID: policy1.id, name: policy1.name}, {policyID: policy2.id, name: policy2.name}, selectedWorkspace?.policyID)) + .map((policy) => ({ + text: policy.name, + value: policy.id, + keyForList: policy.id, + icons: [ + { + id: policy.id, + source: policy?.avatarURL ? policy.avatarURL : ReportUtils.getDefaultWorkspaceAvatar(policy.name), + fallbackIcon: Expensicons.FallbackWorkspaceAvatar, + name: policy.name, + type: CONST.ICON_TYPE_WORKSPACE, + }, + ], + isSelected: selectedWorkspace?.policyID === policy.id, + })); }, [allPolicies, selectedWorkspace]); const navigateBack = () => { diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index 374e27819e70..6c0eae98fb85 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -1,6 +1,7 @@ import {useNavigation} from '@react-navigation/native'; import React, {useMemo, useRef, useState} from 'react'; import type {TextInput} from 'react-native'; +import {View} from 'react-native'; import type {Place} from 'react-native-google-places-autocomplete'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -16,7 +17,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useLocationBias from '@hooks/useLocationBias'; import useNetwork from '@hooks/useNetwork'; -import useSubmitButtonVisibility from '@hooks/useSubmitButtonVisibility'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -57,7 +57,6 @@ function IOURequestStepWaypoint({ }: IOURequestStepWaypointProps) { const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); - const {isSubmitButtonVisible, showSubmitButton, hideSubmitButton, formStyle} = useSubmitButtonVisibility(); const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false); const navigation = useNavigation(); const isFocused = navigation.isFocused(); @@ -158,7 +157,6 @@ function IOURequestStepWaypoint({ onEntryTransitionEnd={() => textInput.current?.focus()} shouldEnableMaxHeight testID={IOURequestStepWaypoint.displayName} - style={styles.overflowHidden} > - { - textInput.current = e as unknown as TextInput; - }} - hint={!isOffline ? 'distance.error.selectSuggestedAddress' : ''} - containerStyles={[styles.mt4]} - label={translate('distance.address')} - defaultValue={waypointAddress} - onPress={selectWaypoint} - onFocus={hideSubmitButton} - onBlur={showSubmitButton} - maxInputLength={CONST.FORM_CHARACTER_LIMIT} - renamedInputKeys={{ - address: `waypoint${pageIndex}`, - city: '', - country: '', - street: '', - street2: '', - zipCode: '', - lat: '', - lng: '', - state: '', - }} - predefinedPlaces={recentWaypoints} - resultTypes="" - /> + + { + textInput.current = e as unknown as TextInput; + }} + hint={!isOffline ? 'distance.error.selectSuggestedAddress' : ''} + containerStyles={[styles.mt4]} + label={translate('distance.address')} + defaultValue={waypointAddress} + onPress={selectWaypoint} + maxInputLength={CONST.FORM_CHARACTER_LIMIT} + renamedInputKeys={{ + address: `waypoint${pageIndex}`, + city: '', + country: '', + street: '', + street2: '', + zipCode: '', + lat: '', + lng: '', + state: '', + }} + predefinedPlaces={recentWaypoints} + resultTypes="" + /> + @@ -249,10 +244,10 @@ export default withWritableReportOrNotFound( recentWaypoints: { key: ONYXKEYS.NVP_RECENT_WAYPOINTS, - // Only grab the most recent 20 waypoints because that's all that is shown in the UI. This also puts them into the format of data + // Only grab the most recent 5 waypoints because that's all that is shown in the UI. This also puts them into the format of data // that the google autocomplete component expects for it's "predefined places" feature. selector: (waypoints) => - (waypoints ? waypoints.slice(0, CONST.RECENT_WAYPOINTS_NUMBER) : []).map((waypoint) => ({ + (waypoints ? waypoints.slice(0, 5) : []).map((waypoint) => ({ name: waypoint.name, description: waypoint.address ?? '', geometry: { diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 62a7f7d73825..0a13bcd46a3e 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -1,12 +1,14 @@ import type {RouteProp} from '@react-navigation/core'; import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; -import React, {forwardRef} from 'react'; +import React, {forwardRef, useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; +import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -16,6 +18,9 @@ type WithWritableReportOrNotFoundOnyxProps = { /** The report corresponding to the reportID in the route params */ report: OnyxEntry; + /** Whether the reports are loading. When false it means they are ready to be used. */ + isLoadingApp: OnyxEntry; + /** The draft report corresponding to the reportID in the route params */ reportDraft: OnyxEntry; }; @@ -49,12 +54,27 @@ export default function , keyof WithWritableReportOrNotFoundOnyxProps>> { // eslint-disable-next-line rulesdir/no-negated-variables function WithWritableReportOrNotFound(props: TProps, ref: ForwardedRef) { - const {report = {reportID: ''}, route} = props; + const {report = {reportID: ''}, route, isLoadingApp = true} = props; const iouTypeParamIsInvalid = !Object.values(CONST.IOU.TYPE) .filter((type) => shouldIncludeDeprecatedIOUType || (type !== CONST.IOU.TYPE.REQUEST && type !== CONST.IOU.TYPE.SEND)) .includes(route.params?.iouType); + const isEditing = 'action' in route.params && route.params?.action === CONST.IOU.ACTION.EDIT; const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + useEffect(() => { + if (Boolean(report?.reportID) || !route.params.reportID) { + return; + } + + ReportActions.openReport(route.params.reportID); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (isEditing && isLoadingApp) { + return ; + } + if (iouTypeParamIsInvalid || !canUserPerformWriteAction) { return ; } @@ -74,6 +94,9 @@ export default function `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID ?? '0'}`, }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, reportDraft: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${route.params.reportID ?? '0'}`, }, diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index 132388365ada..aee11c89f22c 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -1,3 +1,5 @@ +import type {RouteProp} from '@react-navigation/native'; +import {useRoute} from '@react-navigation/native'; import {format} from 'date-fns'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; @@ -21,9 +23,11 @@ import type {Log} from '@libs/Console'; import localFileCreate from '@libs/localFileCreate'; import localFileDownload from '@libs/localFileDownload'; import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import type {CapturedLogs} from '@src/types/onyx'; type ConsolePageOnyxProps = { @@ -44,6 +48,8 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const route = useRoute>(); + const logsList = useMemo( () => Object.entries(logs ?? {}) @@ -114,7 +120,7 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { Navigation.goBack(ROUTES.SETTINGS_TROUBLESHOOT)} + onBackButtonPress={() => Navigation.goBack(route.params?.backTo)} /> Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_CONSOLE.getRoute())} /> typeof InteractionManager.runAfterInteractions, + onrejected?: () => typeof InteractionManager.runAfterInteractions, + ) => Promise; + done: (...args: Array) => typeof InteractionManager.runAfterInteractions; + cancel: () => void; + } | null>(null); + + useEffect( + () => () => { + if (!navigateBackToPreviousScreenTask.current) { + return; + } + + navigateBackToPreviousScreenTask.current.cancel(); + }, + [], + ); + const navigateBackToPreviousScreen = useCallback(() => Navigation.goBack(), []); const updateStatus = useCallback( ({emojiCode, statusText}: FormOnyxValues) => { @@ -90,7 +110,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '', }); User.clearDraftCustomStatus(); - InteractionManager.runAfterInteractions(() => { + navigateBackToPreviousScreenTask.current = InteractionManager.runAfterInteractions(() => { navigateBackToPreviousScreen(); }); }, @@ -106,7 +126,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) }); formRef.current?.resetForm({[INPUT_IDS.EMOJI_CODE]: ''}); - InteractionManager.runAfterInteractions(() => { + navigateBackToPreviousScreenTask.current = InteractionManager.runAfterInteractions(() => { navigateBackToPreviousScreen(); }); }; diff --git a/src/pages/settings/Report/NamePage.tsx b/src/pages/settings/Report/NamePage.tsx new file mode 100644 index 000000000000..de73e59bb7da --- /dev/null +++ b/src/pages/settings/Report/NamePage.tsx @@ -0,0 +1,22 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import * as ReportUtils from '@libs/ReportUtils'; +import type {ReportSettingsNavigatorParamList} from '@navigation/types'; +import GroupChatNameEditPage from '@pages/GroupChatNameEditPage'; +import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; +import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; +import type SCREENS from '@src/SCREENS'; +import RoomNamePage from './RoomNamePage'; + +type NamePageProps = WithReportOrNotFoundProps & StackScreenProps; + +function NamePage({report}: NamePageProps) { + if (ReportUtils.isGroupChat(report)) { + return ; + } + return ; +} + +NamePage.displayName = 'NamePage'; + +export default withReportOrNotFound()(NamePage); diff --git a/src/pages/settings/Report/ReportSettingsPage.tsx b/src/pages/settings/Report/ReportSettingsPage.tsx index aabd8d2fe376..132ae06d4867 100644 --- a/src/pages/settings/Report/ReportSettingsPage.tsx +++ b/src/pages/settings/Report/ReportSettingsPage.tsx @@ -96,11 +96,7 @@ function ReportSettingsPage({report, policies}: ReportSettingsPageProps) { shouldShowRightIcon title={report?.reportName === '' ? reportName : report?.reportName} description={isGroupChat ? translate('common.name') : translate('newRoomPage.roomName')} - onPress={() => - isGroupChat - ? Navigation.navigate(ROUTES.REPORT_SETTINGS_GROUP_NAME.getRoute(reportID)) - : Navigation.navigate(ROUTES.REPORT_SETTINGS_ROOM_NAME.getRoute(reportID)) - } + onPress={() => Navigation.navigate(ROUTES.REPORT_SETTINGS_NAME.getRoute(reportID))} /> )} diff --git a/src/pages/settings/Report/RoomNamePage.tsx b/src/pages/settings/Report/RoomNamePage.tsx index 5ba85961e5ba..13c853674b4b 100644 --- a/src/pages/settings/Report/RoomNamePage.tsx +++ b/src/pages/settings/Report/RoomNamePage.tsx @@ -1,5 +1,4 @@ import {useIsFocused} from '@react-navigation/native'; -import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -18,14 +17,10 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; -import type {ReportSettingsNavigatorParamList} from '@navigation/types'; -import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; -import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/RoomNameForm'; import type {Policy, Report} from '@src/types/onyx'; @@ -37,7 +32,9 @@ type RoomNamePageOnyxProps = { policy: OnyxEntry; }; -type RoomNamePageProps = RoomNamePageOnyxProps & WithReportOrNotFoundProps & StackScreenProps; +type RoomNamePageProps = RoomNamePageOnyxProps & { + report: Report; +}; function RoomNamePage({report, policy, reports}: RoomNamePageProps) { const styles = useThemeStyles(); @@ -111,13 +108,11 @@ function RoomNamePage({report, policy, reports}: RoomNamePageProps) { RoomNamePage.displayName = 'RoomNamePage'; -export default withReportOrNotFound()( - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, - }, - })(RoomNamePage), -); +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, + }, +})(RoomNamePage); diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx index dfa769077374..13d03fd557e9 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx @@ -11,9 +11,9 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import Clipboard from '@libs/Clipboard'; import localFileDownload from '@libs/localFileDownload'; import type {BackToParams} from '@libs/Navigation/types'; @@ -31,7 +31,7 @@ function CodesStep({account, backTo}: CodesStepProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {isExtraSmallScreenWidth, isSmallScreenWidth} = useWindowDimensions(); + const {isExtraSmallScreenWidth, isSmallScreenWidth} = useResponsiveLayout(); const [error, setError] = useState(''); const {setStep} = useTwoFactorAuthContext(); diff --git a/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx b/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx new file mode 100644 index 000000000000..83ede1532efc --- /dev/null +++ b/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; + +function SubscriptionSettingsPage() { + const {isSmallScreenWidth} = useWindowDimensions(); + const {translate} = useLocalize(); + + return ( + + Navigation.goBack()} + shouldShowBackButton={isSmallScreenWidth} + icon={Illustrations.CreditCardsNew} + /> + + ); +} + +SubscriptionSettingsPage.displayName = 'SubscriptionSettingsPage'; + +export default SubscriptionSettingsPage; diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index b2523a4e039c..817edb5cc9f7 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -55,7 +55,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const debugConsoleItem: BaseMenuItem = { translationKey: 'initialSettingsPage.troubleshoot.viewConsole', icon: Expensicons.Gear, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CONSOLE)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CONSOLE.getRoute(ROUTES.SETTINGS_TROUBLESHOOT))), }; const baseMenuItems: BaseMenuItem[] = [ diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index 1d1d6583ffa8..2710cadf94e4 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -12,9 +12,9 @@ import type {MagicCodeInputHandle} from '@components/MagicCodeInput'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -46,7 +46,7 @@ function ActivatePhysicalCardPage({ }: ActivatePhysicalCardPageProps) { const theme = useTheme(); const styles = useThemeStyles(); - const {isExtraSmallScreenHeight} = useWindowDimensions(); + const {isExtraSmallScreenHeight} = useResponsiveLayout(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 9cff3694fc5d..b049cacfa0b6 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -22,6 +22,8 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActions from '@libs/actions/Report'; +import {READ_COMMANDS} from '@libs/API/types'; +import HttpUtils from '@libs/HttpUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -158,6 +160,7 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro const selectReport = useCallback( (option: ListItem) => { + HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); if (!option) { return; } diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 55c2f54b5e48..c486e5b294c3 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -11,6 +11,8 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActions from '@libs/actions/Report'; +import {READ_COMMANDS} from '@libs/API/types'; +import HttpUtils from '@libs/HttpUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -21,6 +23,7 @@ import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; const selectReportHandler = (option: unknown) => { + HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); const optionItem = option as ReportUtils.OptionData; if (!optionItem || !optionItem?.reportID) { diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index cbc94ad37f03..4afd3e1b31dd 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -1,14 +1,18 @@ /* eslint-disable rulesdir/no-negated-variables */ import React, {useEffect} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Policy from '@userActions/Policy/Policy'; +import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -17,13 +21,28 @@ import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import callOrReturn from '@src/types/utils/callOrReturn'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const POLICY_ACCESS_VARIANTS = { +const ACCESS_VARIANTS = { [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => PolicyUtils.isPaidGroupPolicy(policy) && !!policy?.isPolicyExpenseChatEnabled, - [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry) => PolicyUtils.isPolicyAdmin(policy), -} as const satisfies Record boolean>; - -type PolicyAccessVariant = keyof typeof POLICY_ACCESS_VARIANTS; + [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => PolicyUtils.isPolicyAdmin(policy, login), + [CONST.IOU.ACCESS_VARIANTS.CREATE]: ( + policy: OnyxEntry, + login: string, + report: OnyxEntry, + allPolicies: OnyxCollection, + iouType?: IOUType, + ) => + !!iouType && + IOUUtils.isValidMoneyRequestType(iouType) && + // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the expense + (isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType)) && + (iouType !== CONST.IOU.TYPE.INVOICE || PolicyUtils.canSendInvoice(allPolicies)), +} as const satisfies Record, iouType?: IOUType) => boolean>; + +type AccessVariant = keyof typeof ACCESS_VARIANTS; type AccessOrNotFoundWrapperOnyxProps = { + /** The report that holds the transaction */ + report: OnyxEntry; + /** The report currently being looked at */ policy: OnyxEntry; @@ -35,11 +54,14 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { /** The children to render */ children: ((props: AccessOrNotFoundWrapperOnyxProps) => React.ReactNode) | React.ReactNode; + /** The id of the report that holds the transaction */ + reportID?: string; + /** The report currently being looked at */ - policyID: string; + policyID?: string; /** Defines which types of access should be verified */ - accessVariants?: PolicyAccessVariant[]; + accessVariants?: AccessVariant[]; /** The current feature name that the user tries to get access to */ featureName?: PolicyFeatureName; @@ -49,6 +71,12 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { /** Whether or not to block user from accessing the page */ shouldBeBlocked?: boolean; + + /** The type of the transaction */ + iouType?: IOUType; + + /** The list of all policies */ + allPolicies?: OnyxCollection; } & Pick; type PageNotFoundFallbackProps = Pick & {shouldShowFullScreenFallback: boolean}; @@ -64,7 +92,7 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN /> ) : ( Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(policyID ? ROUTES.WORKSPACE_PROFILE.getRoute(policyID) : ROUTES.HOME)} // eslint-disable-next-line react/jsx-props-no-spreading {...fullPageNotFoundViewProps} /> @@ -72,9 +100,11 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN } function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps, shouldBeBlocked, ...props}: AccessOrNotFoundWrapperProps) { - const {policy, policyID, featureName, isLoadingReportData} = props; - + const {policy, policyID, report, iouType, allPolicies, featureName, isLoadingReportData} = props; + const {login = ''} = useCurrentUserPersonalDetails(); const isPolicyIDInRoute = !!policyID?.length; + const isMoneyRequest = !!iouType && IOUUtils.isValidMoneyRequestType(iouType); + const isFromGlobalCreate = isEmptyObject(report?.reportID); useEffect(() => { if (!isPolicyIDInRoute || !isEmptyObject(policy)) { @@ -86,17 +116,17 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPolicyIDInRoute, policyID]); - const shouldShowFullScreenLoadingIndicator = isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); + const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true; const isPageAccessible = accessVariants.reduce((acc, variant) => { - const accessFunction = POLICY_ACCESS_VARIANTS[variant]; - return acc && accessFunction(policy); + const accessFunction = ACCESS_VARIANTS[variant]; + return acc && accessFunction(policy, login, report, allPolicies ?? null, iouType); }, true); - const shouldShowNotFoundPage = - isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked; + const isPolicyNotAccessible = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id; + const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked; if (shouldShowFullScreenLoadingIndicator) { return ; @@ -115,11 +145,14 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps return callOrReturn(props.children, props); } -export type {PolicyAccessVariant}; +export type {AccessVariant}; export default withOnyx({ + report: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + }, policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index cbcc3c88fb2c..3eba3346593e 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -13,6 +13,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; @@ -142,7 +143,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc const hasMembersError = PolicyUtils.hasEmployeeListError(policy); const hasPolicyCategoryError = PolicyUtils.hasPolicyCategoriesError(policyCategories); const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatarURL ?? {}); - const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy); + const {login} = useCurrentUserPersonalDetails(); + const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy, login); const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); const isFreeGroupPolicy = PolicyUtils.isFreeGroupPolicy(policy); const [featureStates, setFeatureStates] = useState(policyFeatureStates); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index cc881b22c94d..9142361a531e 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -17,7 +17,9 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActions from '@libs/actions/Report'; +import {READ_COMMANDS} from '@libs/API/types'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import HttpUtils from '@libs/HttpUtils'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -233,6 +235,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli if (!isValid) { return; } + HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; selectedOptions.forEach((option) => { diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index cd11826cde75..699ba9a14564 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -1,7 +1,8 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -13,11 +14,14 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import * as Category from '@userActions/Policy/Category'; import * as Policy from '@userActions/Policy/Policy'; +import * as Tag from '@userActions/Policy/Tag'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -27,6 +31,12 @@ import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscree import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import ToggleSettingOptionRow from './workflows/ToggleSettingsOptionRow'; +type ItemType = 'organize' | 'integrate'; +type ConnectionWarningModalState = { + isOpen: boolean; + itemType?: ItemType; +}; + type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; type Item = { @@ -54,6 +64,9 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const {canUseAccountingIntegrations} = usePermissions(); const hasAccountingConnection = !!policy?.areConnectionsEnabled && !isEmptyObject(policy?.connections); const isSyncTaxEnabled = !!policy?.connections?.quickbooksOnline?.config.syncTax || !!policy?.connections?.xero?.config.importTaxRates; + const policyID = policy?.id ?? ''; + + const [connectionWarningModalState, setConnectionWarningModalState] = useState({isOpen: false}); const spendItems: Item[] = [ { @@ -87,6 +100,13 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.areCategoriesEnabled, action: (isEnabled: boolean) => { + if (hasAccountingConnection) { + setConnectionWarningModalState({ + isOpen: true, + itemType: 'organize', + }); + return; + } Category.enablePolicyCategories(policy?.id ?? '', isEnabled); }, }, @@ -98,7 +118,14 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.areTagsEnabled, action: (isEnabled: boolean) => { - Policy.enablePolicyTags(policy?.id ?? '', isEnabled); + if (hasAccountingConnection) { + setConnectionWarningModalState({ + isOpen: true, + itemType: 'organize', + }); + return; + } + Tag.enablePolicyTags(policy?.id ?? '', isEnabled); }, }, { @@ -106,9 +133,16 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro titleTranslationKey: 'workspace.moreFeatures.taxes.title', subtitleTranslationKey: 'workspace.moreFeatures.taxes.subtitle', isActive: (policy?.tax?.trackingEnabled ?? false) || isSyncTaxEnabled, - disabled: isSyncTaxEnabled || policy?.connections?.quickbooksOnline?.data?.country === CONST.COUNTRY.US, + disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.tax, action: (isEnabled: boolean) => { + if (hasAccountingConnection) { + setConnectionWarningModalState({ + isOpen: true, + itemType: 'organize', + }); + return; + } Policy.enablePolicyTaxes(policy?.id ?? '', isEnabled); }, }, @@ -122,6 +156,13 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro isActive: !!policy?.areConnectionsEnabled, pendingAction: policy?.pendingFields?.areConnectionsEnabled, action: (isEnabled: boolean) => { + if (hasAccountingConnection) { + setConnectionWarningModalState({ + isOpen: true, + itemType: 'integrate', + }); + return; + } Policy.enablePolicyConnections(policy?.id ?? '', isEnabled); }, disabled: hasAccountingConnection, @@ -165,7 +206,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro isActive={item.isActive} pendingAction={item.pendingAction} onToggle={item.action} - disabled={item.disabled} + showLockIcon={item.disabled} errors={item.errors} onCloseError={item.onCloseError} /> @@ -206,6 +247,17 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }, [fetchFeatures]), ); + const getConnectionWarningPrompt = useCallback(() => { + switch (connectionWarningModalState.itemType) { + case 'organize': + return translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledText'); + case 'integrate': + return translate('workspace.moreFeatures.connectionsWarningModal.disconnectText'); + default: + return undefined; + } + }, [connectionWarningModalState.itemType, translate]); + return ( {sections.map(renderSection)} + + { + setConnectionWarningModalState({ + isOpen: false, + itemType: undefined, + }); + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + }} + onCancel={() => + setConnectionWarningModalState({ + isOpen: false, + itemType: undefined, + }) + } + isVisible={connectionWarningModalState.isOpen} + prompt={getConnectionWarningPrompt()} + confirmText={translate('workspace.moreFeatures.connectionsWarningModal.manageSettings')} + cancelText={translate('common.cancel')} + /> ); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 5716812ced16..393afcd25eb8 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -227,8 +227,8 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli <> ; }; @@ -101,7 +105,7 @@ function accountingIntegrationData( } } -function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataFetchNeeded}: PolicyAccountingPageProps) { +function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccountingPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -110,6 +114,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0}); const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); + const [datetimeToRelative, setDateTimeToRelative] = useState(''); const threeDotsMenuContainerRef = useRef(null); const isSyncInProgress = !!connectionSyncProgress?.stageInProgress && connectionSyncProgress.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE; @@ -117,6 +122,8 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME).filter((name) => !(name === CONST.POLICY.CONNECTIONS.NAME.XERO && !canUseXeroIntegration)); const connectedIntegration = accountingIntegrations.find((integration) => !!policy?.connections?.[integration]) ?? connectionSyncProgress?.connectionName; const policyID = policy?.id ?? ''; + const successfulDate = policy?.connections?.quickbooksOnline?.lastSync?.successfulDate; + const formattedDate = useMemo(() => (successfulDate ? new Date(successfulDate) : new Date()), [successfulDate]); const policyConnectedToXero = connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.XERO; @@ -129,7 +136,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF { icon: Expensicons.Sync, text: translate('workspace.accounting.syncNow'), - onSelected: () => syncConnection(policyID), + onSelected: () => syncConnection(policyID, connectedIntegration), disabled: isOffline, }, { @@ -138,10 +145,14 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF onSelected: () => setIsDisconnectModalOpen(true), }, ], - [translate, policyID, isOffline], + [translate, policyID, isOffline, connectedIntegration], ); - const connectionsMenuItems: MenuItemProps[] = useMemo(() => { + useEffect(() => { + setDateTimeToRelative(formatDistanceToNow(formattedDate, {addSuffix: true})); + }, [formattedDate]); + + const connectionsMenuItems: MenuItemData[] = useMemo(() => { if (isEmptyObject(policy?.connections) && !isSyncInProgress) { return accountingIntegrations.map((integration) => { const integrationData = accountingIntegrationData(integration, policyID, translate); @@ -160,19 +171,20 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF if (!connectedIntegration) { return []; } + const shouldShowSynchronizationError = hasSynchronizationError(policy, connectedIntegration, isSyncInProgress); const integrationData = accountingIntegrationData(connectedIntegration, policyID, translate); const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {}; return [ { ...iconProps, interactive: false, - wrapperStyle: [styles.sectionMenuItemTopDescription], + wrapperStyle: [styles.sectionMenuItemTopDescription, shouldShowSynchronizationError && styles.pb0], shouldShowRightComponent: true, title: integrationData?.title, - - description: isSyncInProgress - ? translate('workspace.accounting.connections.syncStageName', connectionSyncProgress.stageInProgress) - : translate('workspace.accounting.lastSync'), + errorText: shouldShowSynchronizationError ? translate('workspace.accounting.syncError', connectedIntegration) : undefined, + errorTextStyle: [styles.mt5], + shouldShowRedDotIndicator: true, + description: isSyncInProgress ? translate('workspace.accounting.connections.syncStageName', connectionSyncProgress.stageInProgress) : datetimeToRelative, rightComponent: isSyncInProgress ? ( ), }, - ...(policyConnectedToXero + ...(policyConnectedToXero && !shouldShowSynchronizationError ? [ { description: translate('workspace.xero.organization'), @@ -212,10 +224,12 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF } Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.getRoute(policyID, currentXeroOrganization?.id ?? '')); }, + pendingAction: policy?.connections?.xero?.config?.pendingFields?.tenantID, + brickRoadIndicator: policy?.connections?.xero?.config?.errorFields?.tenantID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, ] : []), - ...(isEmptyObject(policy?.connections) + ...(isEmptyObject(policy?.connections) || shouldShowSynchronizationError ? [] : [ { @@ -245,21 +259,26 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF ]), ]; }, [ - connectedIntegration, - connectionSyncProgress?.stageInProgress, - currentXeroOrganization, - currentXeroOrganizationName, - tenants, + policy, isSyncInProgress, - overflowMenu, - policy?.connections, - policyConnectedToXero, + connectedIntegration, policyID, - styles, + translate, + styles.sectionMenuItemTopDescription, + styles.pb0, + styles.mt5, + styles.popoverMenuIcon, + styles.fontWeightNormal, + connectionSyncProgress?.stageInProgress, theme.spinner, + overflowMenu, threeDotsMenuPosition, - translate, + policyConnectedToXero, + currentXeroOrganizationName, + tenants.length, + datetimeToRelative, accountingIntegrations, + currentXeroOrganization?.id, ]); const otherIntegrationsItems = useMemo(() => { @@ -292,21 +311,6 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF accountingIntegrations, ]); - const headerThreeDotsMenuItems: ThreeDotsMenuProps['menuItems'] = [ - { - icon: Expensicons.Key, - shouldShowRightIcon: true, - iconRight: Expensicons.NewWindow, - text: translate('workspace.accounting.enterCredentials'), - onSelected: () => {}, - }, - { - icon: Expensicons.Trashcan, - text: translate('workspace.accounting.disconnect'), - onSelected: () => setIsDisconnectModalOpen(true), - }, - ]; - return ( @@ -336,30 +338,28 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF titleStyles={styles.accountSettingsSectionTitle} childrenStyles={styles.pt5} > - {isConnectionDataFetchNeeded ? ( - - - - ) : ( - <> + {connectionsMenuItems.map((menuItem) => ( + + + + ))} + {otherIntegrationsItems && ( + - {otherIntegrationsItems && ( - - - - )} - + )} diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx index f96daaefff47..e5c09b4ef7a6 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx @@ -1,6 +1,8 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -13,6 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -58,6 +61,20 @@ function QuickbooksAccountSelectPage({policy}: WithPolicyConnectionsProps) { [policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx index 4c1eba8f2167..0478a91379a4 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx @@ -1,6 +1,8 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -13,6 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -59,6 +62,20 @@ function QuickbooksInvoiceAccountSelectPage({policy}: WithPolicyConnectionsProps [policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx index 892a84f9dfde..7457d225e4df 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -12,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Account} from '@src/types/onyx/Policy'; @@ -62,6 +65,20 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyConne [nonReimbursableExpensesAccount, policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( {translate(`workspace.qbo.accounts.${nonReimbursableExpensesExportDestination}AccountDescription`)} ) : null } - sections={[{data}]} + sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportAccount} shouldDebounceRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx index 5f72beb8d89f..4c114cc48422 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -12,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Account} from '@src/types/onyx/Policy'; @@ -48,6 +51,20 @@ function QuickbooksExportInvoiceAccountSelectPage({policy}: WithPolicyConnection [receivableAccount, policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( {translate('workspace.qbo.exportInvoicesDescription')}} - sections={[{data}]} + sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportInvoice} shouldDebounceRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx index 5264f6c0c45e..79f62934bcd4 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -12,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -34,7 +37,7 @@ function QuickbooksNonReimbursableDefaultVendorSelectPage({policy}: WithPolicyCo keyForList: vendor.name, isSelected: vendor.id === nonReimbursableBillDefaultVendor, })) ?? []; - return [{data}]; + return data.length ? [{data}] : []; }, [nonReimbursableBillDefaultVendor, vendors]); const selectVendor = useCallback( @@ -47,6 +50,20 @@ function QuickbooksNonReimbursableDefaultVendorSelectPage({policy}: WithPolicyCo [nonReimbursableBillDefaultVendor, policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( mode.isSelected)?.keyForList} + initiallyFocusedOptionKey={sections[0]?.data.find((mode) => mode.isSelected)?.keyForList} + listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx index e9944a289922..1d85d85c133d 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx @@ -1,5 +1,7 @@ import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -12,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Account} from '@src/types/onyx/Policy'; @@ -27,18 +30,41 @@ function QuickbooksOutOfPocketExpenseAccountSelectPage({policy}: WithPolicyConne const {reimbursableExpensesExportDestination, reimbursableExpensesAccount} = policy?.connections?.quickbooksOnline?.config ?? {}; + const [title, description] = useMemo(() => { + let titleText: string | undefined; + let descriptionText: string | undefined; + switch (reimbursableExpensesExportDestination) { + case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK: + titleText = translate('workspace.qbo.bankAccount'); + descriptionText = translate('workspace.qbo.bankAccountDescription'); + break; + case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY: + titleText = translate('workspace.qbo.account'); + descriptionText = translate('workspace.qbo.accountDescription'); + break; + case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL: + titleText = translate('workspace.qbo.accountsPayable'); + descriptionText = translate('workspace.qbo.accountsPayableDescription'); + break; + default: + break; + } + + return [titleText, descriptionText]; + }, [translate, reimbursableExpensesExportDestination]); + const data: CardListItem[] = useMemo(() => { let accounts: Account[]; switch (reimbursableExpensesExportDestination) { case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK: accounts = bankAccounts ?? []; break; - case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL: - accounts = accountPayable ?? []; - break; case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY: accounts = journalEntryAccounts ?? []; break; + case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL: + accounts = accountPayable ?? []; + break; default: accounts = []; } @@ -63,6 +89,20 @@ function QuickbooksOutOfPocketExpenseAccountSelectPage({policy}: WithPolicyConne [reimbursableExpensesAccount, policyID], ); + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + return ( - + {translate('workspace.qbo.accountsPayableDescription')}} - sections={[{data}]} + headerContent={{description}} + sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportAccount} shouldDebounceRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx index 861f7d416902..ad99dc8cd07d 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -18,12 +18,34 @@ function QuickbooksOutOfPocketExpenseConfigurationPage({policy}: WithPolicyConne const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; - const {syncLocations, reimbursableExpensesAccount, reimbursableExpensesExportDestination, errorFields, syncTax, pendingFields} = policy?.connections?.quickbooksOnline?.config ?? {}; + const {syncLocations, syncTax, reimbursableExpensesAccount, reimbursableExpensesExportDestination, errorFields, pendingFields} = policy?.connections?.quickbooksOnline?.config ?? {}; const isLocationEnabled = Boolean(syncLocations && syncLocations !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const isTaxesEnabled = Boolean(syncTax); const shouldShowTaxError = isTaxesEnabled && reimbursableExpensesExportDestination === CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY; const shouldShowLocationError = isLocationEnabled && reimbursableExpensesExportDestination !== CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY; const hasErrors = Boolean(errorFields?.reimbursableExpensesExportDestination) || shouldShowTaxError || shouldShowLocationError; + const [exportHintText, accountDescription] = useMemo(() => { + let hintText: string | undefined; + let description: string | undefined; + switch (reimbursableExpensesExportDestination) { + case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK: + hintText = isLocationEnabled ? undefined : translate('workspace.qbo.exportCheckDescription'); + description = translate('workspace.qbo.bankAccount'); + break; + case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY: + hintText = isTaxesEnabled ? undefined : translate('workspace.qbo.exportJournalEntryDescription'); + description = translate('workspace.qbo.account'); + break; + case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL: + hintText = isLocationEnabled ? undefined : translate('workspace.qbo.exportVendorBillDescription'); + description = translate('workspace.qbo.accountsPayable'); + break; + default: + break; + } + + return [hintText, description]; + }, [translate, reimbursableExpensesExportDestination, isLocationEnabled, isTaxesEnabled]); return ( - {!isLocationEnabled && {translate('workspace.qbo.exportOutOfPocketExpensesDescription')}} + {translate('workspace.qbo.exportOutOfPocketExpensesDescription')} Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_SELECT.getRoute(policyID))} brickRoadIndicator={hasErrors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} shouldShowRightIcon - hintText={ - reimbursableExpensesExportDestination === CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL && !isLocationEnabled - ? translate('workspace.qbo.exportVendorBillDescription') - : undefined - } + hintText={exportHintText} + /> + + + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT.getRoute(policyID))} + brickRoadIndicator={errorFields?.exportAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + shouldShowRightIcon + errorText={errorFields?.exportAccount ? translate('common.genericErrorMessage') : undefined} /> - {isLocationEnabled && {translate('workspace.qbo.outOfPocketLocationEnabledDescription')}} - {!isLocationEnabled && ( - - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT.getRoute(policyID))} - brickRoadIndicator={errorFields?.exportAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - shouldShowRightIcon - errorText={errorFields?.exportAccount ? translate('common.genericErrorMessage') : undefined} - /> - - )} diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx index 1c74b146eea5..14007f81f6b8 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx @@ -18,6 +18,22 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Account, QBOReimbursableExportAccountType} from '@src/types/onyx/Policy'; +function Footer({isTaxEnabled, isLocationsEnabled}: {isTaxEnabled: boolean; isLocationsEnabled: boolean}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + if (!isTaxEnabled && !isLocationsEnabled) { + return null; + } + + return ( + + {isTaxEnabled && {translate('workspace.qbo.outOfPocketTaxEnabledDescription')}} + {isLocationsEnabled && {translate('workspace.qbo.outOfPocketLocationEnabledDescription')}} + + ); +} + type CardListItem = ListItem & { value: QBOReimbursableExportAccountType; isShown: boolean; @@ -107,7 +123,12 @@ function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyConnec onSelectRow={selectExportEntity} shouldDebounceRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} - footerContent={isTaxesEnabled && {translate('workspace.qbo.outOfPocketTaxEnabledDescription')}} + footerContent={ +