diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js index a72dd6a9250a..a1c2c452273e 100644 --- a/.eslintrc.changed.js +++ b/.eslintrc.changed.js @@ -10,7 +10,17 @@ module.exports = { }, overrides: [ { - files: ['src/libs/ReportUtils.ts', 'src/libs/actions/IOU.ts', 'src/libs/actions/Report.ts', 'src/libs/actions/Task.ts'], + files: [ + 'src/libs/ReportUtils.ts', + 'src/libs/actions/IOU.ts', + 'src/libs/actions/Report.ts', + 'src/libs/actions/Task.ts', + 'src/libs/OptionsListUtils.ts', + 'src/libs/ReportActionsUtils.ts', + 'src/libs/TransactionUtils/index.ts', + 'src/pages/home/ReportScreen.tsx', + 'src/pages/workspace/WorkspaceInitialPage.tsx', + ], rules: { 'rulesdir/no-default-id-values': 'off', }, diff --git a/.prettierignore b/.prettierignore index b428978a1563..8584ae14b917 100644 --- a/.prettierignore +++ b/.prettierignore @@ -22,3 +22,6 @@ src/libs/E2E/reactNativeLaunchingTest.ts # Automatically generated files src/libs/SearchParser/searchParser.js src/libs/SearchParser/autocompleteParser.js + +# Disable prettier in the submodule +Mobile-Expensify diff --git a/Mobile-Expensify b/Mobile-Expensify index 9854d5bfa2e3..43c5ef761b59 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 9854d5bfa2e31c702066e161256e0a9655051892 +Subproject commit 43c5ef761b59d38a297904c5917c326d86c83fb7 diff --git a/android/app/build.gradle b/android/app/build.gradle index 5ebefd8304c7..cf34cd05f8fd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007702 - versionName "9.0.77-2" + versionCode 1009007704 + versionName "9.0.77-4" // 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/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index c60670c72324..ac086d3a9bed 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -175,7 +175,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later. // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server. { - test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'), + test: new RegExp('node_modules/pdfjs-dist/build/pdf.worker.min.mjs'), type: 'asset/source', }, diff --git a/docs/articles/expensify-classic/connections/Expensify-API.md b/docs/articles/expensify-classic/connections/Expensify-API.md index fba85dcd154a..e2fbdbfd7703 100644 --- a/docs/articles/expensify-classic/connections/Expensify-API.md +++ b/docs/articles/expensify-classic/connections/Expensify-API.md @@ -11,11 +11,11 @@ To begin, review our [Integration Server Manual](https://integrations.expensify. We've compiled answers to some frequently asked questions to help you get started. -**Should I give your support team my API credentials when I need help?** +## Should I give your support team my API credentials when I need help? If you’re seeking help with Expensify's API, do not share your partnerUserSecret. If you do, immediately rotate your credentials on [this page](https://www.expensify.com/tools/integrations/). -**Is there a rate limit?** +## Is there a rate limit? To keep our platform stable and handle high traffic, Expensify limits how many API requests you can send: - Up to 5 requests every 10 seconds @@ -23,38 +23,38 @@ To keep our platform stable and handle high traffic, Expensify limits how many A Sending more requests than allowed may result in an error with status code `429`. -**What is a Policy ID?** +## What is a Policy ID? This is also known as a Workspace ID. To find your Policy/Workspace ID, Hover over Settings and click Workspaces. Click the name of the Workspace. Copy the ID number from the URL. For example, if the URL is https://www.expensify.com/policy?param={"policyID":"0810E551A5F2A9C2”}, then your workspace ID is 0810E551A5F2A9C2. -**Can I use the parent type `file` to export workspace/policy data?** +## Can I use the parent type `file` to export workspace/policy data? No. The parent type `file` can only be used to export expense and report data — not policy information. To export policy data (e.g., categories, tags), you must use the `get` type with `inputSettings.type` set to `policy`. -**Can I use the API to create Domain Groups?** +## Can I use the API to create Domain Groups? No, you cannot create domain groups. You can only assign users to them. -**I’m exporting expense IDs `${expense.transactionID}` but when I open my CSV in Excel, it’s changing all the IDs and making them look the same. How can I prevent this?** +## I’m exporting expense IDs `${expense.transactionID}` but when I open my CSV in Excel, it’s changing all the IDs and making them look the same. How can I prevent this? Try prepending a non-numeric character like a quote to force Excel to interpret the value as a string and not a number (i.e., `'${expense.transactionID}`). -**How can we export the person who will approve a report while the reports are still processing?** +## How can we export the person who will approve a report while the reports are still processing? Use the field ${report.managerEmail}. -**Why won’t my boolean field return any data?** +## Why won’t my boolean field return any data? Boolean fields won't output values without a string. For example, instead of using `${expense.billable}`, use `${expense.billable?string("Yes", "No")}`. This will display "Yes" if the expense is billable and "No" if it is not. -**Can I export the reports for just one user?** +## Can I export the reports for just one user? Not in a quick convenient way, as you would need to include the user in your template. The simplest approach is to export data for all users and then apply a filter in your preferred spreadsheet program. -**Can I create expenses on behalf of users?** +## Can I create expenses on behalf of users? Yes. However, to access the Expense Creator API on behalf of employees, Expensify needs to verify the following setup: @@ -63,17 +63,17 @@ Verify you have internal authorization to add data to other accounts within your If you need this access, contact concierge@expensify.com and reference this help page. -## Using Postman +# Using Postman Many customers use Postman to help them build out their APIs. Below are some guides contributed by our customers. Please note, in all cases, you will need to first generate your authentication credentials, the steps for which can be found [here](https://integrations.expensify.com/Integration-Server/doc/#introduction) and have them ready: -### Download expenses from a report as a CSV file +## Download expenses from a report as a CSV file **Step 1: Get the ID of a report you want to export in Expensify** Find the ID by opening the expense report and clicking Details at the top right corner of the page. At the top of the menu, the ID is provided as the “Long ID.” -**Step 3: Export (generate) a "Report" as a CSV file** +**Step 2: Export (generate) a "Report" as a CSV file** {% include info.html %} For this you'll use the Documentation under [Report Exporter](https://integrations.expensify.com/Integration-Server/doc/#export). {% include end-info.html %} @@ -146,11 +146,11 @@ The template key will have the value like below: The template variable determines what information is saved in your CSV file. If you want more columns than merchant, amount, and transaction date, follow the syntax as defined in the export template format documentation. -**Step 4: Save your generated file name** +**Step 3: Save your generated file name** -Expensify currently supports only the "onReceive":{"immediateResponse":["returnRandomFileName"]} option in step 3, so you should receive a random filename back from the API like "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv". You will need to document this filename if you plan on running the download command after this one. +Expensify currently supports only the "onReceive":{"immediateResponse":["returnRandomFileName"]} option in step 2, so you should receive a random filename back from the API like "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv". You will need to document this filename if you plan on running the download command after this one. -**Step 5: Download your exported report** +**Step 4: Download your exported report** Set up another API call in almost the same way you did before. You don't need the template key in the Body anymore, so delete that and set the Body type to "none". Then modify your requestJobDescription to read like below, but with your own credentials and file name: @@ -170,7 +170,7 @@ Click Go and you should see the CSV in the response body. *Thank you to our customer Frederico Pettinella who originally wrote and shared this guide.* -### Use Advanced Employee Updater API with Postman +## Use Advanced Employee Updater API with Postman 1. Create a new request. 2. Select POST as the method. diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md index ea058df9c1b1..1b1702c6fcc7 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -51,6 +51,11 @@ When an expense is submitted to a workspace, your approver will receive an email {% include end-selector.html %} +![Click Global Create]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-1.png){:width="100%"} +![Click Submit expense]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-2.png){:width="100%"} +![Click Scan]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-3.png){:width="100%"} +![Enter workspace or individual's name]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-4.png){:width="100%"} + {% include info.html %} You can also forward receipts to receipts@expensify.com using your primary or secondary email address. SmartScan will automatically extract all the details from the receipt and add them to your expenses. {% include end-info.html %} diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index fe4c07fdac9e..108706d79a0c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.77.2 + 9.0.77.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 6a92f70d678f..ea782231aaec 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.77.2 + 9.0.77.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7ae2ed9457e8..b14e33cdde82 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.77 CFBundleVersion - 9.0.77.2 + 9.0.77.4 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0389642465da..11ae0e5f2f3f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -309,7 +309,7 @@ PODS: - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) - - NitroModules (0.18.1): + - NitroModules (0.18.2): - DoubleConversion - glog - hermes-engine @@ -3251,7 +3251,7 @@ SPEC CHECKSUMS: MapboxMaps: e76b14f52c54c40b76ddecd04f40448e6f35a864 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 - NitroModules: ebe2ba2d01dc03c1f82441561fe6062b8c3c4366 + NitroModules: 2f68aaf756386d1c998900de71aef4e50b37c71b Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af onfido-react-native-sdk: 4ccfdeb10f9ccb4a5799d2555cdbc2a068a42c0d Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89 diff --git a/metro.config.js b/metro.config.js index c6e4ba6bb4ec..98bea7be80ed 100644 --- a/metro.config.js +++ b/metro.config.js @@ -4,6 +4,7 @@ const {getDefaultConfig: getReactNativeDefaultConfig} = require('@react-native/m const {mergeConfig} = require('@react-native/metro-config'); const defaultAssetExts = require('metro-config/src/defaults/defaults').assetExts; const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts; +const {wrapWithReanimatedMetroConfig} = require('react-native-reanimated/metro-config'); require('dotenv').config(); const defaultConfig = getReactNativeDefaultConfig(__dirname); @@ -26,4 +27,4 @@ const config = { }, }; -module.exports = mergeConfig(defaultConfig, expoConfig, config); +module.exports = wrapWithReanimatedMetroConfig(mergeConfig(defaultConfig, expoConfig, config)); diff --git a/package-lock.json b/package-lock.json index 497c8bbaeb45..0a2e67f38ee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.77-2", + "version": "9.0.77-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.77-2", + "version": "9.0.77-4", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -77,7 +77,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.20", + "react-fast-pdf": "1.0.21", "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", @@ -13722,7 +13722,9 @@ "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.9", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18771,7 +18773,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -19325,17 +19329,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/defaults": { "version": "1.0.4", "license": "MIT", @@ -24297,7 +24290,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "license": "MIT", "dependencies": { @@ -24321,6 +24316,8 @@ }, "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, "license": "MIT", "engines": { @@ -24728,9 +24725,10 @@ } }, "node_modules/internal-ip/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -25666,23 +25664,6 @@ "reflect.getprototypeof": "^1.0.3" } }, - "node_modules/jackspeak": { - "version": "2.3.6", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jake": { "version": "10.8.7", "dev": true, @@ -30105,7 +30086,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -32484,9 +32467,9 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.20.tgz", - "integrity": "sha512-E2PJOO5oEqi6eNPllNOlQ8y0DiLZ3AW8t+MCN7AgJPp5pY04SeDveXHWvPN0nPU4X5sRBZ7CejeYce2QMMQDyg==", + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.21.tgz", + "integrity": "sha512-8Uuz/jPHjHqElH+aUj3ldS/Hg/NoZ5ZS/VupGzDkVJST0UiGzxkvDxxFIQuYuiaI4NGwGmqtQGGYsjJKpyWnig==", "license": "MIT", "dependencies": { "react-pdf": "^9.1.1", @@ -37764,7 +37747,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.0.4", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", "dev": true, "license": "MIT", "dependencies": { @@ -37781,23 +37766,20 @@ "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", - "http-proxy-middleware": "^2.0.3", + "http-proxy-middleware": "^2.0.7", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", - "rimraf": "^5.0.5", "schema-utils": "^4.2.0", "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.1.0", - "ws": "^8.16.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -37821,16 +37803,10 @@ } } }, - "node_modules/webpack-dev-server/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/webpack-dev-server/node_modules/colorette": { "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, @@ -37845,27 +37821,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/glob": { - "version": "10.3.12", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.1.0", "dev": true, @@ -37888,28 +37843,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/minimatch": { - "version": "9.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/webpack-dev-server/node_modules/minipass": { - "version": "7.0.4", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/webpack-dev-server/node_modules/open": { "version": "10.1.0", "dev": true, @@ -37927,25 +37860,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/rimraf": { - "version": "5.0.5", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.2.0", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "license": "MIT", "dependencies": { @@ -37955,7 +37873,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -37963,7 +37881,9 @@ } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "7.2.1", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c67c99e28e2d..c621b583452e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.77-2", + "version": "9.0.77-4", "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.", @@ -139,7 +139,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.20", + "react-fast-pdf": "1.0.21", "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", diff --git a/patches/react-native-draggable-flatlist+4.0.1.patch b/patches/react-native-draggable-flatlist+4.0.1.patch index 348f1aa5de8a..a3d29b66de7a 100644 --- a/patches/react-native-draggable-flatlist+4.0.1.patch +++ b/patches/react-native-draggable-flatlist+4.0.1.patch @@ -12,7 +12,7 @@ index d7d98c2..2f59c7a 100644 runOnJS(onDragEnd)({ from: activeIndexAnim.value, diff --git a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx -index ea21575..66c5eed 100644 +index ea21575..dc6b095 100644 --- a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx +++ b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx @@ -1,14 +1,14 @@ @@ -32,14 +32,13 @@ index ea21575..66c5eed 100644 cellDataRef: React.MutableRefObject>; keyToIndexRef: React.MutableRefObject>; containerRef: React.RefObject; -@@ -54,8 +54,8 @@ function useSetupRefs({ +@@ -54,8 +54,7 @@ function useSetupRefs({ ...DEFAULT_PROPS.animationConfig, ...animationConfig, } as WithSpringConfig; - const animationConfigRef = useRef(animConfig); - animationConfigRef.current = animConfig; + const animationConfigRef = useSharedValue(animConfig); -+ animationConfigRef.value = animConfig; const cellDataRef = useRef(new Map()); const keyToIndexRef = useRef(new Map()); @@ -57,7 +56,7 @@ index ce4ab68..efea240 100644 return translate; diff --git a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts -index 7c20587..857c7d0 100644 +index 7c20587..33042e9 100644 --- a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts +++ b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts @@ -1,8 +1,9 @@ @@ -72,18 +71,17 @@ index 7c20587..857c7d0 100644 } from "react-native-reanimated"; import { DEFAULT_ANIMATION_CONFIG } from "../constants"; import { useAnimatedValues } from "../context/animatedValueContext"; -@@ -15,8 +16,8 @@ type Params = { +@@ -15,8 +16,7 @@ type Params = { export function useOnCellActiveAnimation( { animationConfig }: Params = { animationConfig: {} } ) { - const animationConfigRef = useRef(animationConfig); - animationConfigRef.current = animationConfig; + const animationConfigRef = useSharedValue(animationConfig); -+ animationConfigRef.value = animationConfig; const isActive = useIsActive(); -@@ -26,7 +27,7 @@ export function useOnCellActiveAnimation( +@@ -26,7 +26,7 @@ export function useOnCellActiveAnimation( const toVal = isActive && isTouchActiveNative.value ? 1 : 0; return withSpring(toVal, { ...DEFAULT_ANIMATION_CONFIG, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2a2959f43f66..a43f1622ec9a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -733,6 +733,8 @@ const ONYXKEYS = { RULES_MAX_EXPENSE_AGE_FORM_DRAFT: 'rulesMaxExpenseAgeFormDraft', DEBUG_DETAILS_FORM: 'debugDetailsForm', DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft', + WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm', + WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft', }, } as const; @@ -826,6 +828,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm; [ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm; [ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm; + [ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f48a5cae92f0..58d28a46a7b8 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1325,6 +1325,26 @@ const ROUTES = { route: 'settings/workspaces/:policyID/per-diem/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/settings` as const, }, + WORKSPACE_PER_DIEM_DETAILS: { + route: 'settings/workspaces/:policyID/per-diem/details/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/details/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_DESTINATION: { + route: 'settings/workspaces/:policyID/per-diem/edit/destination/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/destination/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_SUBRATE: { + route: 'settings/workspaces/:policyID/per-diem/edit/subrate/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/subrate/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_AMOUNT: { + route: 'settings/workspaces/:policyID/per-diem/edit/amount/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/amount/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_CURRENCY: { + route: 'settings/workspaces/:policyID/per-diem/edit/currency/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/currency/${rateID}/${subRateID}` as const, + }, RULES_CUSTOM_NAME: { route: 'settings/workspaces/:policyID/rules/name', getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 86c71e182a2b..6274be1044b4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -558,6 +558,11 @@ const SCREENS = { PER_DIEM_IMPORT: 'Per_Diem_Import', PER_DIEM_IMPORTED: 'Per_Diem_Imported', PER_DIEM_SETTINGS: 'Per_Diem_Settings', + PER_DIEM_DETAILS: 'Per_Diem_Details', + PER_DIEM_EDIT_DESTINATION: 'Per_Diem_Edit_Destination', + PER_DIEM_EDIT_SUBRATE: 'Per_Diem_Edit_Subrate', + PER_DIEM_EDIT_AMOUNT: 'Per_Diem_Edit_Amount', + PER_DIEM_EDIT_CURRENCY: 'Per_Diem_Edit_Currency', }, EDIT_REQUEST: { diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx index 0ac410013214..de65f40b3b4f 100644 --- a/src/components/AmountWithoutCurrencyForm.tsx +++ b/src/components/AmountWithoutCurrencyForm.tsx @@ -12,10 +12,13 @@ type AmountFormProps = { /** Callback to update the amount in the FormProvider */ onInputChange?: (value: string) => void; + + /** Should we allow negative number as valid input */ + shouldAllowNegative?: boolean; } & Partial; function AmountWithoutCurrencyForm( - {value: amount, onInputChange, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, + {value: amount, onInputChange, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, ref: ForwardedRef, ) { const {toLocaleDigit} = useLocalize(); @@ -32,13 +35,13 @@ function AmountWithoutCurrencyForm( // More info: https://github.com/Expensify/App/issues/16974 const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount); const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces); - const withLeadingZero = addLeadingZero(replacedCommasAmount); - if (!validateAmount(withLeadingZero, 2)) { + const withLeadingZero = addLeadingZero(replacedCommasAmount, shouldAllowNegative); + if (!validateAmount(withLeadingZero, 2, CONST.IOU.AMOUNT_MAX_LENGTH, shouldAllowNegative)) { return; } onInputChange?.(withLeadingZero); }, - [onInputChange], + [onInputChange, shouldAllowNegative], ); const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit); @@ -54,7 +57,7 @@ function AmountWithoutCurrencyForm( accessibilityLabel={accessibilityLabel} role={role} ref={ref} - keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} + keyboardType={!shouldAllowNegative ? CONST.KEYBOARD_TYPE.DECIMAL_PAD : undefined} // On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag. // See https://github.com/Expensify/App/issues/51868 for more information autoCapitalize="words" diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 19bb98bff58e..f8e9e836c736 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -14,7 +14,7 @@ import RadioListItem from './SelectionList/RadioListItem'; import type {ListItem} from './SelectionList/types'; type CategoryPickerProps = { - policyID: string; + policyID: string | undefined; selectedCategory?: string; onSubmit: (item: ListItem) => void; }; diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index cea339de07e2..0cddb32f5aeb 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -16,6 +16,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; +import getPlatform from '@libs/getPlatform'; import CONST from '@src/CONST'; const excludeNoStyles: Array = []; @@ -140,7 +141,11 @@ function Composer( textAlignVertical="center" style={[composerStyle, maxHeightStyle]} markdownStyle={markdownStyle} - autoFocus={autoFocus} + // /* + // There are cases in hybird app on android that screen goes up when there is autofocus on keyboard. (e.g. https://github.com/Expensify/App/issues/53185) + // Workaround for this issue is to maunally focus keyboard after it's acutally rendered which is done by useAutoFocusInput hook. + // */ + autoFocus={getPlatform() !== 'android' ? autoFocus : false} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 98ac9e00a98a..5af76a2406b5 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -1,4 +1,5 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; +import {useIsFocused} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; @@ -252,7 +253,8 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isComposerFullSize]); - useHtmlPaste(textInput, handlePaste, true); + const isActive = useIsFocused(); + useHtmlPaste(textInput, handlePaste, isActive); useEffect(() => { setIsRendered(true); diff --git a/src/components/EmptySelectionListContent.tsx b/src/components/EmptySelectionListContent.tsx index a5ac2c84eb2b..67a9a2fc83f3 100644 --- a/src/components/EmptySelectionListContent.tsx +++ b/src/components/EmptySelectionListContent.tsx @@ -7,6 +7,7 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import BlockingView from './BlockingViews/BlockingView'; import * as Illustrations from './Icon/Illustrations'; +import ScrollView from './ScrollView'; import Text from './Text'; import TextLink from './TextLink'; @@ -39,17 +40,19 @@ function EmptySelectionListContent({contentType}: EmptySelectionListContentProps ); return ( - - - + + + + + ); } diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 19af05a1581b..00965d197937 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -177,14 +177,14 @@ function MoneyRequestConfirmationList({ shouldPlaySound = true, isConfirmed, }: MoneyRequestConfirmationListProps) { - const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID ?? '-1'}`); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID ?? '-1'}`); - const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '-1'}`); - const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID ?? '-1'}`); - const [defaultMileageRate] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID ?? '-1'}`, { + const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`); + const [defaultMileageRate] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, { selector: (selectedPolicy) => DistanceRequestUtils.getDefaultMileageRate(selectedPolicy), }); - const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID ?? '-1'}`); + const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`); const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES); const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST); @@ -202,17 +202,22 @@ function MoneyRequestConfirmationList({ const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE; const isScanRequest = useMemo(() => TransactionUtils.isScanRequest(transaction), [transaction]); - const transactionID = transaction?.transactionID ?? '-1'; - const customUnitRateID = TransactionUtils.getRateID(transaction) ?? '-1'; + const transactionID = transaction?.transactionID; + const customUnitRateID = TransactionUtils.getRateID(transaction); useEffect(() => { - if ((customUnitRateID && customUnitRateID !== '-1') || !isDistanceRequest) { + if (customUnitRateID !== '-1' || !isDistanceRequest || !transactionID || !policy?.id) { return; } - const defaultRate = defaultMileageRate?.customUnitRateID ?? ''; - const lastSelectedRate = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate; + const defaultRate = defaultMileageRate?.customUnitRateID; + const lastSelectedRate = lastSelectedDistanceRates?.[policy.id] ?? defaultRate; const rateID = lastSelectedRate; + + if (!rateID) { + return; + } + IOU.setCustomUnitRateID(transactionID, rateID); }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, transactionID, isDistanceRequest]); @@ -242,6 +247,7 @@ function MoneyRequestConfirmationList({ if ( !shouldShowTax || !transaction || + !transactionID || (transaction.taxCode && previousTransactionModifiedCurrency === transaction.modifiedCurrency && previousTransactionCurrency === transaction.currency && @@ -296,7 +302,12 @@ function MoneyRequestConfirmationList({ return true; } - if (!participant.isInvoiceRoom && !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1)) { + if ( + !participant.isInvoiceRoom && + !participant.isPolicyExpenseChat && + !participant.isSelfDM && + ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? CONST.DEFAULT_NUMBER_ID) + ) { return true; } @@ -325,7 +336,7 @@ function MoneyRequestConfirmationList({ if (isFirstUpdatedDistanceAmount.current) { return; } - if (!isDistanceRequest) { + if (!isDistanceRequest || !transactionID) { return; } const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); @@ -334,7 +345,7 @@ function MoneyRequestConfirmationList({ }, [distance, rate, unit, transactionID, currency, isDistanceRequest]); useEffect(() => { - if (!shouldCalculateDistanceAmount) { + if (!shouldCalculateDistanceAmount || !transactionID) { return; } @@ -342,7 +353,7 @@ function MoneyRequestConfirmationList({ IOU.setMoneyRequestAmount(transactionID, amount, currency ?? ''); // If it's a split request among individuals, set the split shares - const participantAccountIDs: number[] = selectedParticipantsProp.map((participant) => participant.accountID ?? -1); + const participantAccountIDs: number[] = selectedParticipantsProp.map((participant) => participant.accountID ?? CONST.DEFAULT_NUMBER_ID); if (isTypeSplit && !isPolicyExpenseChat && amount && transaction?.currency) { IOU.setSplitShares(transaction, amount, currency, participantAccountIDs); } @@ -364,20 +375,25 @@ function MoneyRequestConfirmationList({ return; } - let taxableAmount: number; - let taxCode: string; + let taxableAmount: number | undefined; + let taxCode: string | undefined; if (isDistanceRequest) { - const customUnitRate = getDistanceRateCustomUnitRate(policy, customUnitRateID); - taxCode = customUnitRate?.attributes?.taxRateExternalID ?? ''; - taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance); + if (customUnitRateID) { + const customUnitRate = getDistanceRateCustomUnitRate(policy, customUnitRateID); + taxCode = customUnitRate?.attributes?.taxRateExternalID; + taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance); + } } else { taxableAmount = transaction.amount ?? 0; taxCode = transaction.taxCode ?? TransactionUtils.getDefaultTaxCode(policy, transaction) ?? ''; } - const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? ''; - const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, transaction.currency); - const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString())); - IOU.setMoneyRequestTaxAmount(transaction.transactionID ?? '', taxAmountInSmallestCurrencyUnits); + + if (taxCode && taxableAmount) { + const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? ''; + const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, transaction.currency); + const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString())); + IOU.setMoneyRequestTaxAmount(transaction.transactionID, taxAmountInSmallestCurrencyUnits); + } }, [ policy, shouldShowTax, @@ -522,7 +538,7 @@ function MoneyRequestConfirmationList({ rightElement: ( onSplitShareChange(participantOption.accountID ?? -1, Number(value))} + onAmountChange={(value: string) => onSplitShareChange(participantOption.accountID ?? CONST.DEFAULT_NUMBER_ID, Number(value))} maxLength={formattedTotalAmount.length} contentWidth={formattedTotalAmount.length * 8} /> @@ -637,7 +653,7 @@ function MoneyRequestConfirmationList({ }, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants]); useEffect(() => { - if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { + if (!isDistanceRequest || isMovingTransactionFromTrackExpense || !transactionID) { return; } @@ -669,16 +685,20 @@ function MoneyRequestConfirmationList({ // Auto select the category if there is only one enabled category and it is required useEffect(() => { const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled); - if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { + if (!transactionID || iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { return; } - IOU.setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? ''); + IOU.setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? '', policy?.id); // Keep 'transaction' out to ensure that we autoselect the option only once // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [shouldShowCategories, policyCategories, isCategoryRequired]); + }, [shouldShowCategories, policyCategories, isCategoryRequired, policy?.id]); // Auto select the tag if there is only one enabled tag and it is required useEffect(() => { + if (!transactionID) { + return; + } + let updatedTagsString = TransactionUtils.getTag(transaction); policyTagLists.forEach((tagList, index) => { const isTagListRequired = tagList.required ?? false; @@ -721,7 +741,7 @@ function MoneyRequestConfirmationList({ */ const confirm = useCallback( (paymentMethod: PaymentMethodType | undefined) => { - if (routeError) { + if (!!routeError || !transactionID) { return; } if (iouType === CONST.IOU.TYPE.INVOICE && !hasInvoicingDetails(policy)) { diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index e32c4eae410f..51cb2a6d6f39 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -163,7 +163,7 @@ type MoneyRequestConfirmationListFooterProps = { transaction: OnyxEntry; /** The transaction ID */ - transactionID: string; + transactionID: string | undefined; /** The unit */ unit: Unit | undefined; @@ -295,7 +295,7 @@ function MoneyRequestConfirmationListFooter({ description={translate('iou.amount')} interactive={!isReadOnly} onPress={() => { - if (isDistanceRequest) { + if (isDistanceRequest || !transactionID) { return; } @@ -326,6 +326,10 @@ function MoneyRequestConfirmationListFooter({ title={iouComment} description={translate('common.description')} onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); }} style={[styles.moneyRequestMenuItem]} @@ -349,7 +353,13 @@ function MoneyRequestConfirmationListFooter({ description={translate('common.distance')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} disabled={didConfirm} interactive={!isReadOnly} /> @@ -366,7 +376,13 @@ function MoneyRequestConfirmationListFooter({ description={translate('common.rate')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} disabled={didConfirm} interactive={!!rate && !isReadOnly && isPolicyExpenseChat} /> @@ -384,6 +400,10 @@ function MoneyRequestConfirmationListFooter({ style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); }} disabled={didConfirm} @@ -408,6 +428,10 @@ function MoneyRequestConfirmationListFooter({ style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); }} disabled={didConfirm} @@ -427,12 +451,16 @@ function MoneyRequestConfirmationListFooter({ title={iouCategory} description={translate('common.category')} numberOfLinesTitle={2} - onPress={() => + onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID), CONST.NAVIGATION.ACTION_TYPE.PUSH, - ) - } + ); + }} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} disabled={didConfirm} @@ -454,9 +482,13 @@ function MoneyRequestConfirmationListFooter({ title={TransactionUtils.getTagForDisplay(transaction, index)} description={name} numberOfLinesTitle={2} - onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)) - } + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); + }} style={[styles.moneyRequestMenuItem]} disabled={didConfirm} interactive={!isReadOnly} @@ -476,7 +508,13 @@ function MoneyRequestConfirmationListFooter({ description={taxRates?.name} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); + }} disabled={didConfirm} interactive={canModifyTaxFields} /> @@ -493,7 +531,13 @@ function MoneyRequestConfirmationListFooter({ description={translate('iou.taxAmount')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); + }} disabled={didConfirm} interactive={canModifyTaxFields} /> @@ -512,7 +556,13 @@ function MoneyRequestConfirmationListFooter({ }`} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} interactive shouldRenderAsHTML /> @@ -557,7 +607,13 @@ function MoneyRequestConfirmationListFooter({ {isLocalFile && Str.isPDF(receiptFilename) ? ( Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID)); + }} accessibilityRole={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} disabled={!shouldDisplayReceipt} @@ -570,7 +626,13 @@ function MoneyRequestConfirmationListFooter({ ) : ( Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID)); + }} disabled={!shouldDisplayReceipt || isThumbnail} accessibilityRole={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} @@ -625,7 +687,10 @@ function MoneyRequestConfirmationListFooter({ isLabelHoverable={false} interactive={!isReadOnly && canUpdateSenderWorkspace} onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '-1', reportID, Navigation.getActiveRouteWithoutParams())); + if (!transaction?.transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); }} style={styles.moneyRequestMenuItem} labelStyle={styles.mt2} @@ -644,11 +709,15 @@ function MoneyRequestConfirmationListFooter({ ? receiptThumbnailContent : shouldShowReceiptEmptyState && ( + onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } + ); + }} /> ))} {primaryFields} diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index 495c14ff76e1..6b83afe603c1 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -1,6 +1,6 @@ import 'core-js/proposals/promise-with-resolvers'; // eslint-disable-next-line import/extensions -import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker.min.mjs'; +import pdfWorkerSource from 'pdfjs-dist/build/pdf.worker.min.mjs'; import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import {Document, pdfjs, Thumbnail} from 'react-pdf'; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 65cdb4a7d00b..79497e5fab88 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -365,7 +365,8 @@ function ReportPreview({ const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation; - const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID) || ReportUtils.hasMissingInvoiceBankAccount(iouReportID); + const shouldPromptUserToAddBankAccount = + (ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID) || ReportUtils.hasMissingInvoiceBankAccount(iouReportID)) && !ReportUtils.isSettled(iouReportID); const shouldShowRBR = hasErrors && !iouSettled; /* diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index cdfcb22f1f2c..605fb284bf24 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -17,6 +17,7 @@ import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; import TextInputClearButton from '@components/TextInput/TextInputClearButton'; import TextInputLabel from '@components/TextInput/TextInputLabel'; +import useHtmlPaste from '@hooks/useHtmlPaste'; import useLocalize from '@hooks/useLocalize'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -99,6 +100,8 @@ function BaseTextInput( const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); + useHtmlPaste(input, undefined, isMarkdownEnabled); + // AutoFocus which only works on mount: useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 00675ca4ccd6..45aa868ad219 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,7 +1,7 @@ import {Str} from 'expensify-common'; -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, MutableRefObject} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native'; import {ActivityIndicator, StyleSheet, View} from 'react-native'; import {useSharedValue, withSpring} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; @@ -18,6 +18,7 @@ import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; import TextInputClearButton from '@components/TextInput/TextInputClearButton'; import TextInputLabel from '@components/TextInput/TextInputLabel'; +import useHtmlPaste from '@hooks/useHtmlPaste'; import useLocalize from '@hooks/useLocalize'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -107,6 +108,8 @@ function BaseTextInput( const isLabelActive = useRef(initialActiveLabel); const didScrollToEndRef = useRef(false); + useHtmlPaste(input as MutableRefObject, undefined, isMarkdownEnabled); + // AutoFocus which only works on mount: useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index 04349526aaea..cc875c25d14e 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -18,14 +18,9 @@ type WorkspaceSwitcherButtonOnyxProps = { policy: OnyxEntry; }; -type WorkspaceSwitcherButtonProps = WorkspaceSwitcherButtonOnyxProps & { - /** - * Callback used to keep track of the workspace switching process in the BaseSidebarScreen. - */ - onSwitchWorkspace?: () => void; -}; +type WorkspaceSwitcherButtonProps = WorkspaceSwitcherButtonOnyxProps; -function WorkspaceSwitcherButton({policy, onSwitchWorkspace}: WorkspaceSwitcherButtonProps) { +function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { const {translate} = useLocalize(); const theme = useTheme(); @@ -41,7 +36,7 @@ function WorkspaceSwitcherButton({policy, onSwitchWorkspace}: WorkspaceSwitcherB source: avatar, name: policy?.name ?? '', type: CONST.ICON_TYPE_WORKSPACE, - id: policy?.id ?? '-1', + id: policy?.id ?? CONST.DEFAULT_NUMBER_ID, }; }, [policy]); @@ -54,7 +49,6 @@ function WorkspaceSwitcherButton({policy, onSwitchWorkspace}: WorkspaceSwitcherB accessible testID="WorkspaceSwitcherButton" onPress={() => { - onSwitchWorkspace?.(); pressableRef?.current?.blur(); interceptAnonymousUser(() => { Navigation.navigate(ROUTES.WORKSPACE_SWITCHER); diff --git a/src/hooks/useCleanupSelectedOptions/index.ts b/src/hooks/useCleanupSelectedOptions/index.ts new file mode 100644 index 000000000000..7451e85aef23 --- /dev/null +++ b/src/hooks/useCleanupSelectedOptions/index.ts @@ -0,0 +1,21 @@ +import {NavigationContainerRefContext, useIsFocused} from '@react-navigation/native'; +import {useContext, useEffect} from 'react'; +import NAVIGATORS from '@src/NAVIGATORS'; + +const useCleanupSelectedOptions = (cleanupFunction?: () => void) => { + const navigationContainerRef = useContext(NavigationContainerRefContext); + const state = navigationContainerRef?.getState(); + const lastRoute = state?.routes.at(-1); + const isRightModalOpening = lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; + + const isFocused = useIsFocused(); + + useEffect(() => { + if (isFocused || isRightModalOpening) { + return; + } + cleanupFunction?.(); + }, [isFocused, cleanupFunction, isRightModalOpening]); +}; + +export default useCleanupSelectedOptions; diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts index ebffbf1b54b6..1a7e62f3141e 100644 --- a/src/hooks/useHtmlPaste/index.ts +++ b/src/hooks/useHtmlPaste/index.ts @@ -1,4 +1,3 @@ -import {useNavigation} from '@react-navigation/native'; import {useCallback, useEffect} from 'react'; import Parser from '@libs/Parser'; import CONST from '@src/CONST'; @@ -38,9 +37,7 @@ const insertAtCaret = (target: HTMLElement, insertedText: string, maxLength: num } }; -const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false, maxLength = CONST.MAX_COMMENT_LENGTH + 1) => { - const navigation = useNavigation(); - +const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, isActive = false, maxLength = CONST.MAX_COMMENT_LENGTH + 1) => { /** * Set pasted text to clipboard * @param {String} text @@ -145,27 +142,16 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi ); useEffect(() => { - // we need to re-register listener on navigation focus/blur if the component (like Composer) is not unmounting - // when navigating away to different screen (report) to avoid paste event on other screen being wrongly handled - // by current screen paste listener - let unsubscribeFocus: () => void; - let unsubscribeBlur: () => void; - if (removeListenerOnScreenBlur) { - unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste, true)); - unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste, true)); + if (!isActive) { + return; } - document.addEventListener('paste', handlePaste, true); return () => { - if (removeListenerOnScreenBlur) { - unsubscribeFocus(); - unsubscribeBlur(); - } document.removeEventListener('paste', handlePaste, true); }; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); + }, [isActive]); }; export default useHtmlPaste; diff --git a/src/hooks/useHtmlPaste/types.ts b/src/hooks/useHtmlPaste/types.ts index 0aaa12a49ac9..65778463f80e 100644 --- a/src/hooks/useHtmlPaste/types.ts +++ b/src/hooks/useHtmlPaste/types.ts @@ -4,7 +4,7 @@ import type {TextInput} from 'react-native'; type UseHtmlPaste = ( textInputRef: MutableRefObject<(HTMLTextAreaElement & TextInput) | TextInput | null>, preHtmlPasteCallback?: (event: ClipboardEvent) => boolean, - removeListenerOnScreenBlur?: boolean, + isActive?: boolean, maxLength?: number, // Maximum length of the text input value after pasting ) => void; diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts index 7aff640aed94..81796dae851d 100644 --- a/src/hooks/useOnboardingFlow.ts +++ b/src/hooks/useOnboardingFlow.ts @@ -1,11 +1,11 @@ import {useEffect} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors'; import Permissions from '@libs/Permissions'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; +import * as Session from '@userActions/Session'; import * as OnboardingFlow from '@userActions/Welcome/OnboardingFlow'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -27,8 +27,7 @@ function useOnboardingFlowRouter() { const [dismissedProductTraining, dismissedProductTrainingMetadata] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING); - const [session] = useOnyx(ONYXKEYS.SESSION); - const isPrivateDomain = !!session?.email && !LoginUtils.isEmailPublicDomain(session?.email); + const isPrivateDomain = Session.isUserOnPrivateDomain(); const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY); const [allBetas, allBetasMetadata] = useOnyx(ONYXKEYS.BETAS); diff --git a/src/languages/en.ts b/src/languages/en.ts index 080a87d01e86..efa982c3066c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -61,6 +61,7 @@ import type { DeleteConfirmationParams, DidSplitAmountMessageParams, EditActionParams, + EditDestinationSubtitleParams, ElectronicFundsParams, EnterMagicCodeParams, ExportAgainModalDescriptionParams, @@ -489,7 +490,6 @@ const translations = { skip: 'Skip', chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `Need something specific? Chat with your account manager, ${accountManagerDisplayName}.`, chatNow: 'Chat now', - validate: 'Validate', }, supportalNoAccess: { title: 'Not so fast', @@ -533,7 +533,7 @@ const translations = { attachmentImageResized: 'This image has been resized for previewing. Download for full resolution.', attachmentImageTooLarge: 'This image is too large to preview before uploading.', tooManyFiles: ({fileLimit}: FileLimitParams) => `You can only upload up to ${fileLimit} files at a time.`, - sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Files exceeds ${maxUploadSizeInMB}MB. Please try again.`, + sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Files exceeds ${maxUploadSizeInMB} MB. Please try again.`, }, filePicker: { fileError: 'File error', @@ -1106,7 +1106,7 @@ const translations = { viewPhoto: 'View photo', imageUploadFailed: 'Image upload failed', deleteWorkspaceError: 'Sorry, there was an unexpected problem deleting your workspace avatar', - sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `The selected image exceeds the maximum upload size of ${maxUploadSizeInMB}MB.`, + sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `The selected image exceeds the maximum upload size of ${maxUploadSizeInMB} MB.`, resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) => `Please upload an image larger than ${minHeightInPx}x${minWidthInPx} pixels and smaller than ${maxHeightInPx}x${maxWidthInPx} pixels.`, notAllowedExtension: ({allowedExtensions}: NotAllowedExtensionParams) => `Profile picture must be one of the following types: ${allowedExtensions.join(', ')}.`, @@ -1866,10 +1866,7 @@ const translations = { toUnblock: ' to unblock your login.', }, smsDeliveryFailurePage: { - smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) => - `We've been unable to deliver SMS messages to ${login}, so we've suspended it for 24 hours. Please try validating your number:`, - validationFailed: 'Validation failed because it hasn’t been 24 hours since your last attempt.', - validationSuccess: 'Your number has been validated! Click below to send a new magic sign-in code.', + smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) => `We've been unable to deliver SMS messages to ${login}, so we've suspended it for 24 hours.`, }, welcomeSignUpForm: { join: 'Join', @@ -2640,6 +2637,9 @@ const translations = { existingRateError: ({rate}: CustomUnitRateParams) => `A rate with value ${rate} already exists.`, }, importPerDiemRates: 'Import per diem rates', + editPerDiemRate: 'Edit per diem rate', + editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `Updating this destination will change it for all ${destination} per diem subrates.`, + editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `Updating this currency will change it for all ${destination} per diem subrates.`, }, qbd: { exportOutOfPocketExpensesDescription: 'Set how out-of-pocket expenses export to QuickBooks Desktop.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 908b50726c11..eb4cb728c6f1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -60,6 +60,7 @@ import type { DeleteConfirmationParams, DidSplitAmountMessageParams, EditActionParams, + EditDestinationSubtitleParams, ElectronicFundsParams, EnterMagicCodeParams, ExportAgainModalDescriptionParams, @@ -480,7 +481,6 @@ const translations = { minuteAbbreviation: 'm', chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `¿Necesitas algo específico? Habla con tu gerente de cuenta, ${accountManagerDisplayName}.`, chatNow: 'Chatear ahora', - validate: 'Validar', }, supportalNoAccess: { title: 'No tan rápido', @@ -528,7 +528,7 @@ const translations = { attachmentImageResized: 'Se ha cambiado el tamaño de esta imagen para obtener una vista previa. Descargar para resolución completa.', attachmentImageTooLarge: 'Esta imagen es demasiado grande para obtener una vista previa antes de subirla.', tooManyFiles: ({fileLimit}: FileLimitParams) => `Solamente puedes suber ${fileLimit} archivos a la vez.`, - sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `El archivo supera los ${maxUploadSizeInMB}MB. Por favor, vuelve a intentarlo.`, + sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `El archivo supera los ${maxUploadSizeInMB} MB. Por favor, vuelve a intentarlo.`, }, filePicker: { fileError: 'Error de archivo', @@ -1104,7 +1104,7 @@ const translations = { viewPhoto: 'Ver foto', imageUploadFailed: 'Error al cargar la imagen', deleteWorkspaceError: 'Lo sentimos, hubo un problema eliminando el avatar de tu espacio de trabajo', - sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB}MB.`, + sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB} MB.`, resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) => `Por favor, elige una imagen más grande que ${minHeightInPx}x${minWidthInPx} píxeles y más pequeña que ${maxHeightInPx}x${maxWidthInPx} píxeles.`, notAllowedExtension: ({allowedExtensions}: NotAllowedExtensionParams) => `La foto de perfil debe ser de uno de los siguientes tipos: ${allowedExtensions.join(', ')}.`, @@ -1871,10 +1871,7 @@ const translations = { toUnblock: ' para desbloquear el inicio de sesión.', }, smsDeliveryFailurePage: { - smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) => - `No hemos podido entregar mensajes SMS a ${login}, así que lo hemos suspendido durante 24 horas. Por favor, intenta validar tu número:`, - validationFailed: 'La validación falló porque no han pasado 24 horas desde tu último intento.', - validationSuccess: '¡Tu número ha sido validado! Haz clic abajo para enviar un nuevo código mágico de inicio de sesión.', + smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) => `No hemos podido entregar mensajes SMS a ${login}, por lo que lo hemos suspendido durante 24 horas.`, }, welcomeSignUpForm: { join: 'Unirse', @@ -2664,6 +2661,9 @@ const translations = { existingRateError: ({rate}: CustomUnitRateParams) => `Ya existe una tasa con el valor ${rate}.`, }, importPerDiemRates: 'Importar tasas de per diem', + editPerDiemRate: 'Editar la tasa de per diem', + editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `Actualizar este destino lo modificará para todas las subtasas per diem de ${destination}.`, + editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `Actualizar esta moneda la modificará para todas las subtasas per diem de ${destination}.`, }, qbd: { exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Desktop.', diff --git a/src/languages/params.ts b/src/languages/params.ts index 59e1a061f7bf..f9ca26a3575a 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -586,6 +586,10 @@ type ChatWithAccountManagerParams = { accountManagerDisplayName: string; }; +type EditDestinationSubtitleParams = { + destination: string; +}; + type FlightLayoverParams = { layover: string; }; @@ -798,5 +802,6 @@ export type { CompanyNameParams, CustomUnitRateParams, ChatWithAccountManagerParams, + EditDestinationSubtitleParams, FlightLayoverParams, }; diff --git a/src/libs/API/parameters/ResetSMSDeliveryFailureParams.ts b/src/libs/API/parameters/ResetSMSDeliveryFailureParams.ts deleted file mode 100644 index f9a0bf41a218..000000000000 --- a/src/libs/API/parameters/ResetSMSDeliveryFailureParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -type ResetSMSDeliveryFailureParams = { - login: string; -}; - -export default ResetSMSDeliveryFailureParams; diff --git a/src/libs/API/parameters/TransactionMergeParams.ts b/src/libs/API/parameters/TransactionMergeParams.ts index 9e2516e2637f..ad718d37e6c8 100644 --- a/src/libs/API/parameters/TransactionMergeParams.ts +++ b/src/libs/API/parameters/TransactionMergeParams.ts @@ -1,5 +1,5 @@ type TransactionMergeParams = { - transactionID: string; + transactionID: string | undefined; transactionIDList: string[]; created: string; merchant: string; @@ -11,7 +11,7 @@ type TransactionMergeParams = { reimbursable: boolean; tag: string; receiptID: number; - reportID: string; + reportID: string | undefined; }; export default TransactionMergeParams; diff --git a/src/libs/API/parameters/UpdateWorkspaceCustomUnitParams.ts b/src/libs/API/parameters/UpdateWorkspaceCustomUnitParams.ts new file mode 100644 index 000000000000..fa1fc3d8c911 --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceCustomUnitParams.ts @@ -0,0 +1,6 @@ +type UpdateWorkspaceCustomUnitParams = { + policyID: string; + customUnit: string; +}; + +export default UpdateWorkspaceCustomUnitParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index dee39392a561..7e8b8cec520b 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -356,6 +356,6 @@ export type {default as TogglePlatformMuteParams} from './TogglePlatformMutePara export type {default as JoinAccessiblePolicyParams} from './JoinAccessiblePolicyParams'; export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesParams'; export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams'; +export type {default as UpdateWorkspaceCustomUnitParams} from './UpdateWorkspaceCustomUnitParams'; export type {default as DismissProductTrainingParams} from './DismissProductTraining'; export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage'; -export type {default as ResetSMSDeliveryFailureParams} from './ResetSMSDeliveryFailureParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 65324262de9f..aa9831ca4053 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -440,9 +440,9 @@ const WRITE_COMMANDS = { SELF_TOUR_VIEWED: 'SelfTourViewed', UPDATE_INVOICE_COMPANY_NAME: 'UpdateInvoiceCompanyName', UPDATE_INVOICE_COMPANY_WEBSITE: 'UpdateInvoiceCompanyWebsite', + UPDATE_WORKSPACE_CUSTOM_UNIT: 'UpdateWorkspaceCustomUnit', VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies', DISMISS_PRODUCT_TRAINING: 'DismissProductTraining', - RESET_SMS_DELIVERY_FAILURE: 'ResetSMSDeliveryFailure', } as const; type WriteCommand = ValueOf; @@ -768,7 +768,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams; [WRITE_COMMANDS.REQUEST_TAX_EXEMPTION]: null; - [WRITE_COMMANDS.RESET_SMS_DELIVERY_FAILURE]: Parameters.ResetSMSDeliveryFailureParams; + [WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT]: Parameters.UpdateWorkspaceCustomUnitParams; [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; [WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams; diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 9343164db1e8..cfc56559ed35 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -921,6 +921,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) return validateString(value); case 'created': case 'modifiedCreated': + case 'inserted': case 'posted': return validateDate(value); case 'isLoading': @@ -1046,6 +1047,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) cardNumber: CONST.RED_BRICK_ROAD_PENDING_ACTION, managedCard: CONST.RED_BRICK_ROAD_PENDING_ACTION, posted: CONST.RED_BRICK_ROAD_PENDING_ACTION, + inserted: CONST.RED_BRICK_ROAD_PENDING_ACTION, }, 'string', ); diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index fe40ea67f905..c41b33873a8a 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -292,17 +292,25 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number { function getCustomUnitRateID(reportID: string) { const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; - const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID ?? '-1'); + const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID); let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; + if (isEmptyObject(policy)) { + return customUnitRateID; + } + if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { - const distanceUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); - const lastSelectedDistanceRateID = lastSelectedDistanceRates?.[policy?.id ?? '-1'] ?? '-1'; - const lastSelectedDistanceRate = distanceUnit?.rates[lastSelectedDistanceRateID] ?? {}; - if (lastSelectedDistanceRate.enabled && lastSelectedDistanceRateID) { + const distanceUnit = Object.values(policy.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); + const lastSelectedDistanceRateID = lastSelectedDistanceRates?.[policy.id]; + const lastSelectedDistanceRate = lastSelectedDistanceRateID ? distanceUnit?.rates[lastSelectedDistanceRateID] : undefined; + if (lastSelectedDistanceRate?.enabled && lastSelectedDistanceRateID) { customUnitRateID = lastSelectedDistanceRateID; } else { - customUnitRateID = getDefaultMileageRate(policy)?.customUnitRateID ?? '-1'; + const defaultMileageRate = getDefaultMileageRate(policy); + if (!defaultMileageRate?.customUnitRateID) { + return customUnitRateID; + } + customUnitRateID = defaultMileageRate.customUnitRateID; } } diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 8dc46204db3c..9ed192b09233 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -1,3 +1,4 @@ +import {Str} from 'expensify-common'; import type {OnyxEntry} from 'react-native-onyx'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; @@ -6,16 +7,19 @@ import type {LoginList, PrivatePersonalDetails} from '@src/types/onyx'; import * as LoginUtils from './LoginUtils'; import Navigation from './Navigation/Navigation'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; +import * as PhoneNumberUtils from './PhoneNumber'; import * as UserUtils from './UserUtils'; function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry): Route { const {legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; const address = PersonalDetailsUtils.getCurrentAddress(privatePersonalDetails); + const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumber ?? ''); + const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode); if (!legalFirstName && !legalLastName) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain); } - if (!phoneNumber || !LoginUtils.validateNumber(phoneNumber)) { + if (!phoneNumber || !parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain); } if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) { diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts index 85f95c146dac..12c1931b0199 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.ts +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -10,7 +10,6 @@ const requestsToIgnoreLastUpdateID: string[] = [ SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, WRITE_COMMANDS.CLOSE_ACCOUNT, WRITE_COMMANDS.DELETE_MONEY_REQUEST, - WRITE_COMMANDS.SUBMIT_REPORT, SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES, ]; diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts index 206bb8509af6..d76c9325cc0e 100644 --- a/src/libs/MoneyRequestUtils.ts +++ b/src/libs/MoneyRequestUtils.ts @@ -32,19 +32,23 @@ function stripDecimalsFromAmount(amount: string): string { * Adds a leading zero to the amount if user entered just the decimal separator * * @param amount - Changed amount from user input + * @param shouldAllowNegative - Should allow negative numbers */ -function addLeadingZero(amount: string): string { +function addLeadingZero(amount: string, shouldAllowNegative = false): string { + if (shouldAllowNegative && amount.startsWith('-.')) { + return `-0${amount}`; + } return amount.startsWith('.') ? `0${amount}` : amount; } /** * Check if amount is a decimal up to 3 digits */ -function validateAmount(amount: string, decimals: number, amountMaxLength: number = CONST.IOU.AMOUNT_MAX_LENGTH): boolean { +function validateAmount(amount: string, decimals: number, amountMaxLength: number = CONST.IOU.AMOUNT_MAX_LENGTH, shouldAllowNegative = false): boolean { const regexString = decimals === 0 - ? `^\\d{1,${amountMaxLength}}$` // Don't allow decimal point if decimals === 0 - : `^\\d{1,${amountMaxLength}}(\\.\\d{0,${decimals}})?$`; // Allow the decimal point and the desired number of digits after the point + ? `^${shouldAllowNegative ? '-?' : ''}\\d{1,${amountMaxLength}}$` // Don't allow decimal point if decimals === 0 + : `^${shouldAllowNegative ? '-?' : ''}\\d{1,${amountMaxLength}}(\\.\\d{0,${decimals}})?$`; // Allow the decimal point and the desired number of digits after the point const decimalNumberRegex = new RegExp(regexString, 'i'); return amount === '' || decimalNumberRegex.test(amount); } diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index e01d0fe3115f..e3f34ea3bea3 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -25,6 +25,7 @@ import Log from '@libs/Log'; import NavBarManager from '@libs/NavBarManager'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; import Presentation from '@libs/Navigation/PlatformStackNavigation/navigationOptions/presentation'; import type {PlatformStackNavigationOptions} from '@libs/Navigation/PlatformStackNavigation/types'; import shouldOpenOnAdminRoom from '@libs/Navigation/shouldOpenOnAdminRoom'; @@ -147,7 +148,7 @@ Onyx.connect({ return; } - currentAccountID = value.accountID ?? -1; + currentAccountID = value.accountID ?? CONST.DEFAULT_NUMBER_ID; if (Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) { // This means sign in in RHP was successful, so we can subscribe to user events @@ -249,7 +250,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie } const initialReport = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom(), activeWorkspaceID); - return initialReport?.reportID ?? ''; + return initialReport?.reportID; }); useEffect(() => { @@ -464,7 +465,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie options={{ headerShown: false, presentation: Presentation.TRANSPARENT_MODAL, - animation: 'none', + animation: Animations.NONE, }} getComponent={loadProfileAvatar} listeners={modalScreenListeners} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 909d37485252..7e5ac879cf60 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -577,6 +577,11 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/perDiem/ImportPerDiemPage').default, [SCREENS.WORKSPACE.PER_DIEM_IMPORTED]: () => require('../../../../pages/workspace/perDiem/ImportedPerDiemPage').default, [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemSettingsPage').default, + [SCREENS.WORKSPACE.PER_DIEM_DETAILS]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemDetailsPage').default, + [SCREENS.WORKSPACE.PER_DIEM_EDIT_DESTINATION]: () => require('../../../../pages/workspace/perDiem/EditPerDiemDestinationPage').default, + [SCREENS.WORKSPACE.PER_DIEM_EDIT_SUBRATE]: () => require('../../../../pages/workspace/perDiem/EditPerDiemSubratePage').default, + [SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT]: () => require('../../../../pages/workspace/perDiem/EditPerDiemAmountPage').default, + [SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: () => require('../../../../pages/workspace/perDiem/EditPerDiemCurrencyPage').default, }); const EnablePaymentsStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 01caa79692f1..c72c4de01e4e 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -22,15 +22,9 @@ type TopBarProps = { activeWorkspaceID?: string; shouldDisplaySearch?: boolean; shouldDisplayCancelSearch?: boolean; - - /** - * Callback used to keep track of the workspace switching process in the BaseSidebarScreen. - * Passed to the WorkspaceSwitcherButton component. - */ - onSwitchWorkspace?: () => void; }; -function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false, onSwitchWorkspace}: TopBarProps) { +function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const policy = usePolicy(activeWorkspaceID); @@ -53,10 +47,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, dataSet={{dragArea: true}} > - + > = { SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE, SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT, ], - [SCREENS.WORKSPACE.PER_DIEM]: [SCREENS.WORKSPACE.PER_DIEM_IMPORT, SCREENS.WORKSPACE.PER_DIEM_IMPORTED, SCREENS.WORKSPACE.PER_DIEM_SETTINGS], + [SCREENS.WORKSPACE.PER_DIEM]: [ + SCREENS.WORKSPACE.PER_DIEM_IMPORT, + SCREENS.WORKSPACE.PER_DIEM_IMPORTED, + SCREENS.WORKSPACE.PER_DIEM_SETTINGS, + SCREENS.WORKSPACE.PER_DIEM_DETAILS, + SCREENS.WORKSPACE.PER_DIEM_EDIT_DESTINATION, + SCREENS.WORKSPACE.PER_DIEM_EDIT_SUBRATE, + SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT, + SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY, + ], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 30b64a79dca2..04ed0261a225 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -974,6 +974,21 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: { path: ROUTES.WORKSPACE_PER_DIEM_SETTINGS.route, }, + [SCREENS.WORKSPACE.PER_DIEM_DETAILS]: { + path: ROUTES.WORKSPACE_PER_DIEM_DETAILS.route, + }, + [SCREENS.WORKSPACE.PER_DIEM_EDIT_DESTINATION]: { + path: ROUTES.WORKSPACE_PER_DIEM_EDIT_DESTINATION.route, + }, + [SCREENS.WORKSPACE.PER_DIEM_EDIT_SUBRATE]: { + path: ROUTES.WORKSPACE_PER_DIEM_EDIT_SUBRATE.route, + }, + [SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT]: { + path: ROUTES.WORKSPACE_PER_DIEM_EDIT_AMOUNT.route, + }, + [SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: { + path: ROUTES.WORKSPACE_PER_DIEM_EDIT_CURRENCY.route, + }, }, }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts index 144a56c7e522..ecb0a2f1220b 100644 --- a/src/libs/Navigation/switchPolicyID.ts +++ b/src/libs/Navigation/switchPolicyID.ts @@ -1,4 +1,4 @@ -import {CommonActions, getActionFromState} from '@react-navigation/core'; +import {getActionFromState} from '@react-navigation/core'; import type {NavigationAction, NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native'; import {getPathFromState} from '@react-navigation/native'; import type {Writable} from 'type-fest'; @@ -52,17 +52,6 @@ function getActionForBottomTabNavigator(action: StackNavigationAction, state: Na params.policyID = policyID; } - // If the last route in the BottomTabNavigator is already a 'Home' route, we want to change the params rather than pushing a new 'Home' route, - // so that the screen does not get re-mounted. This would cause an empty screen/white flash when navigating back from the workspace switcher. - const homeRoute = bottomTabNavigatorRoute.state.routes.at(-1); - if (homeRoute && homeRoute.name === SCREENS.HOME) { - return { - ...CommonActions.setParams(params), - source: homeRoute?.key, - }; - } - - // If there is no 'Home' route in the BottomTabNavigator or if we are updating a different navigator, we want to push a new route. return { type: CONST.NAVIGATION.ACTION_TYPE.PUSH, payload: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 27671fac401f..71f11113e84c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -908,6 +908,31 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.PER_DIEM_DETAILS]: { + policyID: string; + rateID: string; + subRateID: string; + }; + [SCREENS.WORKSPACE.PER_DIEM_EDIT_DESTINATION]: { + policyID: string; + rateID: string; + subRateID: string; + }; + [SCREENS.WORKSPACE.PER_DIEM_EDIT_SUBRATE]: { + policyID: string; + rateID: string; + subRateID: string; + }; + [SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT]: { + policyID: string; + rateID: string; + subRateID: string; + }; + [SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: { + policyID: string; + rateID: string; + subRateID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 5bed8780c10a..35b7b6a023ce 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -785,7 +785,7 @@ function getPolicyExpenseReportOption(participant: Participant | ReportUtils.Opt const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? ReportUtils.getReportOrDraftReport(participant.reportID) : null; const visibleParticipantAccountIDs = Object.entries(expenseReport?.participants ?? {}) - .filter(([, reportParticipant]) => reportParticipant && reportParticipant.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) + .filter(([, reportParticipant]) => reportParticipant && !ReportUtils.isHiddenForCurrentUser(reportParticipant.notificationPreference)) .map(([accountID]) => Number(accountID)); const option = createOption( @@ -1859,6 +1859,9 @@ function filterAndOrderOptions(options: Options, searchInputValue: string, confi let {recentReports: filteredReports, personalDetails: filteredPersonalDetails} = filterResult; + // on staging server, in specific cases (see issue) BE returns duplicated personalDetails entries + filteredPersonalDetails = filteredPersonalDetails.filter((detail, index, array) => array.findIndex((i) => i.login === detail.login) === index); + if (typeof config?.maxRecentReportsToShow === 'number') { filteredReports = orderReportOptionsWithSearch(filteredReports, searchInputValue, config); filteredReports = filteredReports.slice(0, config.maxRecentReportsToShow); @@ -1900,7 +1903,7 @@ function getEmptyOptions(): Options { function shouldUseBoldText(report: ReportUtils.OptionData): boolean { const notificationPreference = report.notificationPreference ?? ReportUtils.getReportNotificationPreference(report); - return report.isUnread === true && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + return report.isUnread === true && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE && !ReportUtils.isHiddenForCurrentUser(notificationPreference); } export { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 1c85651efd51..85a9dd7b1a77 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -39,7 +39,7 @@ import * as Localize from './Localize'; import Navigation from './Navigation/Navigation'; import * as NetworkStore from './Network/NetworkStore'; import {getAccountIDsByLogins, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils'; -import {getAllReportTransactions, getCategory, getTag} from './TransactionUtils'; +import {getAllSortedTransactions, getCategory, getTag} from './TransactionUtils'; type MemberEmailsToAccountIDs = Record; @@ -182,7 +182,7 @@ function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => stri return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.toString().length); } -function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate?: Rate, withDecimals?: boolean) { +function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate?: Partial, withDecimals?: boolean) { return getRateDisplayValue((customUnitRate?.rate ?? 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit, withDecimals); } @@ -546,37 +546,35 @@ function getDefaultApprover(policy: OnyxEntry | SearchPolicy): string { return policy?.approver ?? policy?.owner ?? ''; } -/** - * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. - */ -function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { - const employeeAccountID = expenseReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; - const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; - const defaultApprover = getDefaultApprover(policy); - - let categoryAppover; - let tagApprover; - const allTransactions = getAllReportTransactions(expenseReport?.reportID).sort((transA, transB) => (transA.created < transB.created ? -1 : 1)); +function getRuleApprovers(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry) { + const categoryAppovers: string[] = []; + const tagApprovers: string[] = []; + const allReportTransactions = getAllSortedTransactions(expenseReport?.reportID); // Before submitting to their `submitsTo` (in a policy on Advanced Approvals), submit to category/tag approvers. // Category approvers are prioritized, then tag approvers. - for (let i = 0; i < allTransactions.length; i++) { - const transaction = allTransactions.at(i); + for (let i = 0; i < allReportTransactions.length; i++) { + const transaction = allReportTransactions.at(i); const tag = getTag(transaction); const category = getCategory(transaction); - categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; + const categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; + const tagApprover = getTagApproverRule(policy, tag)?.approver; if (categoryAppover) { - return getAccountIDsByLogins([categoryAppover]).at(0) ?? -1; + categoryAppovers.push(categoryAppover); } - if (!tagApprover && getTagApproverRule(policy, tag)?.approver) { - tagApprover = getTagApproverRule(policy, tag)?.approver; + if (tagApprover) { + tagApprovers.push(tagApprover); } } - if (tagApprover) { - return getAccountIDsByLogins([tagApprover]).at(0) ?? -1; - } + return [...categoryAppovers, ...tagApprovers]; +} + +function getManagerAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry) { + const employeeAccountID = expenseReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; + const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; + const defaultApprover = getDefaultApprover(policy); // For policy using the optional or basic workflow, the manager is the policy default approver. if (([CONST.POLICY.APPROVAL_MODE.OPTIONAL, CONST.POLICY.APPROVAL_MODE.BASIC] as Array>).includes(getApprovalWorkflow(policy))) { @@ -591,9 +589,21 @@ function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseR return getAccountIDsByLogins([employee.submitsTo ?? defaultApprover]).at(0) ?? -1; } -function getSubmitToEmail(policy: OnyxEntry, expenseReport: OnyxEntry): string { - const submitToAccountID = getSubmitToAccountID(policy, expenseReport); - return getLoginsByAccountIDs([submitToAccountID]).at(0) ?? ''; +/** + * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. + */ +function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { + const ruleApprovers = getRuleApprovers(policy, expenseReport); + if (ruleApprovers.length > 0 && !isSubmitAndClose(policy)) { + return getAccountIDsByLogins([ruleApprovers.at(0) ?? '']).at(0) ?? -1; + } + + return getManagerAccountID(policy, expenseReport); +} + +function getManagerAccountEmail(policy: OnyxEntry, expenseReport: OnyxEntry): string { + const managerAccountID = getManagerAccountID(policy, expenseReport); + return getLoginsByAccountIDs([managerAccountID]).at(0) ?? ''; } /** @@ -1273,7 +1283,6 @@ export { getCurrentTaxID, areSettingsInErrorFields, settingsPendingAction, - getSubmitToEmail, getForwardsToAccount, getSubmitToAccountID, getWorkspaceAccountID, @@ -1290,6 +1299,8 @@ export { isPolicyAccessible, areAllGroupPoliciesExpenseChatDisabled, hasOtherControlWorkspaces, + getManagerAccountEmail, + getRuleApprovers, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9d8cbe19f097..c20ec7386b0a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1294,12 +1294,9 @@ function getDefaultNotificationPreferenceForReport(report: OnyxEntry): V } /** - * Get the notification preference given a report + * Get the notification preference given a report. This should ALWAYS default to 'hidden'. Do not change this! */ -function getReportNotificationPreference(report: OnyxEntry, shouldDefaltToHidden = true): ValueOf { - if (!shouldDefaltToHidden) { - return report?.participants?.[currentUserAccountID ?? -1]?.notificationPreference ?? getDefaultNotificationPreferenceForReport(report); - } +function getReportNotificationPreference(report: OnyxEntry): ValueOf { return report?.participants?.[currentUserAccountID ?? -1]?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; } @@ -1415,6 +1412,23 @@ function canCreateTaskInReport(report: OnyxEntry): boolean { return true; } +/** + * For all intents and purposes a report that has no notificationPreference at all should be considered "hidden". + * We will remove the 'hidden' field entirely once the backend changes for https://github.com/Expensify/Expensify/issues/450891 are done. + */ +function isHiddenForCurrentUser(notificationPreference: string | null | undefined): boolean; +function isHiddenForCurrentUser(report: OnyxEntry): boolean; +function isHiddenForCurrentUser(reportOrPreference: OnyxEntry | string | null | undefined): boolean { + if (typeof reportOrPreference === 'object' && reportOrPreference !== null) { + const notificationPreference = getReportNotificationPreference(reportOrPreference); + return isHiddenForCurrentUser(notificationPreference); + } + if (reportOrPreference === undefined || reportOrPreference === null || reportOrPreference === '') { + return true; + } + return reportOrPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; +} + /** * Returns true if there are any guides accounts (team.expensify.com) in a list of accountIDs * by cross-referencing the accountIDs with personalDetails since guides that are participants @@ -1426,7 +1440,7 @@ function hasExpensifyGuidesEmails(accountIDs: number[]): boolean { function getMostRecentlyVisitedReport(reports: Array>, reportMetadata: OnyxCollection): OnyxEntry { const filteredReports = reports.filter((report) => { - const shouldKeep = !isChatThread(report) || getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const shouldKeep = !isChatThread(report) || !isHiddenForCurrentUser(report); return shouldKeep && !!report?.reportID && !!(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]?.lastVisitTime ?? report?.lastReadTime); }); return lodashMaxBy(filteredReports, (a) => new Date(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${a?.reportID}`]?.lastVisitTime ?? a?.lastReadTime ?? '').valueOf()); @@ -2233,7 +2247,7 @@ function getParticipantsAccountIDsForDisplay(report: OnyxEntry, shouldEx return false; } - if (shouldExcludeHidden && reportParticipants[accountID]?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + if (shouldExcludeHidden && isHiddenForCurrentUser(reportParticipants[accountID]?.notificationPreference)) { return false; } @@ -2833,7 +2847,7 @@ function getReasonAndReportActionThatRequiresAttention( }; } - if (hasMissingInvoiceBankAccount(optionOrReport.reportID)) { + if (hasMissingInvoiceBankAccount(optionOrReport.reportID) && !isSettled(optionOrReport.reportID)) { return { reason: CONST.REQUIRES_ATTENTION_REASONS.HAS_MISSING_INVOICE_BANK_ACCOUNT, }; @@ -2841,7 +2855,11 @@ function getReasonAndReportActionThatRequiresAttention( if (isInvoiceRoom(optionOrReport)) { const reportAction = Object.values(reportActions).find( - (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && action.childReportID && hasMissingInvoiceBankAccount(action.childReportID), + (action) => + action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && + action.childReportID && + hasMissingInvoiceBankAccount(action.childReportID) && + !isSettled(action.childReportID), ); return reportAction @@ -8115,7 +8133,7 @@ function canJoinChat(report: OnyxEntry, parentReportAction: OnyxInputOrE } // If the notification preference of the chat is not hidden that means we have already joined the chat - if (getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + if (!isHiddenForCurrentUser(report)) { return false; } @@ -8149,7 +8167,7 @@ function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boo return false; } - if (getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + if (isHiddenForCurrentUser(report)) { return false; } @@ -8468,20 +8486,41 @@ function isExported(reportActions: OnyxEntry) { function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; + const fullApprovalChain: string[] = []; const reportTotal = expenseReport?.total ?? 0; + const submitterEmail = PersonalDetailsUtils.getLoginsByAccountIDs([expenseReport?.ownerAccountID ?? -1]).at(0) ?? ''; - // If the policy is not on advanced approval mode, we should not use the approval chain even if it exists. - if (!PolicyUtils.isControlOnAdvancedApprovalMode(policy)) { + if (PolicyUtils.isSubmitAndClose(policy)) { return approvalChain; } - let nextApproverEmail = PolicyUtils.getSubmitToEmail(policy, expenseReport); + // Get category/tag approver list + const ruleApprovers = PolicyUtils.getRuleApprovers(policy, expenseReport); + + // Push rule approvers to approvalChain list before submitsTo/forwardsTo approvers + ruleApprovers.forEach((ruleApprover) => { + // Don't push submiiter to approve as a rule approver + if (fullApprovalChain.includes(ruleApprover) || ruleApprover === submitterEmail) { + return; + } + fullApprovalChain.push(ruleApprover); + }); + + let nextApproverEmail = PolicyUtils.getManagerAccountEmail(policy, expenseReport); while (nextApproverEmail && !approvalChain.includes(nextApproverEmail)) { approvalChain.push(nextApproverEmail); nextApproverEmail = PolicyUtils.getForwardsToAccount(policy, nextApproverEmail, reportTotal); } - return approvalChain; + + approvalChain.forEach((approver) => { + if (fullApprovalChain.includes(approver)) { + return; + } + + fullApprovalChain.push(approver); + }); + return fullApprovalChain; } /** @@ -8830,6 +8869,7 @@ export { getAllReportActionsErrorsAndReportActionThatRequiresAttention, hasInvoiceReports, getReportMetadata, + isHiddenForCurrentUser, }; export type { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 4d0f74e54b1d..626dc8d5ed68 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -125,7 +125,7 @@ function getOrderedReportIDs( } const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); const doesReportHaveViolations = ReportUtils.shouldDisplayViolationsRBRInLHN(report, transactionViolations); - const isHidden = ReportUtils.getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const isHidden = ReportUtils.isHiddenForCurrentUser(report); const isFocused = report.reportID === currentReportId; const hasErrorsOtherThanFailedReceipt = ReportUtils.hasReportErrorsOtherThanFailedReceipt(report, doesReportHaveViolations, transactionViolations); const isReportInAccessible = report?.errorFields?.notFound; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 81a738f724e0..6643cd721d45 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -9,7 +9,8 @@ import type {ValueOf} from 'type-fest'; import {getPolicyCategoriesData} from '@libs/actions/Policy/Category'; import {getPolicyTagsData} from '@libs/actions/Policy/Tag'; import type {TransactionMergeParams} from '@libs/API/parameters'; -import {getCurrencyDecimals} from '@libs/CurrencyUtils'; +import {getCategoryDefaultTaxRate} from '@libs/CategoryUtils'; +import {convertToBackendAmount, getCurrencyDecimals} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {toLocaleDigit} from '@libs/LocaleDigitUtils'; @@ -77,7 +78,7 @@ Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { currentUserEmail = val?.email ?? ''; - currentUserAccountID = val?.accountID ?? -1; + currentUserAccountID = val?.accountID ?? CONST.DEFAULT_NUMBER_ID; }, }); @@ -189,6 +190,7 @@ function buildOptimisticTransaction( billable, reimbursable, attendees, + inserted: DateUtils.getDBTime(), }; } @@ -578,7 +580,7 @@ function getCategory(transaction: OnyxInputOrEntry): string { * Return the cardID from the transaction. */ function getCardID(transaction: Transaction): number { - return transaction?.cardID ?? -1; + return transaction?.cardID ?? CONST.DEFAULT_NUMBER_ID; } /** @@ -963,7 +965,7 @@ function getDefaultTaxCode(policy: OnyxEntry, transaction: OnyxEntry, transaction: OnyxEntry taxRate.code === (transaction?.taxCode ?? defaultTaxCode))?.modifiedName; } -function getTransaction(transactionID: string): OnyxEntry { +function getTransaction(transactionID: string | undefined): OnyxEntry { return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; } @@ -1057,7 +1059,15 @@ function removeSettledAndApprovedTransactions(transactionIDs: string[]) { * 6. It returns the 'keep' and 'change' objects. */ -function compareDuplicateTransactionFields(reviewingTransactionID: string, reportID: string, selectedTransactionID?: string): {keep: Partial; change: FieldsToChange} { +function compareDuplicateTransactionFields( + reviewingTransactionID: string | undefined, + reportID: string | undefined, + selectedTransactionID?: string, +): {keep: Partial; change: FieldsToChange} { + if (!reviewingTransactionID || !reportID) { + return {change: {}, keep: {}}; + } + const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${reviewingTransactionID}`]; const duplicates = transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? []; const transactions = removeSettledAndApprovedTransactions([reviewingTransactionID, ...duplicates]).map((item) => getTransaction(item)); @@ -1158,7 +1168,7 @@ function compareDuplicateTransactionFields(reviewingTransactionID: string, repor } } else if (fieldName === 'category') { const differentValues = getDifferentValues(transactions, keys); - const policyCategories = getPolicyCategoriesData(report?.policyID ?? '-1'); + const policyCategories = report?.policyID ? getPolicyCategoriesData(report.policyID) : {}; const availableCategories = Object.values(policyCategories) .filter((category) => differentValues.includes(category.name) && category.enabled && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) .map((e) => e.name); @@ -1169,7 +1179,7 @@ function compareDuplicateTransactionFields(reviewingTransactionID: string, repor keep[fieldName] = firstTransaction?.[keys[0]] ?? firstTransaction?.[keys[1]]; } } else if (fieldName === 'tag') { - const policyTags = getPolicyTagsData(report?.policyID ?? '-1'); + const policyTags = report?.policyID ? getPolicyTagsData(report?.policyID) : {}; const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags); if (isMultiLevelTags) { if (areAllFieldsEqualForKey || !policy?.areTagsEnabled) { @@ -1200,10 +1210,10 @@ function compareDuplicateTransactionFields(reviewingTransactionID: string, repor return {keep, change}; } -function getTransactionID(threadReportID: string): string { +function getTransactionID(threadReportID: string): string | undefined { const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${threadReportID}`]; - const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); - const IOUTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; + const parentReportAction = ReportUtils.isThread(report) ? ReportActionsUtils.getReportAction(report.parentReportID, report.parentReportActionID) : undefined; + const IOUTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined; return IOUTransactionID; } @@ -1224,11 +1234,11 @@ function buildNewTransactionAfterReviewingDuplicates(reviewDuplicateTransaction: function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry, originalTransaction: Partial): TransactionMergeParams { return { amount: -getAmount(originalTransaction as OnyxEntry, false), - reportID: originalTransaction?.reportID ?? '', - receiptID: originalTransaction?.receipt?.receiptID ?? 0, + reportID: originalTransaction?.reportID, + receiptID: originalTransaction?.receipt?.receiptID ?? CONST.DEFAULT_NUMBER_ID, currency: getCurrency(originalTransaction as OnyxEntry), created: getFormattedCreated(originalTransaction as OnyxEntry), - transactionID: reviewDuplicates?.transactionID ?? '', + transactionID: reviewDuplicates?.transactionID, transactionIDList: removeSettledAndApprovedTransactions(reviewDuplicates?.duplicates ?? []), billable: reviewDuplicates?.billable ?? false, reimbursable: reviewDuplicates?.reimbursable ?? false, @@ -1239,6 +1249,40 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry, policy: OnyxEntry) { + const taxRules = policy?.rules?.expenseRules?.filter((rule) => rule.tax); + if (!taxRules || taxRules?.length === 0) { + return {categoryTaxCode: undefined, categoryTaxAmount: undefined}; + } + + const categoryTaxCode = getCategoryDefaultTaxRate(taxRules, category, policy?.taxRates?.defaultExternalID); + const categoryTaxPercentage = getTaxValue(policy, transaction, categoryTaxCode ?? ''); + let categoryTaxAmount; + + if (categoryTaxPercentage) { + categoryTaxAmount = convertToBackendAmount(calculateTaxAmount(categoryTaxPercentage, getAmount(transaction), getCurrency(transaction))); + } + + return {categoryTaxCode, categoryTaxAmount}; +} + +/** + * Return the sorted list transactions of an iou report + */ +function getAllSortedTransactions(iouReportID?: string): Array> { + return getAllReportTransactions(iouReportID).sort((transA, transB) => { + if (transA.created < transB.created) { + return -1; + } + + if (transA.created > transB.created) { + return 1; + } + + return (transA.inserted ?? '') < (transB.inserted ?? '') ? -1 : 1; + }); +} + export { buildOptimisticTransaction, calculateTaxAmount, @@ -1322,7 +1366,9 @@ export { getCardName, hasReceiptSource, shouldShowAttendees, + getAllSortedTransactions, getFormattedPostedDate, + getCategoryTaxCodeAndAmount, }; export type {TransactionChanges}; diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index 4d38d410cbfd..1c85781806ac 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -40,7 +40,7 @@ function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, cur * Furthermore, muted reports may or may not appear in the LHN depending on priority mode, * but they should not be considered in the unread indicator count. */ - notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && + !ReportUtils.isHiddenForCurrentUser(notificationPreference) && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ); }); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5f8b83d4b5c2..284abf31a896 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -590,8 +590,19 @@ function setMoneyRequestPendingFields(transactionID: string, pendingFields: Onyx Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields}); } -function setMoneyRequestCategory(transactionID: string, category: string) { +function setMoneyRequestCategory(transactionID: string, category: string, policyID?: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {category}); + if (!policyID) { + setMoneyRequestTaxRate(transactionID, ''); + setMoneyRequestTaxAmount(transactionID, null); + return; + } + const transaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`]; + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, PolicyUtils.getPolicy(policyID)); + if (categoryTaxCode && categoryTaxAmount !== undefined) { + setMoneyRequestTaxRate(transactionID, categoryTaxCode); + setMoneyRequestTaxAmount(transactionID, categoryTaxAmount); + } } function setMoneyRequestTag(transactionID: string, tag: string) { @@ -3432,9 +3443,17 @@ function updateMoneyRequestCategory( policyTagList: OnyxEntry, policyCategories: OnyxEntry, ) { + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, policy); const transactionChanges: TransactionChanges = { category, + ...(categoryTaxCode && + categoryTaxAmount !== undefined && { + taxCode: categoryTaxCode, + taxAmount: categoryTaxAmount, + }), }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories); API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY, params, onyxData); } @@ -5359,17 +5378,26 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA Report.notifyNewAction(chatReportID, sessionAccountID); } -function setDraftSplitTransaction(transactionID: string, transactionChanges: TransactionChanges = {}) { +function setDraftSplitTransaction(transactionID: string, transactionChanges: TransactionChanges = {}, policy?: OnyxEntry) { + const newTransactionChanges = {...transactionChanges}; let draftSplitTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`]; if (!draftSplitTransaction) { draftSplitTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; } + if (transactionChanges.category) { + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(transactionChanges.category, draftSplitTransaction, policy); + if (categoryTaxCode && categoryTaxAmount !== undefined) { + newTransactionChanges.taxCode = categoryTaxCode; + newTransactionChanges.taxAmount = categoryTaxAmount; + } + } + const updatedTransaction = draftSplitTransaction ? TransactionUtils.getUpdatedTransaction({ transaction: draftSplitTransaction, - transactionChanges, + transactionChanges: newTransactionChanges, isFromExpenseReport: false, shouldUpdateReceiptState: false, }) @@ -8577,7 +8605,7 @@ function mergeDuplicates(params: TransactionMergeParams) { }, }; - const iouActionsToDelete = getIOUActionForTransactions(params.transactionIDList, params.reportID); + const iouActionsToDelete = params.reportID ? getIOUActionForTransactions(params.transactionIDList, params.reportID) : []; const deletedTime = DateUtils.getDBTime(); const expenseReportActionsOptimisticData: OnyxUpdate = { @@ -8644,6 +8672,10 @@ function updateLastLocationPermissionPrompt() { /** Instead of merging the duplicates, it updates the transaction we want to keep and puts the others on hold without deleting them */ function resolveDuplicates(params: TransactionMergeParams) { + if (!params.transactionID) { + return; + } + const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`]; const optimisticTransactionData: OnyxUpdate = { @@ -8691,7 +8723,7 @@ function resolveDuplicates(params: TransactionMergeParams) { }; }); - const iouActionList = getIOUActionForTransactions(params.transactionIDList, params.reportID); + const iouActionList = params.reportID ? getIOUActionForTransactions(params.transactionIDList, params.reportID) : []; const transactionThreadReportIDList = iouActionList.map((action) => action?.childReportID); const orderedTransactionIDList = iouActionList.map((action) => { const message = ReportActionsUtils.getOriginalMessage(action); @@ -8743,7 +8775,7 @@ function resolveDuplicates(params: TransactionMergeParams) { }); }); - const transactionThreadReportID = getIOUActionForTransactions([params.transactionID], params.reportID).at(0)?.childReportID; + const transactionThreadReportID = params.reportID ? getIOUActionForTransactions([params.transactionID], params.reportID).at(0)?.childReportID : undefined; const optimisticReportAction = ReportUtils.buildOptimisticDismissedViolationReportAction({ reason: 'manual', violationName: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, @@ -8774,6 +8806,7 @@ function resolveDuplicates(params: TransactionMergeParams) { const parameters: ResolveDuplicatesParams = { ...otherParams, + transactionID: params.transactionID, reportActionIDList, transactionIDList: orderedTransactionIDList, dismissedViolationReportActionID: optimisticReportAction.reportActionID, diff --git a/src/libs/actions/Policy/DistanceRate.ts b/src/libs/actions/Policy/DistanceRate.ts index 786e22ef91d9..2a4cf0cde1a7 100644 --- a/src/libs/actions/Policy/DistanceRate.ts +++ b/src/libs/actions/Policy/DistanceRate.ts @@ -129,34 +129,36 @@ function enablePolicyDistanceRates(policyID: string, enabled: boolean) { if (!enabled) { const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const customUnit = getDistanceRateCustomUnit(policy); - const customUnitID = customUnit?.customUnitID ?? ''; + if (customUnit) { + const customUnitID = customUnit.customUnitID; - const rateEntries = Object.entries(customUnit?.rates ?? {}); - // find the rate to be enabled after disabling the distance rate feature - const rateEntryToBeEnabled = rateEntries.at(0); + const rateEntries = Object.entries(customUnit.rates ?? {}); + // find the rate to be enabled after disabling the distance rate feature + const rateEntryToBeEnabled = rateEntries.at(0); - onyxData.optimisticData?.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - customUnits: { - [customUnitID]: { - rates: Object.fromEntries( - rateEntries.map((rateEntry) => { - const [rateID, rate] = rateEntry; - return [ - rateID, - { - ...rate, - enabled: rateID === rateEntryToBeEnabled?.at(0), - }, - ]; - }), - ), + onyxData.optimisticData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + rates: Object.fromEntries( + rateEntries.map((rateEntry) => { + const [rateID, rate] = rateEntry; + return [ + rateID, + { + ...rate, + enabled: rateID === rateEntryToBeEnabled?.at(0), + }, + ]; + }), + ), + }, }, }, - }, - }); + }); + } } const parameters: EnablePolicyDistanceRatesParams = {policyID, enabled}; @@ -177,7 +179,7 @@ function createPolicyDistanceRate(policyID: string, customUnitID: string, custom customUnits: { [customUnitID]: { rates: { - [customUnitRate.customUnitRateID ?? '']: { + [customUnitRate.customUnitRateID]: { ...customUnitRate, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, @@ -196,7 +198,7 @@ function createPolicyDistanceRate(policyID: string, customUnitID: string, custom customUnits: { [customUnitID]: { rates: { - [customUnitRate.customUnitRateID ?? '']: { + [customUnitRate.customUnitRateID]: { pendingAction: null, }, }, @@ -214,7 +216,7 @@ function createPolicyDistanceRate(policyID: string, customUnitID: string, custom customUnits: { [customUnitID]: { rates: { - [customUnitRate.customUnitRateID ?? '']: { + [customUnitRate.customUnitRateID]: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }, }, @@ -339,9 +341,9 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) { const currentRates = customUnit.rates; - const optimisticRates: Record = {}; - const successRates: Record = {}; - const failureRates: Record = {}; + const optimisticRates: Record> = {}; + const successRates: Record> = {}; + const failureRates: Record> = {}; const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID); for (const rateID of Object.keys(customUnit.rates)) { @@ -410,9 +412,9 @@ function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) { const currentRates = customUnit.rates; - const optimisticRates: Record = {}; - const successRates: Record = {}; - const failureRates: Record = {}; + const optimisticRates: Record> = {}; + const successRates: Record> = {}; + const failureRates: Record> = {}; const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID); for (const rateID of Object.keys(currentRates)) { @@ -559,9 +561,9 @@ function deletePolicyDistanceRates(policyID: string, customUnit: CustomUnit, rat function updateDistanceTaxClaimableValue(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) { const currentRates = customUnit.rates; - const optimisticRates: Record = {}; - const successRates: Record = {}; - const failureRates: Record = {}; + const optimisticRates: Record> = {}; + const successRates: Record> = {}; + const failureRates: Record> = {}; const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID); for (const rateID of Object.keys(customUnit.rates)) { @@ -630,9 +632,9 @@ function updateDistanceTaxClaimableValue(policyID: string, customUnit: CustomUni function updateDistanceTaxRate(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) { const currentRates = customUnit.rates; - const optimisticRates: Record = {}; - const successRates: Record = {}; - const failureRates: Record = {}; + const optimisticRates: Record> = {}; + const successRates: Record> = {}; + const failureRates: Record> = {}; const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID); for (const rateID of Object.keys(customUnit.rates)) { diff --git a/src/libs/actions/Policy/PerDiem.ts b/src/libs/actions/Policy/PerDiem.ts index 81898dfb34e0..323045e49821 100644 --- a/src/libs/actions/Policy/PerDiem.ts +++ b/src/libs/actions/Policy/PerDiem.ts @@ -1,3 +1,4 @@ +import lodashDeepClone from 'lodash/cloneDeep'; import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; @@ -13,9 +14,10 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report} from '@src/types/onyx'; -import type {ErrorFields} from '@src/types/onyx/OnyxCommon'; -import type {Rate} from '@src/types/onyx/Policy'; +import type {ErrorFields, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {CustomUnit, Rate} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; const allPolicies: OnyxCollection = {}; Onyx.connect({ @@ -50,6 +52,16 @@ Onyx.connect({ }, }); +type SubRateData = { + pendingAction?: PendingAction; + destination: string; + subRateName: string; + rate: number; + currency: string; + rateID: string; + subRateID: string; +}; + /** * Returns a client generated 13 character hexadecimal value for a custom unit ID */ @@ -193,4 +205,208 @@ function clearPolicyPerDiemRatesErrorFields(policyID: string, customUnitID: stri }); } -export {generateCustomUnitID, enablePerDiem, openPolicyPerDiemPage, importPerDiemRates, downloadPerDiemCSV, clearPolicyPerDiemRatesErrorFields}; +type DeletePerDiemCustomUnitOnyxType = Omit & { + rates: Record | null>; +}; + +function prepareNewCustomUnit(customUnit: CustomUnit, subRatesToBeDeleted: SubRateData[]) { + const mappedDeletedSubRatesToRate = subRatesToBeDeleted.reduce((acc, subRate) => { + if (subRate.rateID in acc) { + acc[subRate.rateID].push(subRate); + } else { + acc[subRate.rateID] = [subRate]; + } + return acc; + }, {} as Record); + + // Copy the custom unit and remove the sub rates that are to be deleted + const newCustomUnit: CustomUnit = lodashDeepClone(customUnit); + const customUnitOnyxUpdate: DeletePerDiemCustomUnitOnyxType = lodashDeepClone(customUnit); + for (const rateID in mappedDeletedSubRatesToRate) { + if (!(rateID in newCustomUnit.rates)) { + // eslint-disable-next-line no-continue + continue; + } + const subRates = mappedDeletedSubRatesToRate[rateID]; + if (subRates.length === newCustomUnit.rates[rateID].subRates?.length) { + delete newCustomUnit.rates[rateID]; + customUnitOnyxUpdate.rates[rateID] = null; + } else { + const newSubRates = newCustomUnit.rates[rateID].subRates?.filter((subRate) => !subRates.some((subRateToBeDeleted) => subRateToBeDeleted.subRateID === subRate.id)); + newCustomUnit.rates[rateID].subRates = newSubRates; + customUnitOnyxUpdate.rates[rateID] = {...customUnitOnyxUpdate.rates[rateID], subRates: newSubRates}; + } + } + return {newCustomUnit, customUnitOnyxUpdate}; +} + +function deleteWorkspacePerDiemRates(policyID: string, customUnit: CustomUnit | undefined, subRatesToBeDeleted: SubRateData[]) { + if (!policyID || isEmptyObject(customUnit) || !subRatesToBeDeleted.length) { + return; + } + const {newCustomUnit, customUnitOnyxUpdate} = prepareNewCustomUnit(customUnit, subRatesToBeDeleted); + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: customUnitOnyxUpdate, + }, + }, + }, + ], + }; + + const parameters = { + policyID, + customUnit: JSON.stringify(newCustomUnit), + }; + + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData); +} + +function editPerDiemRateDestination(policyID: string, rateID: string, customUnit: CustomUnit | undefined, newDestination: string) { + if (!policyID || !rateID || isEmptyObject(customUnit) || !newDestination) { + return; + } + + const newCustomUnit: CustomUnit = lodashDeepClone(customUnit); + newCustomUnit.rates[rateID].name = newDestination; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: newCustomUnit, + }, + }, + }, + ], + }; + + const parameters = { + policyID, + customUnit: JSON.stringify(newCustomUnit), + }; + + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData); +} + +function editPerDiemRateSubrate(policyID: string, rateID: string, subRateID: string, customUnit: CustomUnit | undefined, newSubrate: string) { + if (!policyID || !rateID || isEmptyObject(customUnit) || !newSubrate) { + return; + } + + const newCustomUnit: CustomUnit = lodashDeepClone(customUnit); + newCustomUnit.rates[rateID].subRates = newCustomUnit.rates[rateID].subRates?.map((subRate) => { + if (subRate.id === subRateID) { + return {...subRate, name: newSubrate}; + } + return subRate; + }); + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: newCustomUnit, + }, + }, + }, + ], + }; + + const parameters = { + policyID, + customUnit: JSON.stringify(newCustomUnit), + }; + + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData); +} + +function editPerDiemRateAmount(policyID: string, rateID: string, subRateID: string, customUnit: CustomUnit | undefined, newAmount: number) { + if (!policyID || !rateID || isEmptyObject(customUnit) || !newAmount) { + return; + } + + const newCustomUnit: CustomUnit = lodashDeepClone(customUnit); + newCustomUnit.rates[rateID].subRates = newCustomUnit.rates[rateID].subRates?.map((subRate) => { + if (subRate.id === subRateID) { + return {...subRate, rate: newAmount}; + } + return subRate; + }); + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: newCustomUnit, + }, + }, + }, + ], + }; + + const parameters = { + policyID, + customUnit: JSON.stringify(newCustomUnit), + }; + + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData); +} + +function editPerDiemRateCurrency(policyID: string, rateID: string, customUnit: CustomUnit | undefined, newCurrency: string) { + if (!policyID || !rateID || isEmptyObject(customUnit) || !newCurrency) { + return; + } + + const newCustomUnit: CustomUnit = lodashDeepClone(customUnit); + newCustomUnit.rates[rateID].currency = newCurrency; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: newCustomUnit, + }, + }, + }, + ], + }; + + const parameters = { + policyID, + customUnit: JSON.stringify(newCustomUnit), + }; + + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData); +} + +export { + generateCustomUnitID, + enablePerDiem, + openPolicyPerDiemPage, + importPerDiemRates, + downloadPerDiemCSV, + clearPolicyPerDiemRatesErrorFields, + deleteWorkspacePerDiemRates, + editPerDiemRateDestination, + editPerDiemRateSubrate, + editPerDiemRateAmount, + editPerDiemRateCurrency, +}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7baf66adc5c5..9fe8269bee90 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -545,9 +545,8 @@ function addActions(reportID: string, text = '', file?: FileObject) { }; const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const shouldUpdateNotificationPrefernece = !isEmptyObject(report) && ReportUtils.getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - - if (shouldUpdateNotificationPrefernece) { + const shouldUpdateNotificationPreference = !isEmptyObject(report) && ReportUtils.isHiddenForCurrentUser(report); + if (shouldUpdateNotificationPreference) { optimisticReport.participants = { [currentUserAccountID]: {notificationPreference: ReportUtils.getDefaultNotificationPreferenceForReport(report)}, }; @@ -1932,7 +1931,7 @@ function toggleSubscribeToChildReport(childReportID = '-1', parentReportAction: if (childReportID !== '-1') { openReport(childReportID); const parentReportActionID = parentReportAction?.reportActionID ?? '-1'; - if (!prevNotificationPreference || prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + if (!prevNotificationPreference || ReportUtils.isHiddenForCurrentUser(prevNotificationPreference)) { updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, parentReportID, parentReportActionID); } else { updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, parentReportID, parentReportActionID); @@ -1957,8 +1956,9 @@ function toggleSubscribeToChildReport(childReportID = '-1', parentReportAction: const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID); - const notificationPreference = - prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const notificationPreference = ReportUtils.isHiddenForCurrentUser(prevNotificationPreference) + ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS + : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; updateNotificationPreference(newChat.reportID, prevNotificationPreference, notificationPreference, parentReportID, parentReportAction?.reportActionID); } } @@ -3062,7 +3062,12 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, - value: {[report.parentReportActionID]: {childReportNotificationPreference: ReportUtils.getReportNotificationPreference(report, false)}}, + value: { + [report.parentReportActionID]: { + childReportNotificationPreference: + report?.participants?.[currentUserAccountID ?? -1]?.notificationPreference ?? ReportUtils.getDefaultNotificationPreferenceForReport(report), + }, + }, }); } diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 8685a0363e31..1dbb01b008dd 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -17,7 +17,6 @@ import type { RequestAccountValidationLinkParams, RequestNewValidateCodeParams, RequestUnlinkValidationLinkParams, - ResetSMSDeliveryFailureParams, SignInUserWithLinkParams, SignUpUserParams, UnlinkLoginParams, @@ -1201,52 +1200,6 @@ function isUserOnPrivateDomain() { return false; } -/** - * To reset SMS delivery failure - */ -function resetSMSDeliveryFailure(login: string) { - const params: ResetSMSDeliveryFailureParams = {login}; - - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - errors: null, - smsDeliveryFailureStatus: { - isLoading: true, - }, - }, - }, - ]; - const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - smsDeliveryFailureStatus: { - isLoading: false, - isReset: true, - }, - }, - }, - ]; - const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - smsDeliveryFailureStatus: { - isLoading: false, - }, - }, - }, - ]; - - API.write(WRITE_COMMANDS.RESET_SMS_DELIVERY_FAILURE, params, {optimisticData, successData, failureData}); -} - export { beginSignIn, beginAppleSignIn, @@ -1286,5 +1239,4 @@ export { signInAfterTransitionFromOldDot, validateUserAndGetAccessiblePolicies, isUserOnPrivateDomain, - resetSMSDeliveryFailure, }; diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index df8c0474fbaa..bd68d15caadc 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -252,7 +252,7 @@ function createTaskAndNavigate( }, ); - const shouldUpdateNotificationPreference = !isEmptyObject(parentReport) && ReportUtils.getReportNotificationPreference(parentReport) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const shouldUpdateNotificationPreference = !isEmptyObject(parentReport) && ReportUtils.isHiddenForCurrentUser(parentReport); if (shouldUpdateNotificationPreference) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 5d610e93bee7..c743e18b23fb 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -290,7 +290,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { .sort((a, b) => a.localeCompare(b)) .at(0); const distanceRateCustomUnit = PolicyUtils.getDistanceRateCustomUnit(policy); - const customUnitID = distanceRateCustomUnit?.customUnitID ?? '-1'; + const customUnitID = distanceRateCustomUnit?.customUnitID; const ratesToUpdate = Object.values(distanceRateCustomUnit?.rates ?? {}).filter( (rate) => !!rate.attributes?.taxRateExternalID && taxesToDelete.includes(rate.attributes?.taxRateExternalID), ); @@ -303,11 +303,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const isForeignTaxRemoved = foreignTaxDefault && taxesToDelete.includes(foreignTaxDefault); const optimisticRates: Record> = {}; - const successRates: Record = {}; - const failureRates: Record = {}; + const successRates: Record> = {}; + const failureRates: Record> = {}; ratesToUpdate.forEach((rate) => { - const rateID = rate.customUnitRateID ?? ''; + const rateID = rate.customUnitRateID; optimisticRates[rateID] = { attributes: { taxRateExternalID: null, @@ -343,11 +343,12 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, - customUnits: distanceRateCustomUnit && { - [customUnitID]: { - rates: optimisticRates, + customUnits: distanceRateCustomUnit && + customUnitID && { + [customUnitID]: { + rates: optimisticRates, + }, }, - }, }, }, ], @@ -363,11 +364,12 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, - customUnits: distanceRateCustomUnit && { - [customUnitID]: { - rates: successRates, + customUnits: distanceRateCustomUnit && + customUnitID && { + [customUnitID]: { + rates: successRates, + }, }, - }, }, }, ], @@ -387,11 +389,12 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { return acc; }, {}), }, - customUnits: distanceRateCustomUnit && { - [customUnitID]: { - rates: failureRates, + customUnits: distanceRateCustomUnit && + customUnitID && { + [customUnitID]: { + rates: failureRates, + }, }, - }, }, }, ], @@ -552,7 +555,7 @@ function setPolicyTaxCode(policyID: string, oldTaxCode: string, newTaxCode: stri }; } return rates; - }, {} as Record), + }, {} as Record>), }, }; const oldDefaultExternalID = policy?.taxRates?.defaultExternalID; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 8f8c416ceeb3..fd9d5f1820e6 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -32,6 +32,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as Pusher from '@libs/Pusher/pusher'; import PusherUtils from '@libs/PusherUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile'; import Visibility from '@libs/Visibility'; @@ -795,9 +796,7 @@ const isChannelMuted = (reportId: string) => Onyx.disconnect(connection); const notificationPreference = report?.participants?.[currentUserAccountID]?.notificationPreference; - resolve( - !notificationPreference || notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE || notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, - ); + resolve(!notificationPreference || notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE || ReportUtils.isHiddenForCurrentUser(notificationPreference)); }, }); }); diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index a0370ef6cbbd..4d084cfa924d 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -12,7 +12,6 @@ import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import useHtmlPaste from '@hooks/useHtmlPaste'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -66,8 +65,6 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr const privateNotesInput = useRef(null); const focusTimeoutRef = useRef(null); - useHtmlPaste(privateNotesInput); - useFocusEffect( useCallback(() => { focusTimeoutRef.current = setTimeout(() => { diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index f0308301e142..4fa2fe25797b 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -158,7 +158,7 @@ function ProfilePage({route}: ProfilePageProps) { const notificationPreferenceValue = ReportUtils.getReportNotificationPreference(report); - const shouldShowNotificationPreference = !isEmptyObject(report) && !isCurrentUser && notificationPreferenceValue !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const shouldShowNotificationPreference = !isEmptyObject(report) && !isCurrentUser && !ReportUtils.isHiddenForCurrentUser(notificationPreferenceValue); const notificationPreference = shouldShowNotificationPreference ? translate(`notificationPreferencesPage.notificationPreferences.${notificationPreferenceValue}` as TranslationPaths) : ''; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 9d2d7e4ada75..dc751bae7bff 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -288,7 +288,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta roomDescription = translate('newRoomPage.roomName'); } - const shouldShowNotificationPref = !isMoneyRequestReport && ReportUtils.getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const shouldShowNotificationPref = !isMoneyRequestReport && !ReportUtils.isHiddenForCurrentUser(report); const shouldShowWriteCapability = !isMoneyRequestReport; const shouldShowMenuItem = shouldShowNotificationPref || shouldShowWriteCapability || (!!report?.visibility && report.chatType !== CONST.REPORT.CHAT_TYPE.INVOICE); diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 79d7cfe4acc5..4bcf12623d04 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -63,7 +63,7 @@ function RoomInvitePage({ // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers = useMemo(() => { const visibleParticipantAccountIDs = Object.entries(report.participants ?? {}) - .filter(([, participant]) => participant && participant.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) + .filter(([, participant]) => participant && !ReportUtils.isHiddenForCurrentUser(participant.notificationPreference)) .map(([accountID]) => Number(accountID)); return [...PersonalDetailsUtils.getLoginsByAccountIDs(visibleParticipantAccountIDs), ...CONST.EXPENSIFY_EMAILS].map((participant) => PhoneNumber.addSMSDomainIfPhoneNumber(participant), diff --git a/src/pages/TransactionDuplicate/Confirmation.tsx b/src/pages/TransactionDuplicate/Confirmation.tsx index 520a253469db..8d973a262186 100644 --- a/src/pages/TransactionDuplicate/Confirmation.tsx +++ b/src/pages/TransactionDuplicate/Confirmation.tsx @@ -39,8 +39,8 @@ function Confirmation() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [reviewDuplicates, reviewDuplicatesResult] = useOnyx(ONYXKEYS.REVIEW_DUPLICATES); const transaction = useMemo(() => TransactionUtils.buildNewTransactionAfterReviewingDuplicates(reviewDuplicates), [reviewDuplicates]); - const transactionID = TransactionUtils.getTransactionID(route.params.threadReportID ?? ''); - const compareResult = TransactionUtils.compareDuplicateTransactionFields(transactionID, reviewDuplicates?.reportID ?? '-1'); + const transactionID = TransactionUtils.getTransactionID(route.params.threadReportID); + const compareResult = TransactionUtils.compareDuplicateTransactionFields(transactionID, reviewDuplicates?.reportID); const {goBack} = useReviewDuplicatesNavigation(Object.keys(compareResult.change ?? {}), 'confirmation', route.params.threadReportID, route.params.backTo); const [report, reportResult] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`); @@ -54,12 +54,15 @@ function Confirmation() { const mergeDuplicates = useCallback(() => { IOU.mergeDuplicates(transactionsMergeParams); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportAction?.childReportID ?? '-1')); + if (!reportAction?.childReportID) { + return; + } + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportAction?.childReportID)); }, [reportAction?.childReportID, transactionsMergeParams]); const resolveDuplicates = useCallback(() => { IOU.resolveDuplicates(transactionsMergeParams); - Navigation.dismissModal(reportAction?.childReportID ?? '-1'); + Navigation.dismissModal(reportAction?.childReportID); }, [transactionsMergeParams, reportAction?.childReportID]); const contextValue = useMemo( @@ -75,8 +78,8 @@ function Confirmation() { [report, reportAction], ); - const reportTransactionID = TransactionUtils.getTransactionID(report?.reportID ?? ''); - const doesTransactionBelongToReport = reviewDuplicates?.transactionID === reportTransactionID || reviewDuplicates?.duplicates.includes(reportTransactionID); + const reportTransactionID = report?.reportID ? TransactionUtils.getTransactionID(report.reportID) : undefined; + const doesTransactionBelongToReport = reviewDuplicates?.transactionID === reportTransactionID || (reportTransactionID && reviewDuplicates?.duplicates.includes(reportTransactionID)); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = diff --git a/src/pages/WorkspaceSwitcherPage/index.tsx b/src/pages/WorkspaceSwitcherPage/index.tsx index 87ba17b6504d..cb52c52cb64c 100644 --- a/src/pages/WorkspaceSwitcherPage/index.tsx +++ b/src/pages/WorkspaceSwitcherPage/index.tsx @@ -20,6 +20,7 @@ import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import switchPolicyAfterInteractions from './switchPolicyAfterInteractions'; import WorkspaceCardCreateAWorkspace from './WorkspaceCardCreateAWorkspace'; type WorkspaceListItem = { @@ -87,7 +88,9 @@ function WorkspaceSwitcherPage() { setActiveWorkspaceID(newPolicyID); Navigation.goBack(); if (newPolicyID !== activeWorkspaceID) { - Navigation.navigateWithSwitchPolicyID({policyID: newPolicyID}); + // On native platforms, we will see a blank screen if we navigate to a new HomeScreen route while navigating back at the same time. + // Therefore we delay switching the workspace until after back navigation, using the InteractionManager. + switchPolicyAfterInteractions(newPolicyID); } }, [activeWorkspaceID, setActiveWorkspaceID, isFocused], @@ -102,7 +105,7 @@ function WorkspaceSwitcherPage() { .filter((policy) => PolicyUtils.shouldShowPolicy(policy, !!isOffline, currentUserLogin) && !policy?.isJoinRequestPending) .map((policy) => ({ text: policy?.name ?? '', - policyID: policy?.id ?? '-1', + policyID: policy?.id, brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id), icons: [ { diff --git a/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.native.tsx b/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.native.tsx new file mode 100644 index 000000000000..a3df127564b1 --- /dev/null +++ b/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.native.tsx @@ -0,0 +1,10 @@ +import {InteractionManager} from 'react-native'; +import Navigation from '@libs/Navigation/Navigation'; + +function switchPolicyAfterInteractions(newPolicyID: string | undefined) { + InteractionManager.runAfterInteractions(() => { + Navigation.navigateWithSwitchPolicyID({policyID: newPolicyID}); + }); +} + +export default switchPolicyAfterInteractions; diff --git a/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.tsx b/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.tsx new file mode 100644 index 000000000000..612759a8601c --- /dev/null +++ b/src/pages/WorkspaceSwitcherPage/switchPolicyAfterInteractions/index.tsx @@ -0,0 +1,7 @@ +import Navigation from '@libs/Navigation/Navigation'; + +function switchPolicyAfterInteractions(newPolicyID: string | undefined) { + Navigation.navigateWithSwitchPolicyID({policyID: newPolicyID}); +} + +export default switchPolicyAfterInteractions; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 36d4a30f0a0d..6b1b66aa6138 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -112,6 +112,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro const {isOffline} = useNetwork(); const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); const {activeWorkspaceID} = useActiveWorkspace(); + const lastAccessedReportIDRef = useRef(false); const [modal] = useOnyx(ONYXKEYS.MODAL); const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`, {initialValue: false}); @@ -151,6 +152,10 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro return; } + if (lastAccessedReportIDRef.current) { + return; + } + const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID; // It's possible that reports aren't fully loaded yet @@ -160,6 +165,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro } Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`); + lastAccessedReportIDRef.current = true; navigation.setParams({reportID: lastAccessedReportID}); }, [activeWorkspaceID, canUseDefaultRooms, navigation, route, finishedLoadingApp]); @@ -558,14 +564,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro // If a user has chosen to leave a thread, and then returns to it (e.g. with the back button), we need to call `openReport` again in order to allow the user to rejoin and to receive real-time updates useEffect(() => { - if ( - !shouldUseNarrowLayout || - !isFocused || - prevIsFocused || - !ReportUtils.isChatThread(report) || - ReportUtils.getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN || - isSingleTransactionView - ) { + if (!shouldUseNarrowLayout || !isFocused || prevIsFocused || !ReportUtils.isChatThread(report) || !ReportUtils.isHiddenForCurrentUser(report) || isSingleTransactionView) { return; } Report.openReport(reportID ?? ''); diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index 97eb942ba7c4..647c17f70d88 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -132,12 +132,14 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)); }; + const shouldShowAddBankAccountButton = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && ReportUtils.hasMissingInvoiceBankAccount(reportID) && !ReportUtils.isSettled(reportID); + return ( {!isHidden ? ( <> {renderReportActionItemFragments(isApprovedOrSubmittedReportAction)} - {action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && ReportUtils.hasMissingInvoiceBankAccount(reportID) && ( + {shouldShowAddBankAccountButton && (