diff --git a/.eslintrc.js b/.eslintrc.js
index c2198da60c52..56a5236a7899 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -94,7 +94,6 @@ module.exports = {
rules: {
'prefer-regex-literals': 'off',
'rulesdir/no-multiple-onyx-in-file': 'off',
- 'rulesdir/onyx-props-must-have-default': 'off',
'react-native-a11y/has-accessibility-hint': ['off'],
'react/jsx-no-constructed-context-values': 'error',
'react-native-a11y/has-valid-accessibility-descriptors': [
diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh
new file mode 100644
index 000000000000..737d9bffacf9
--- /dev/null
+++ b/.github/scripts/verifyRedirect.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# HelpDot - Verifies that redirects.csv does not have any duplicates
+# Duplicate sourceURLs break redirection on cloudflare pages
+
+declare -r REDIRECTS_FILE="docs/redirects.csv"
+
+duplicates=$(awk -F, 'a[$1]++{print $1}' $REDIRECTS_FILE)
+
+if [[ -z "$duplicates" ]]; then
+ exit 0
+fi
+
+echo "duplicate redirects are not allowed: $duplicates"
+exit 1
diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml
index d4577e112d59..699bd379fb77 100644
--- a/.github/workflows/deployExpensifyHelp.yml
+++ b/.github/workflows/deployExpensifyHelp.yml
@@ -36,6 +36,9 @@ jobs:
- name: Create docs routes file
run: ./.github/scripts/createDocsRoutes.sh
+
+ - name: Check duplicates in redirect.csv
+ run: ./.github/scripts/verifyRedirect.sh
- name: Build with Jekyll
uses: actions/jekyll-build-pages@0143c158f4fa0c5dcd99499a5d00859d79f70b0e
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index d97ea2b86269..bb850e6eda10 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -87,11 +87,12 @@ jobs:
MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }}
MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }}
- - name: Run Fastlane production
- if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- run: bundle exec fastlane android production
- env:
- VERSION: ${{ env.VERSION_CODE }}
+ # Note: Android production deploys are temporarily disabled until https://github.com/Expensify/App/issues/40108 is resolved
+ # - name: Run Fastlane production
+ # if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
+ # run: bundle exec fastlane android production
+ # env:
+ # VERSION: ${{ env.VERSION_CODE }}
- name: Archive Android sourcemaps
uses: actions/upload-artifact@v3
@@ -158,7 +159,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }}
-
+
- name: Build staging desktop app
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml
index 156b9764bcca..f20939f9df0a 100644
--- a/.github/workflows/typecheck.yml
+++ b/.github/workflows/typecheck.yml
@@ -30,7 +30,7 @@ jobs:
# - git diff is used to see the files that were added on this branch
# - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main
# - wc counts the words in the result of the intersection
- count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l)
+ count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'web/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l)
if [ "$count_new_js" -gt "0" ]; then
echo "ERROR: Found new JavaScript files in the project; use TypeScript instead."
exit 1
diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association
index b3adf0f59b9c..a2c7365f7de8 100644
--- a/.well-known/apple-app-site-association
+++ b/.well-known/apple-app-site-association
@@ -32,10 +32,6 @@
"/": "/iou/*",
"comment": "I Owe You reports"
},
- {
- "/": "/request/*",
- "comment": "Money request"
- },
{
"/": "/enable-payments/*",
"comment": "Payments setup"
@@ -54,11 +50,11 @@
},
{
"/": "/split/*",
- "comment": "Split Bill"
+ "comment": "Split Expense"
},
{
"/": "/request/*",
- "comment": "Request Money"
+ "comment": "Submit Expense"
},
{
"/": "/new/*",
@@ -82,7 +78,7 @@
},
{
"/": "/send/*",
- "comment": "Send money"
+ "comment": "Pay someone"
},
{
"/": "/money2020/*",
diff --git a/README.md b/README.md
index 026a63606db0..8adadfc9cbe6 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,16 @@ If you want to run the app on an actual physical iOS device, please follow the i
## Running the MacOS desktop app 🖥
* To run the **Development app**, run: `npm run desktop`, this will start a new Electron process running on your MacOS desktop in the `dist/Mac` folder.
+## Receiving Notifications
+To receive notifications on development build of the app while hitting the Staging or Production API, you need to use the production airship config.
+### Android
+1. Copy the [production config](https://github.com/Expensify/App/blob/d7c1256f952c0020344d809ee7299b49a4c70db2/android/app/src/main/assets/airshipconfig.properties#L1-L7) to the [development config](https://github.com/Expensify/App/blob/d7c1256f952c0020344d809ee7299b49a4c70db2/android/app/src/development/assets/airshipconfig.properties#L1-L8).
+2. Rebuild the app.
+
+### iOS
+1. Replace the [development key and secret](https://github.com/Expensify/App/blob/d7c1256f952c0020344d809ee7299b49a4c70db2/ios/AirshipConfig.plist#L7-L10) with the [production values](https://github.com/Expensify/App/blob/d7c1256f952c0020344d809ee7299b49a4c70db2/ios/AirshipConfig.plist#L11-L14).
+2. Rebuild the app.
+
## Troubleshooting
1. If you are having issues with **_Getting Started_**, please reference [React Native's Documentation](https://reactnative.dev/docs/environment-setup)
2. If you are running into CORS errors like (in the browser dev console)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index da340184e7c8..0db4b032ec9d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001046210
- versionName "1.4.62-10"
+ versionCode 1001046300
+ versionName "1.4.63-0"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Zenefits.md b/docs/articles/expensify-classic/integrations/HR-integrations/Zenefits.md
index e94d915e4dfa..07421553aeb2 100644
--- a/docs/articles/expensify-classic/integrations/HR-integrations/Zenefits.md
+++ b/docs/articles/expensify-classic/integrations/HR-integrations/Zenefits.md
@@ -17,7 +17,7 @@ Expensify's direct integration with Zenefits will automatically:
- Every employee record in Zenefits must have a work email address since we use this as the unique identifier in Expensify.
- Zenefits will add all your employees to one Expensify workspace. If your company uses multiple Expensify workspaces, you'll be given the option to choose which workspace to connect to when you're setting up the integration.
-## To connect your Expensify workspace to Gusto:
+## To connect your Expensify workspace to Zenefits:
1. Navigate to **Settings > Workspaces > Group > _[Workspace Name]_ > Connections**
2. Scroll down to HR Integrations, click the **Connect to Zenefits** radio button, then click **Sync with Zenefits**
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 51c8c7515e10..af595ecc5f83 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -152,7 +152,6 @@ https://help.expensify.com/articles/expensify-classic/manage-employees-and-repor
https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members
https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members
https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/
-https://help.expensify.com/articles/expensify-classic/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency
https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing-Reports,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports
https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO,https://help.expensify.com/articles/expensify-classic/settings/Enable-two-factor-authentication
https://help.expensify.com/articles/expensify-classic/workspaces/Budgets,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 4dea8203b477..ddcc64604581 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.62
+ 1.4.63
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.62.10
+ 1.4.63.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 0d1e81ade440..a57ffb9500c5 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.62
+ 1.4.63
CFBundleSignature
????
CFBundleVersion
- 1.4.62.10
+ 1.4.63.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index a2dfb017df48..3c8e8c1fb63f 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 1.4.62
+ 1.4.63
CFBundleVersion
- 1.4.62.10
+ 1.4.63.0
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 1ebfc6bb1b62..f564bfd931e4 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1835,7 +1835,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.47):
+ - RNLiveMarkdown (0.1.62):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1853,9 +1853,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNLiveMarkdown/common (= 0.1.47)
+ - RNLiveMarkdown/common (= 0.1.62)
- Yoga
- - RNLiveMarkdown/common (0.1.47):
+ - RNLiveMarkdown/common (0.1.62):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2564,7 +2564,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: f172c7199283dc9d21bccf7e21ea10741fd19e1d
+ RNLiveMarkdown: 47dfb50244f9ba1caefbc0efc6404ba41bf6620a
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d
RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729
@@ -2581,7 +2581,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf
- Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70
+ Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312
PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d
diff --git a/package-lock.json b/package-lock.json
index 203e062de680..920fefc8242b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "new.expensify",
- "version": "1.4.62-10",
+ "version": "1.4.63-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.62-10",
+ "version": "1.4.63-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.47",
+ "@expensify/react-native-live-markdown": "0.1.62",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -57,7 +57,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -207,7 +207,7 @@
"electron-builder": "24.13.2",
"eslint": "^7.6.0",
"eslint-config-airbnb-typescript": "^17.1.0",
- "eslint-config-expensify": "^2.0.44",
+ "eslint-config-expensify": "^2.0.47",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^24.1.0",
@@ -3570,9 +3570,9 @@
}
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.47",
- "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.47.tgz",
- "integrity": "sha512-zUfwgg6qq47MnGuynamDpdHSlBYwVKFV4Zc/2wlVzFcBndQOjOyFu04Ns8YDB4Gl80LyGvfAuBT/sU+kvmMU6g==",
+ "version": "0.1.62",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.62.tgz",
+ "integrity": "sha512-o70/tFIGZJ1U8U8aqTQu1HAZed6nt5LYWk74mrceRxQHOqsKhZgn2q5EuEy8EMIcnCGKjwxuDyZJbuRexgHx/A==",
"engines": {
"node": ">= 18.0.0"
},
@@ -16462,10 +16462,8 @@
},
"node_modules/classnames": {
"version": "2.5.0",
- "license": "MIT",
- "workspaces": [
- "benchmarks"
- ]
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz",
+ "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA=="
},
"node_modules/clean-css": {
"version": "5.3.2",
@@ -16551,7 +16549,8 @@
},
"node_modules/clipboard": {
"version": "2.0.11",
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+ "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
@@ -18058,7 +18057,8 @@
},
"node_modules/delegate": {
"version": "3.2.0",
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
+ "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"node_modules/delegates": {
"version": "1.0.0",
@@ -19250,9 +19250,10 @@
}
},
"node_modules/eslint-config-expensify": {
- "version": "2.0.44",
+ "version": "2.0.48",
+ "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.48.tgz",
+ "integrity": "sha512-PFegJ9Wfsiu5tgevhjA1toCxsZ8Etfk6pIjtXAnwpmVj7q4CtB3QDRusJoUDyJ3HrZr8AsFKViz7CU/CBTfwOw==",
"dev": true,
- "license": "ISC",
"dependencies": {
"@lwc/eslint-plugin-lwc": "^1.7.2",
"babel-eslint": "^10.1.0",
@@ -20212,8 +20213,8 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e",
- "integrity": "sha512-/NAZoAXqeqFWHvC61dueqq9VjRugF69urUtDdDhsfvu1sQE2PCnBoM7a+ACoAEWRYrnP82cyHHhdSA8e7fPuAg==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219",
+ "integrity": "sha512-zz0/y0apISP1orxXEQOgn+Uod45O4wVypwwtaqcDPV4dH1tC3i4L98NoLSZvLn7Y17EcceSkfN6QCEsscgFTDQ==",
"license": "MIT",
"dependencies": {
"classnames": "2.5.0",
@@ -20266,6 +20267,8 @@
},
"node_modules/expensify-common/node_modules/ua-parser-js": {
"version": "1.0.37",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
+ "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
"funding": [
{
"type": "opencollective",
@@ -20280,7 +20283,6 @@
"url": "https://github.com/sponsors/faisalman"
}
],
- "license": "MIT",
"engines": {
"node": "*"
}
@@ -21728,7 +21730,8 @@
},
"node_modules/good-listener": {
"version": "1.2.2",
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
+ "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"dependencies": {
"delegate": "^3.1.2"
}
@@ -22779,7 +22782,8 @@
},
"node_modules/immediate": {
"version": "3.0.6",
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
@@ -26838,7 +26842,8 @@
},
"node_modules/lie": {
"version": "3.1.1",
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+ "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"dependencies": {
"immediate": "~3.0.5"
}
@@ -26981,7 +26986,8 @@
},
"node_modules/localforage": {
"version": "1.10.0",
- "license": "Apache-2.0",
+ "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
+ "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"dependencies": {
"lie": "3.1.1"
}
@@ -33311,7 +33317,8 @@
},
"node_modules/select": {
"version": "1.1.2",
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
+ "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
},
"node_modules/select-hose": {
"version": "2.0.0",
diff --git a/package.json b/package.json
index 43a3ed8cae6a..20d066eabebe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.62-10",
+ "version": "1.4.63-0",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -64,7 +64,7 @@
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.47",
+ "@expensify/react-native-live-markdown": "0.1.62",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -108,7 +108,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -258,7 +258,7 @@
"electron-builder": "24.13.2",
"eslint": "^7.6.0",
"eslint-config-airbnb-typescript": "^17.1.0",
- "eslint-config-expensify": "^2.0.44",
+ "eslint-config-expensify": "^2.0.47",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^24.1.0",
diff --git a/src/CONFIG.ts b/src/CONFIG.ts
index 76ea18d37d5f..9ed4242d7604 100644
--- a/src/CONFIG.ts
+++ b/src/CONFIG.ts
@@ -48,6 +48,7 @@ export default {
EXPENSIFY: {
// Note: This will be EXACTLY what is set for EXPENSIFY_URL whether the proxy is enabled or not.
EXPENSIFY_URL: expensifyURL,
+ SECURE_EXPENSIFY_URL: secureExpensifyUrl,
NEW_EXPENSIFY_URL: newExpensifyURL,
// The DEFAULT API is the API used by most environments, except staging, where we use STAGING (defined below)
@@ -72,7 +73,7 @@ export default {
IS_USING_LOCAL_WEB: useNgrok || expensifyURLRoot.includes('dev'),
PUSHER: {
APP_KEY: get(Config, 'PUSHER_APP_KEY', '268df511a204fbb60884'),
- SUFFIX: get(Config, 'PUSHER_DEV_SUFFIX', ''),
+ SUFFIX: ENVIRONMENT === CONST.ENVIRONMENT.DEV ? get(Config, 'PUSHER_DEV_SUFFIX', '') : '',
CLUSTER: 'mt1',
},
SITE_TITLE: 'New Expensify',
diff --git a/src/CONST.ts b/src/CONST.ts
index f06578ef925d..1f16828783ab 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -638,9 +638,10 @@ const CONST = {
LIMIT: 50,
// OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts
TYPE: {
+ ACTIONABLEJOINREQUEST: 'ACTIONABLEJOINREQUEST',
ACTIONABLEMENTIONWHISPER: 'ACTIONABLEMENTIONWHISPER',
+ ACTIONABLETRACKEXPENSEWHISPER: 'ACTIONABLETRACKEXPENSEWHISPER',
ADDCOMMENT: 'ADDCOMMENT',
- ACTIONABLEJOINREQUEST: 'ACTIONABLEJOINREQUEST',
APPROVED: 'APPROVED',
CHANGEFIELD: 'CHANGEFIELD', // OldDot Action
CHANGEPOLICY: 'CHANGEPOLICY', // OldDot Action
@@ -773,6 +774,9 @@ const CONST = {
INVITE: 'invited',
NOTHING: 'nothing',
},
+ ACTIONABLE_TRACK_EXPENSE_WHISPER_RESOLUTION: {
+ NOTHING: 'nothing',
+ },
ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION: {
ACCEPT: 'accept',
DECLINE: 'decline',
@@ -880,7 +884,7 @@ const CONST = {
},
TIMING: {
CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action',
- SEARCH_RENDER: 'search_render',
+ CHAT_FINDER_RENDER: 'search_render',
CHAT_RENDER: 'chat_render',
OPEN_REPORT: 'open_report',
HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render',
@@ -1216,9 +1220,9 @@ const CONST = {
NOT_IMPORTED: 'NOT_IMPORTED',
IMPORTED: 'IMPORTED',
},
- QUICK_BOOKS_ONLINE: 'quickbooksOnline',
+ QUICKBOOKS_ONLINE: 'quickbooksOnline',
- QUICK_BOOKS_IMPORTS: {
+ QUICKBOOKS_IMPORTS: {
SYNC_CLASSES: 'syncClasses',
ENABLE_NEW_CATEGORIES: 'enableNewCategories',
SYNC_CUSTOMERS: 'syncCustomers',
@@ -1364,7 +1368,7 @@ const CONST = {
},
KYC_WALL_SOURCE: {
- REPORT: 'REPORT', // The user attempted to pay a money request
+ REPORT: 'REPORT', // The user attempted to pay an expense
ENABLE_WALLET: 'ENABLE_WALLET', // The user clicked on the `Enable wallet` button on the Wallet page
TRANSFER_BALANCE: 'TRANSFER_BALANCE', // The user attempted to transfer their wallet balance to their bank account or debit card
},
@@ -1400,7 +1404,7 @@ const CONST = {
},
IOU: {
- // This is the transactionID used when going through the create money request flow so that it mimics a real transaction (like the edit flow)
+ // This is the transactionID used when going through the create expense flow so that it mimics a real transaction (like the edit flow)
OPTIMISTIC_TRANSACTION_ID: '1',
// Note: These payment types are used when building IOU reportAction message values in the server and should
// not be changed.
@@ -1412,6 +1416,9 @@ const CONST = {
ACTION: {
EDIT: 'edit',
CREATE: 'create',
+ MOVE: 'move',
+ CATEGORIZE: 'categorize',
+ SHARE: 'share',
},
DEFAULT_AMOUNT: 0,
TYPE: {
@@ -1434,6 +1441,7 @@ const CONST = {
DELETE: 'delete',
APPROVE: 'approve',
TRACK: 'track',
+ MOVE: 'move',
},
AMOUNT_MAX_LENGTH: 10,
RECEIPT_STATE: {
@@ -1453,6 +1461,11 @@ const CONST = {
CANCEL_REASON: {
PAYMENT_EXPIRED: 'CANCEL_REASON_PAYMENT_EXPIRED',
},
+ SHARE: {
+ ROLE: {
+ ACCOUNTANT: 'accountant',
+ },
+ },
},
GROWL: {
@@ -1606,25 +1619,23 @@ const CONST = {
GENERAL_SETTINGS: 'generalSettings',
},
CONNECTIONS: {
- SYNC_STATUS: {
- STARTING: 'starting',
- FINISHED: 'finished',
- PROGRESS: 'progress',
- },
NAME: {
// Here we will add other connections names when we add support for them
QBO: 'quickbooksOnline',
},
SYNC_STAGE_NAME: {
STARTING_IMPORT: 'startingImport',
- QBO_CUSTOMERS: 'quickbooksOnlineImportCustomers',
- QBO_EMPLOYEES: 'quickbooksOnlineImportEmployees',
- QBO_ACCOUNTS: 'quickbooksOnlineImportAccounts',
- QBO_CLASSES: 'quickbooksOnlineImportClasses',
- QBO_LOCATIONS: 'quickbooksOnlineImportLocations',
- QBO_PROCESSING: 'quickbooksOnlineImportProcessing',
- QBO_PAYMENTS: 'quickbooksOnlineSyncBillPayments',
- QBO_TAX_CODES: 'quickbooksOnlineSyncTaxCodes',
+ QBO_IMPORT_MAIN: 'quickbooksOnlineImportMain',
+ QBO_IMPORT_CUSTOMERS: 'quickbooksOnlineImportCustomers',
+ QBO_IMPORT_EMPLOYEES: 'quickbooksOnlineImportEmployees',
+ QBO_IMPORT_ACCOUNTS: 'quickbooksOnlineImportAccounts',
+ QBO_IMPORT_CLASSES: 'quickbooksOnlineImportClasses',
+ QBO_IMPORT_LOCATIONS: 'quickbooksOnlineImportLocations',
+ QBO_IMPORT_PROCESSING: 'quickbooksOnlineImportProcessing',
+ QBO_SYNC_PAYMENTS: 'quickbooksOnlineSyncBillPayments',
+ QBO_IMPORT_TAX_CODES: 'quickbooksOnlineSyncTaxCodes',
+ QBO_CHECK_CONNECTION: 'quickbooksOnlineCheckConnection',
+ JOB_DONE: 'jobDone',
},
},
},
@@ -3293,6 +3304,13 @@ const CONST = {
SCAN: 'scan',
DISTANCE: 'distance',
},
+ TAB_SEARCH: {
+ ALL: 'all',
+ SENT: 'sent',
+ DRAFTS: 'drafts',
+ WAITING_ON_YOU: 'waitingOnYou',
+ FINISHED: 'finished',
+ },
STATUS_TEXT_MAX_LENGTH: 100,
DROPDOWN_BUTTON_SIZE: {
@@ -3543,12 +3561,11 @@ const CONST = {
ONBOARDING_CONCIERGE: {
[onboardingChoices.TRACK]:
- "# Welcome to Expensify, let's start tracking your expenses!\n" +
- "Hi there, I'm Concierge. Chat with me here for anything you need.\n" +
+ "# Let's start tracking your expenses!\n" +
'\n' +
"To track your expenses, create a workspace to keep everything in one place. Here's how:\n" +
'1. From the home screen, click the green + button > New Workspace\n' +
- '2. Give your workspace a name (e.g. "My business expenses”).\n' +
+ '2. Give your workspace a name (e.g. "My business expenses").\n' +
'\n' +
'Then, add expenses to your workspace:\n' +
'1. Find your workspace using the search field.\n' +
@@ -3557,8 +3574,7 @@ const CONST = {
'\n' +
"We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!",
[onboardingChoices.EMPLOYER]:
- '# Welcome to Expensify, the fastest way to get paid back!\n' +
- "Hi there, I'm Concierge. Chat with me here for anything you need.\n" +
+ '# Expensify is the fastest way to get paid back!\n' +
'\n' +
'To submit expenses for reimbursement:\n' +
'1. From the home screen, click the green + button > Request money.\n' +
@@ -3566,21 +3582,19 @@ const CONST = {
'\n' +
"That'll send a request to get you paid back. Let me know if you have any questions!",
[onboardingChoices.MANAGE_TEAM]:
- "# Welcome to Expensify, let's start managing your team's expenses!\n" +
- "Hi there, I'm Concierge. Chat with me here for anything you need.\n" +
+ "# Let's start managing your team's expenses!\n" +
'\n' +
"To manage your team's expenses, create a workspace to keep everything in one place. Here's how:\n" +
'1. From the home screen, click the green + button > New Workspace\n' +
- '2. Give your workspace a name (e.g. “Sales team expenses”).\n' +
+ '2. Give your workspace a name (e.g. "Sales team expenses").\n' +
'\n' +
- 'Then, invite your team to your workspace via the Members pane and connect a business bank account to reimburse them. Let me know if you have any questions!',
+ 'Then, invite your team to your workspace via the Members pane and [connect a business bank account](https://help.expensify.com/articles/new-expensify/bank-accounts/Connect-a-Bank-Account) to reimburse them. Let me know if you have any questions!',
[onboardingChoices.PERSONAL_SPEND]:
- "# Welcome to Expensify, let's start tracking your expenses!\n" +
- "Hi there, I'm Concierge. Chat with me here for anything you need.\n" +
+ "# Let's start tracking your expenses! \n" +
'\n' +
"To track your expenses, create a workspace to keep everything in one place. Here's how:\n" +
'1. From the home screen, click the green + button > New Workspace\n' +
- '2. Give your workspace a name (e.g. "My expenses”).\n' +
+ '2. Give your workspace a name (e.g. "My expenses").\n' +
'\n' +
'Then, add expenses to your workspace:\n' +
'1. Find your workspace using the search field.\n' +
@@ -3589,19 +3603,13 @@ const CONST = {
'\n' +
"We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!",
[onboardingChoices.CHAT_SPLIT]:
- '# Welcome to Expensify, where splitting the bill is an easy conversation!\n' +
- "Hi there, I'm Concierge. Chat with me here for anything you need.\n" +
+ '# Splitting the bill is as easy as a conversation!\n' +
'\n' +
'To split an expense:\n' +
'1. From the home screen, click the green + button > Request money.\n' +
'2. Enter an amount or scan a receipt, then choose who you want to split it with.\n' +
'\n' +
"We'll send a request to each person so they can pay you back. Let me know if you have any questions!",
- [onboardingChoices.LOOKING_AROUND]:
- '# Welcome to Expensify!\n' +
- "Hi there, I'm Concierge. Chat with me here for anything you need.\n" +
- '\n' +
- "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.",
},
REPORT_FIELD_TITLE_FIELD_ID: 'text_title',
@@ -4345,8 +4353,10 @@ const CONST = {
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
+
type IOUType = ValueOf;
+type IOUAction = ValueOf;
-export type {Country, IOUType};
+export type {Country, IOUAction, IOUType};
export default CONST;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 3959f76a626f..819680db0e8a 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -126,7 +126,7 @@ const ONYXKEYS = {
/** The NVP with the last payment method used per policy */
NVP_LAST_PAYMENT_METHOD: 'nvp_private_lastPaymentMethod',
- /** This NVP holds to most recent waypoints that a person has used when creating a distance request */
+ /** This NVP holds to most recent waypoints that a person has used when creating a distance expense */
NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints',
/** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */
@@ -312,15 +312,19 @@ const ONYXKEYS = {
COLLECTION: {
DOWNLOAD: 'download_',
POLICY: 'policy_',
- POLICY_MEMBERS: 'policyMembers_',
POLICY_DRAFTS: 'policyDrafts_',
- POLICY_MEMBERS_DRAFTS: 'policyMembersDrafts_',
POLICY_JOIN_MEMBER: 'policyJoinMember_',
POLICY_CATEGORIES: 'policyCategories_',
POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_',
POLICY_TAGS: 'policyTags_',
POLICY_RECENTLY_USED_TAGS: 'nvp_recentlyUsedTags_',
+ // Whether the policy's connection data was attempted to be fetched in
+ // the current user session. As this state only exists client-side, it
+ // should not be included as part of the policy object. The policy
+ // object should mirror the data as it's stored in the database.
+ POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED: 'policyHasConnectionsDataBeenFetched_',
OLD_POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_',
+ POLICY_CONNECTION_SYNC_PROGRESS: 'policyConnectionSyncProgress_',
WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_',
WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_',
REPORT: 'report_',
@@ -340,20 +344,17 @@ const ONYXKEYS = {
TRANSACTION: 'transactions_',
TRANSACTION_VIOLATIONS: 'transactionViolations_',
TRANSACTION_DRAFT: 'transactionsDraft_',
-
- // Holds temporary transactions used during the creation and edit flow
+ SKIP_CONFIRMATION: 'skipConfirmation_',
TRANSACTION_BACKUP: 'transactionsBackup_',
SPLIT_TRANSACTION_DRAFT: 'splitTransactionDraft_',
PRIVATE_NOTES_DRAFT: 'privateNotesDraft_',
NEXT_STEP: 'reportNextStep_',
- // Manual request tab selector
+ // Manual expense tab selector
SELECTED_TAB: 'selectedTab_',
/** This is deprecated, but needed for a migration, so we still need to include it here so that it will be initialized in Onyx.init */
DEPRECATED_POLICY_MEMBER_LIST: 'policyMemberList_',
-
- POLICY_CONNECTION_SYNC_PROGRESS: 'policyConnectionSyncProgress_',
},
/** List of Form ids */
@@ -526,10 +527,9 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy;
[ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategories;
[ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTagList;
- [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers;
- [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories;
- [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers;
+ [ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED]: boolean;
+ [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyEmployeeList;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string;
[ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report;
@@ -545,6 +545,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup;
[ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction;
+ [ONYXKEYS.COLLECTION.SKIP_CONFIRMATION]: boolean;
[ONYXKEYS.COLLECTION.TRANSACTION_BACKUP]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations;
[ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index df5c510ca954..46f2e2fef049 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1,5 +1,6 @@
import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
+import type {IOUAction, IOUType} from './CONST';
import type {IOURequestType} from './libs/actions/IOU';
import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
@@ -22,13 +23,18 @@ const ROUTES = {
ALL_SETTINGS: 'all-settings',
+ SEARCH: {
+ route: '/search/:query',
+ getRoute: (query: string) => `search/${query}` as const,
+ },
+
// This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated
CONCIERGE: 'concierge',
FLAG_COMMENT: {
route: 'flag/:reportID/:reportActionID',
getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` as const,
},
- SEARCH: 'search',
+ CHAT_FINDER: 'chat-finder',
DETAILS: {
route: 'details',
getRoute: (login: string) => `details?login=${encodeURIComponent(login)}` as const,
@@ -205,7 +211,7 @@ const ROUTES = {
EDIT_REQUEST: {
route: 'r/:threadReportID/edit/:field/:tagIndex?',
getRoute: (threadReportID: string, field: ValueOf, tagIndex?: number) =>
- `r/${threadReportID}/edit/${field}${typeof tagIndex === 'number' ? `/${tagIndex}` : ''}` as const,
+ `r/${threadReportID}/edit/${field as string}${typeof tagIndex === 'number' ? `/${tagIndex}` : ''}` as const,
},
EDIT_CURRENCY_REQUEST: {
route: 'r/:threadReportID/edit/currency',
@@ -274,7 +280,7 @@ const ROUTES = {
EDIT_SPLIT_BILL: {
route: `r/:reportID/split/:reportActionID/edit/:field/:tagIndex?`,
getRoute: (reportID: string, reportActionID: string, field: ValueOf, tagIndex?: number) =>
- `r/${reportID}/split/${reportActionID}/edit/${field}${typeof tagIndex === 'number' ? `/${tagIndex}` : ''}` as const,
+ `r/${reportID}/split/${reportActionID}/edit/${field as string}${typeof tagIndex === 'number' ? `/${tagIndex}` : ''}` as const,
},
TASK_TITLE: {
route: 'r/:reportID/title',
@@ -301,127 +307,118 @@ const ROUTES = {
getRoute: (reportID: string) => `r/${reportID}/members` as const,
},
ROOM_INVITE: {
- route: 'r/:reportID/invite',
- getRoute: (reportID: string) => `r/${reportID}/invite` as const,
+ route: 'r/:reportID/invite/:role?',
+ getRoute: (reportID: string, role?: string) => `r/${reportID}/invite/${role}` as const,
},
MONEY_REQUEST_PARTICIPANTS: {
route: ':iouType/new/participants/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}` as const,
+ getRoute: (iouType: IOUType, reportID = '') => `${iouType}/new/participants/${reportID}` as const,
},
MONEY_REQUEST_HOLD_REASON: {
- route: ':iouType/edit/reason/:transactionID?',
- getRoute: (iouType: string, transactionID: string, reportID: string, backTo: string) => `${iouType}/edit/reason/${transactionID}?backTo=${backTo}&reportID=${reportID}` as const,
+ route: ':type/edit/reason/:transactionID?',
+ getRoute: (type: ValueOf, transactionID: string, reportID: string, backTo: string) =>
+ `${type}/edit/reason/${transactionID}?backTo=${backTo}&reportID=${reportID}` as const,
},
MONEY_REQUEST_MERCHANT: {
route: ':iouType/new/merchant/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}` as const,
+ getRoute: (iouType: IOUType, reportID = '') => `${iouType}/new/merchant/${reportID}` as const,
},
MONEY_REQUEST_RECEIPT: {
route: ':iouType/new/receipt/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const,
+ getRoute: (iouType: IOUType, reportID = '') => `${iouType}/new/receipt/${reportID}` as const,
},
MONEY_REQUEST_CREATE: {
route: ':action/:iouType/start/:transactionID/:reportID',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
- `${action}/${iouType}/start/${transactionID}/${reportID}` as const,
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => `${action as string}/${iouType as string}/start/${transactionID}/${reportID}` as const,
},
MONEY_REQUEST_STEP_CONFIRMATION: {
route: ':action/:iouType/confirmation/:transactionID/:reportID',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
- `${action}/${iouType}/confirmation/${transactionID}/${reportID}` as const,
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) =>
+ `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}` as const,
},
MONEY_REQUEST_STEP_AMOUNT: {
route: ':action/:iouType/amount/:transactionID/:reportID',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/amount/${transactionID}/${reportID}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/amount/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_TAX_RATE: {
route: ':action/:iouType/taxRate/:transactionID/:reportID?',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/taxRate/${transactionID}/${reportID}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/taxRate/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_TAX_AMOUNT: {
route: ':action/:iouType/taxAmount/:transactionID/:reportID?',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/taxAmount/${transactionID}/${reportID}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/taxAmount/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_CATEGORY: {
route: ':action/:iouType/category/:transactionID/:reportID/:reportActionID?',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
- getUrlWithBackToParam(`${action}/${iouType}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
MONEY_REQUEST_STEP_CURRENCY: {
route: ':action/:iouType/currency/:transactionID/:reportID/:pageIndex?',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', currency = '', backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/currency/${transactionID}/${reportID}/${pageIndex}?currency=${currency}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex = '', currency = '', backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/currency/${transactionID}/${reportID}/${pageIndex}?currency=${currency}`, backTo),
},
MONEY_REQUEST_STEP_DATE: {
route: ':action/:iouType/date/:transactionID/:reportID',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/date/${transactionID}/${reportID}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/date/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_DESCRIPTION: {
route: ':action/:iouType/description/:transactionID/:reportID/:reportActionID?',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
- getUrlWithBackToParam(`${action}/${iouType}/description/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/description/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
MONEY_REQUEST_STEP_DISTANCE: {
route: ':action/:iouType/distance/:transactionID/:reportID',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/distance/${transactionID}/${reportID}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/distance/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_MERCHANT: {
route: ':action/:iouType/merchant/:transactionID/:reportID',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/merchant/${transactionID}/${reportID}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/merchant/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_PARTICIPANTS: {
- route: 'create/:iouType/participants/:transactionID/:reportID',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`create/${iouType}/participants/${transactionID}/${reportID}`, backTo),
+ route: ':action/:iouType/participants/:transactionID/:reportID',
+ getRoute: (iouType: IOUType, transactionID: string, reportID: string, backTo = '', action: IOUAction = 'create') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/participants/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_SCAN: {
route: ':action/:iouType/scan/:transactionID/:reportID',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/scan/${transactionID}/${reportID}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/scan/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_TAG: {
route: ':action/:iouType/tag/:orderWeight/:transactionID/:reportID/:reportActionID?',
- getRoute: (
- action: ValueOf,
- iouType: ValueOf,
- orderWeight: number,
- transactionID: string,
- reportID: string,
- backTo = '',
- reportActionID?: string,
- ) => getUrlWithBackToParam(`${action}/${iouType}/tag/${orderWeight}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, orderWeight: number, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/tag/${orderWeight}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
MONEY_REQUEST_STEP_WAYPOINT: {
route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID?: string, pageIndex = '', backTo = '') =>
- getUrlWithBackToParam(`${action}/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID?: string, pageIndex = '', backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo),
},
// This URL is used as a redirect to one of the create tabs below. This is so that we can message users with a link
// straight to those flows without needing to have optimistic transaction and report IDs.
MONEY_REQUEST_START: {
route: 'start/:iouType/:iouRequestType',
- getRoute: (iouType: ValueOf, iouRequestType: IOURequestType) => `start/${iouType}/${iouRequestType}` as const,
+ getRoute: (iouType: IOUType, iouRequestType: IOURequestType) => `start/${iouType as string}/${iouRequestType}` as const,
},
MONEY_REQUEST_CREATE_TAB_DISTANCE: {
route: ':action/:iouType/start/:transactionID/:reportID/distance',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
- `create/${iouType}/start/${transactionID}/${reportID}/distance` as const,
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => `create/${iouType as string}/start/${transactionID}/${reportID}/distance` as const,
},
MONEY_REQUEST_CREATE_TAB_MANUAL: {
route: ':action/:iouType/start/:transactionID/:reportID/manual',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
- `${action}/${iouType}/start/${transactionID}/${reportID}/manual` as const,
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) =>
+ `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/manual` as const,
},
MONEY_REQUEST_CREATE_TAB_SCAN: {
route: ':action/:iouType/start/:transactionID/:reportID/scan',
- getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
- `create/${iouType}/start/${transactionID}/${reportID}/scan` as const,
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => `create/${iouType as string}/start/${transactionID}/${reportID}/scan` as const,
},
MONEY_REQUEST_STATE_SELECTOR: {
@@ -563,7 +560,7 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/members',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const,
},
- WORKSPACE_ACCOUNTING: {
+ POLICY_ACCOUNTING: {
route: 'settings/workspaces/:policyID/accounting',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const,
},
@@ -654,7 +651,7 @@ const ROUTES = {
WORKSPACE_OWNER_CHANGE_CHECK: {
route: 'settings/workspaces/:policyID/change-owner/:accountID/:error',
getRoute: (policyID: string, accountID: number, error: ValueOf) =>
- `settings/workspaces/${policyID}/change-owner/${accountID}/${error}` as const,
+ `settings/workspaces/${policyID}/change-owner/${accountID}/${error as string}` as const,
},
WORKSPACE_TAX_CREATE: {
route: 'settings/workspaces/:policyID/taxes/new',
@@ -707,27 +704,27 @@ const ROUTES = {
route: 'r/:reportID/transaction/:transactionID/receipt',
getRoute: (reportID: string, transactionID: string) => `r/${reportID}/transaction/${transactionID}/receipt` as const,
},
- WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: {
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import` as const,
},
- WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS: {
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/accounts',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/accounts` as const,
},
- WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES: {
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/classes',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/classes` as const,
},
- WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS: {
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/customers',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/customers` as const,
},
- WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS: {
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/locations',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/locations` as const,
},
- WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_TAXES: {
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_TAXES: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/taxes',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/taxes` as const,
},
@@ -742,7 +739,7 @@ const HYBRID_APP_ROUTES = {
MONEY_REQUEST_CREATE: '/request/new/scan',
} as const;
-export {getUrlWithBackToParam, HYBRID_APP_ROUTES};
+export {HYBRID_APP_ROUTES, getUrlWithBackToParam};
export default ROUTES;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -762,4 +759,4 @@ type RouteIsPlainString = AssertTypesNotEqual {
// Check if the file dimensions indicate corruption
- // The width/height for corrupt file is -1 on android native and 0 on ios native
- if (!fileData.width || !fileData.height || (fileData.width <= 0 && fileData.height <= 0)) {
+ // The width/height for a corrupted file is -1 on android native and 0 on ios native
+ // We must check only numeric values because the width/height can be undefined for non-image files
+ if ((typeof fileData.width === 'number' && fileData.width <= 0) || (typeof fileData.height === 'number' && fileData.height <= 0)) {
showImageCorruptionAlert();
return Promise.resolve();
}
diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index f6afb4dae2d6..c7a4ece0de97 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -65,7 +65,6 @@ function AvatarWithDisplayName({
const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails);
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false);
const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report);
- const isExpenseRequest = ReportUtils.isExpenseRequest(report);
const avatarBorderColor = isAnonymous ? theme.highlightBG : theme.componentBG;
const actorAccountID = useRef(null);
@@ -128,7 +127,7 @@ function AvatarWithDisplayName({
/>
)}
-
+
;
};
diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx
new file mode 100644
index 000000000000..4d482cb92ead
--- /dev/null
+++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx
@@ -0,0 +1,68 @@
+import React, {useState} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import {WebView} from 'react-native-webview';
+import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
+import Button from '@components/Button';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import Modal from '@components/Modal';
+import useLocalize from '@hooks/useLocalize';
+import {getQuickBooksOnlineSetupLink} from '@libs/actions/connections/QuickBooksOnline';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Session} from '@src/types/onyx';
+import type {ConnectToQuickbooksOnlineButtonProps} from './types';
+
+type ConnectToQuickbooksOnlineButtonOnyxProps = {
+ /** Session info for the currently logged in user. */
+ session: OnyxEntry;
+};
+
+const renderLoading = () => ;
+
+function ConnectToQuickbooksOnlineButton({policyID, session}: ConnectToQuickbooksOnlineButtonProps & ConnectToQuickbooksOnlineButtonOnyxProps) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const {translate} = useLocalize();
+
+ return (
+ <>
+
);
}
diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx
index e3d226a17999..8830681bc55f 100644
--- a/src/components/Indicator.tsx
+++ b/src/components/Indicator.tsx
@@ -8,14 +8,11 @@ import * as PolicyUtils from '@libs/PolicyUtils';
import * as UserUtils from '@libs/UserUtils';
import * as PaymentMethods from '@userActions/PaymentMethods';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {BankAccountList, FundList, LoginList, Policy, PolicyMembers, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx';
+import type {BankAccountList, FundList, LoginList, Policy, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx';
type CheckingMethod = () => boolean;
type IndicatorOnyxProps = {
- /** The employee list of all policies (coming from Onyx) */
- allPolicyMembers: OnyxCollection;
-
/** All the user's policies (from Onyx via withFullPolicy) */
policies: OnyxCollection;
@@ -40,14 +37,13 @@ type IndicatorOnyxProps = {
type IndicatorProps = IndicatorOnyxProps;
-function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) {
+function Indicator({reimbursementAccount, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) {
const theme = useTheme();
const styles = useThemeStyles();
// If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and
// those should be cleaned out before doing any error checking
const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id));
- const cleanAllPolicyMembers = Object.fromEntries(Object.entries(allPolicyMembers ?? {}).filter(([, policyMembers]) => !!policyMembers));
// All of the error & info-checking methods are put into an array. This is so that using _.some() will return
// early as soon as the first error / info condition is returned. This makes the checks very efficient since
@@ -57,7 +53,7 @@ function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccoun
() => PaymentMethods.hasPaymentMethodError(bankAccountList, fundList),
() => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError),
() => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError),
- () => Object.values(cleanAllPolicyMembers).some(PolicyUtils.hasPolicyMemberError),
+ () => Object.values(cleanPolicies).some(PolicyUtils.hasEmployeeListError),
() => Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
() => !!loginList && UserUtils.hasLoginListError(loginList),
@@ -77,9 +73,6 @@ function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccoun
Indicator.displayName = 'Indicator';
export default withOnyx({
- allPolicyMembers: {
- key: ONYXKEYS.COLLECTION.POLICY_MEMBERS,
- },
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
},
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index f23c8db97f47..5168544357e6 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -334,7 +334,7 @@ function MenuItem(
const StyleUtils = useStyleUtils();
const combinedStyle = [style, styles.popoverMenuItem];
const {isSmallScreenWidth} = useWindowDimensions();
- const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {};
+ const {isExecuting, singleExecution} = useContext(MenuItemGroupContext) ?? {};
const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false;
const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1;
@@ -409,15 +409,11 @@ function MenuItem(
}
if (onPress && event) {
- if (!singleExecution || !waitForNavigate) {
+ if (!singleExecution) {
onPress(event);
return;
}
- singleExecution(
- waitForNavigate(() => {
- onPress(event);
- }),
- )();
+ singleExecution(onPress)(event);
}
};
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 14227d6a2051..56dc6bf0075d 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -43,7 +43,7 @@ type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & {
/** The report currently being looked at */
report: OnyxTypes.Report;
- /** The policy tied to the money request report */
+ /** The policy tied to the expense report */
policy: OnyxEntry;
/** Array of report actions for the report */
@@ -288,7 +288,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
danger
/>
setIsDeleteRequestModalVisible(false)}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index aa6c75edbf5d..49bc43dff523 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -1,43 +1,49 @@
import {useIsFocused} from '@react-navigation/native';
import {format} from 'date-fns';
+import Str from 'expensify-common/lib/str';
import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {StyleProp, ViewStyle} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
+import type {OnyxEntry} from 'react-native-onyx';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
+import usePrevious from '@hooks/usePrevious';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import type {DefaultMileageRate} from '@libs/DistanceRequestUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
+import type {DefaultMileageRate} from '@libs/DistanceRequestUtils';
import * as IOUUtils from '@libs/IOUUtils';
import Log from '@libs/Log';
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
+import {isTaxTrackingEnabled} from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportUtils from '@libs/ReportUtils';
+import playSound, {SOUNDS} from '@libs/Sound';
import * as TransactionUtils from '@libs/TransactionUtils';
+import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import * as IOU from '@userActions/IOU';
+import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
-import type {ReceiptSource} from '@src/types/onyx/Transaction';
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
import type {DropdownOption} from './ButtonWithDropdownMenu/types';
import ConfirmedRoute from './ConfirmedRoute';
+import ConfirmModal from './ConfirmModal';
import FormHelpMessage from './FormHelpMessage';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
import OptionsSelector from './OptionsSelector';
+import PDFThumbnail from './PDFThumbnail';
import ReceiptEmptyState from './ReceiptEmptyState';
import ReceiptImage from './ReceiptImage';
import SettlementButton from './SettlementButton';
@@ -45,8 +51,6 @@ import ShowMoreButton from './ShowMoreButton';
import Switch from './Switch';
import Text from './Text';
-type IouType = ValueOf;
-
type MoneyRequestConfirmationListOnyxProps = {
/** Collection of categories attached to a policy */
policyCategories: OnyxEntry;
@@ -57,21 +61,19 @@ type MoneyRequestConfirmationListOnyxProps = {
/** The policy of the report */
policy: OnyxEntry;
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- iou: OnyxEntry;
-
/** The session of the logged in user */
session: OnyxEntry;
- /** Unit and rate used for if the money request is a distance request */
+ /** Unit and rate used for if the expense is a distance expense */
mileageRate: OnyxEntry;
};
+
type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & {
/** Callback to inform parent modal of success */
- onConfirm?: (selectedParticipants: Array) => void;
+ onConfirm?: (selectedParticipants: Participant[]) => void;
- /** Callback to parent modal to send money */
- onSendMoney?: (paymentMethod: IouType | PaymentMethodType | undefined) => void;
+ /** Callback to parent modal to pay someone */
+ onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void;
/** Callback to inform a participant is selected */
onSelectParticipant?: (option: Participant) => void;
@@ -89,7 +91,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
iouCurrencyCode?: string;
/** IOU type */
- iouType?: IouType;
+ iouType?: IOUType;
/** IOU date */
iouCreated?: string;
@@ -100,9 +102,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
/** IOU Category */
iouCategory?: string;
- /** IOU Tag */
- iouTag?: string;
-
/** IOU isBillable */
iouIsBillable?: boolean;
@@ -110,9 +109,9 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
onToggleBillable?: (isOn: boolean) => void;
/** Selected participants from MoneyRequestModal with login / accountID */
- selectedParticipants: Array;
+ selectedParticipants: Participant[];
- /** Payee of the money request with login */
+ /** Payee of the expense with login */
payeePersonalDetails?: OnyxEntry;
/** Can the participants be modified or not */
@@ -131,7 +130,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
reportID?: string;
/** File path of the receipt */
- receiptPath?: ReceiptSource;
+ receiptPath?: string;
/** File name of the receipt */
receiptFilename?: string;
@@ -139,19 +138,16 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
/** List styles for OptionsSelector */
listStyles?: StyleProp;
- /** ID of the transaction that represents the money request */
- transactionID?: string;
-
- /** Transaction that represents the money request */
+ /** Transaction that represents the expense */
transaction?: OnyxEntry;
- /** Whether the money request is a distance request */
+ /** Whether the expense is a distance expense */
isDistanceRequest?: boolean;
- /** Whether the money request is a scan request */
+ /** Whether the expense is a scan expense */
isScanRequest?: boolean;
- /** Whether we're editing a split bill */
+ /** Whether we're editing a split expense */
isEditingSplitBill?: boolean;
/** Whether we should show the amount, date, and merchant fields. */
@@ -165,6 +161,14 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
/** The ID of the report action */
reportActionID?: string;
+
+ /** The action to take */
+ action?: IOUAction;
+};
+
+const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => {
+ const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? '';
+ return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0);
};
function MoneyRequestConfirmationList({
@@ -189,19 +193,6 @@ function MoneyRequestConfirmationList({
hasMultipleParticipants,
selectedParticipants: selectedParticipantsProp,
payeePersonalDetails: payeePersonalDetailsProp,
- iou = {
- id: '',
- amount: 0,
- currency: CONST.CURRENCY.USD,
- comment: '',
- merchant: '',
- category: '',
- tag: '',
- billable: false,
- created: '',
- participants: [],
- receiptPath: '',
- },
canModifyParticipants: canModifyParticipantsProp = false,
session,
isReadOnly = false,
@@ -215,22 +206,20 @@ function MoneyRequestConfirmationList({
iouCreated,
iouIsBillable = false,
onToggleBillable,
- iouTag = '',
- transactionID = '',
hasSmartScanFailed,
reportActionID,
+ action = CONST.IOU.ACTION.CREATE,
}: MoneyRequestConfirmationListProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
- const {canUseViolations} = usePermissions();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const {canUseViolations} = usePermissions();
const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
- const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
+ const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT;
const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
-
- const isSplitWithScan = isSplitBill && isScanRequest;
+ const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE;
const {unit, rate, currency} = mileageRate ?? {
unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES,
@@ -240,32 +229,34 @@ function MoneyRequestConfirmationList({
const distance = transaction?.routes?.route0.distance ?? 0;
const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0;
const taxRates = policy?.taxRates;
+ const transactionID = transaction?.transactionID ?? '';
// A flag for showing the categories field
const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {})));
+
// A flag and a toggler for showing the rest of the form fields
const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false);
- // Do not hide fields in case of send money request
- const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill;
+ // Do not hide fields in case of paying someone
+ const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || !!isEditingSplitBill;
// In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item
- const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan;
- const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !isDistanceRequest && !isSplitWithScan;
+ const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend;
+ const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend;
const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
// A flag for showing the tags field
- const shouldShowTags = isPolicyExpenseChat && (!!iouTag || OptionsListUtils.hasEnabledTags(policyTagLists));
+ const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);
- // A flag for showing tax fields - tax rate and tax amount
- const shouldShowTax = isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled);
+ // A flag for showing tax rate
+ const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy);
// A flag for showing the billable field
- const shouldShowBillable = !(policy?.disabledFields?.defaultBillable ?? true);
-
+ const shouldShowBillable = policy?.disabledFields?.defaultBillable === false;
+ const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action);
const hasRoute = TransactionUtils.hasRoute(transaction);
- const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate);
+ const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense;
const formattedAmount = isDistanceRequestWithPendingRoute
? ''
: CurrencyUtils.convertToDisplayString(
@@ -273,11 +264,9 @@ function MoneyRequestConfirmationList({
isDistanceRequest ? currency : iouCurrencyCode,
);
const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode);
+ const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : '';
- const defaultTaxKey = taxRates?.defaultExternalID;
- const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) ${CONST.DOT_SEPARATOR} ${translate('common.default')}`) ?? '';
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing is not working when a left hand side value is ''
- const taxRateTitle = transaction?.taxRate?.text || defaultTaxName;
+ const previousTransactionAmount = usePrevious(transaction?.amount);
const isFocused = useIsFocused();
const [formError, setFormError] = useState('');
@@ -285,6 +274,14 @@ function MoneyRequestConfirmationList({
const [didConfirm, setDidConfirm] = useState(false);
const [didConfirmSplit, setDidConfirmSplit] = useState(false);
+ const [merchantError, setMerchantError] = useState(false);
+
+ const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
+
+ const navigateBack = () => {
+ Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID));
+ };
+
const shouldDisplayFieldError: boolean = useMemo(() => {
if (!isEditingSplitBill) {
return false;
@@ -294,20 +291,38 @@ function MoneyRequestConfirmationList({
}, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]);
const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
- const shouldDisplayMerchantError = isPolicyExpenseChat && shouldDisplayFieldError && isMerchantEmpty;
+ const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant;
+
+ const isCategoryRequired = canUseViolations && !!policy?.requiresCategory;
useEffect(() => {
- if (shouldDisplayFieldError && didConfirmSplit) {
- setFormError('iou.error.genericSmartscanFailureMessage');
+ if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) {
return;
}
+ if (!isMerchantEmpty && merchantError) {
+ setMerchantError(false);
+ if (formError === 'iou.error.invalidMerchant') {
+ setFormError('');
+ }
+ }
+ }, [formError, isMerchantEmpty, merchantError, isMerchantRequired]);
+
+ useEffect(() => {
if (shouldDisplayFieldError && hasSmartScanFailed) {
setFormError('iou.receiptScanningFailed');
return;
}
+ if (shouldDisplayFieldError && didConfirmSplit) {
+ setFormError('iou.error.genericSmartscanFailureMessage');
+ return;
+ }
+ if (merchantError) {
+ setFormError('iou.error.invalidMerchant');
+ return;
+ }
// reset the form error whenever the screen gains or loses focus
setFormError('');
- }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]);
+ }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]);
useEffect(() => {
if (!shouldCalculateDistanceAmount) {
@@ -315,39 +330,50 @@ function MoneyRequestConfirmationList({
}
const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0);
- IOU.setMoneyRequestAmount(amount);
- }, [shouldCalculateDistanceAmount, distance, rate, unit]);
+ IOU.setMoneyRequestAmount(transactionID, amount, currency ?? '');
+ }, [shouldCalculateDistanceAmount, distance, rate, unit, transactionID, currency]);
+
+ // Calculate and set tax amount in transaction draft
+ useEffect(() => {
+ const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString();
+ const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));
+
+ if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) {
+ return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true);
+ }
+
+ IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true);
+ }, [taxRates?.defaultValue, transaction, transactionID, previousTransactionAmount]);
/**
* Returns the participants with amount
*/
const getParticipantsWithAmount = useCallback(
- (participantsList: Array): Array => {
- const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? '');
- return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(
- participantsList,
- iouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '',
- );
+ (participantsList: Participant[]) => {
+ const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? '');
+ return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '');
},
[iouAmount, iouCurrencyCode],
);
- // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again
+ // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again
if (isEditingSplitBill && didConfirm) {
setDidConfirm(false);
}
- const splitOrRequestOptions: Array> = useMemo(() => {
+ const splitOrRequestOptions: Array> = useMemo(() => {
let text;
- if (isSplitBill && iouAmount === 0) {
- text = translate('iou.split');
- } else if ((!!receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) {
- text = translate('iou.request');
+ if (isTypeTrackExpense) {
+ text = translate('iou.trackExpense');
+ } else if (isTypeSplit && iouAmount === 0) {
+ text = translate('iou.splitExpense');
+ } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) {
+ text = translate('iou.submitExpense');
if (iouAmount !== 0) {
- text = translate('iou.requestAmount', {amount: formattedAmount});
+ text = translate('iou.submitAmount', {amount: formattedAmount});
}
} else {
- const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.requestAmount';
+ const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.submitAmount';
text = translate(translationKey, {amount: formattedAmount});
}
return [
@@ -356,17 +382,13 @@ function MoneyRequestConfirmationList({
value: iouType,
},
];
- }, [isSplitBill, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]);
+ }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]);
- const selectedParticipants: Array = useMemo(
- () => selectedParticipantsProp.filter((participant) => participant.selected),
- [selectedParticipantsProp],
- );
+ const selectedParticipants = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]);
const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]);
const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants;
const shouldDisablePaidBySection = canModifyParticipants;
-
- const optionSelectorSections: OptionsListUtils.CategorySection[] = useMemo(() => {
+ const optionSelectorSections = useMemo(() => {
const sections = [];
const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected);
if (hasMultipleParticipants) {
@@ -400,9 +422,9 @@ function MoneyRequestConfirmationList({
},
);
} else {
- const formattedSelectedParticipants = selectedParticipantsProp.map((participant) => ({
+ const formattedSelectedParticipants = selectedParticipants.map((participant) => ({
...participant,
- isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
+ isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
}));
sections.push({
title: translate('common.to'),
@@ -413,18 +435,18 @@ function MoneyRequestConfirmationList({
return sections;
}, [
selectedParticipants,
+ selectedParticipantsProp,
hasMultipleParticipants,
iouAmount,
iouCurrencyCode,
getParticipantsWithAmount,
- selectedParticipantsProp,
payeePersonalDetails,
translate,
shouldDisablePaidBySection,
canModifyParticipants,
]);
- const selectedOptions: Array = useMemo(() => {
+ const selectedOptions = useMemo(() => {
if (!hasMultipleParticipants) {
return [];
}
@@ -432,7 +454,7 @@ function MoneyRequestConfirmationList({
}, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]);
useEffect(() => {
- if (!isDistanceRequest) {
+ if (!isDistanceRequest || isMovingTransactionFromTrackExpense) {
return;
}
@@ -444,9 +466,51 @@ function MoneyRequestConfirmationList({
IOU.setMoneyRequestPendingFields(transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? CONST.CURRENCY.USD, translate, toLocaleDigit);
- IOU.setMoneyRequestMerchant(transactionID, distanceMerchant, false);
- }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transactionID]);
+ IOU.setMoneyRequestMerchant(transactionID, distanceMerchant, true);
+ }, [
+ isDistanceRequestWithPendingRoute,
+ hasRoute,
+ distance,
+ unit,
+ rate,
+ currency,
+ translate,
+ toLocaleDigit,
+ isDistanceRequest,
+ transaction,
+ transactionID,
+ action,
+ isMovingTransactionFromTrackExpense,
+ ]);
+ // 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) {
+ return;
+ }
+ IOU.setMoneyRequestCategory(transactionID, enabledCategories[0].name);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [shouldShowCategories, policyCategories, isCategoryRequired]);
+
+ // Auto select the tag if there is only one enabled tag and it is required
+ useEffect(() => {
+ let updatedTagsString = TransactionUtils.getTag(transaction);
+ policyTagLists.forEach((tagList, index) => {
+ const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled);
+ const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations;
+ if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) {
+ return;
+ }
+ updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index);
+ });
+ if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) {
+ IOU.setMoneyRequestTag(transactionID, updatedTagsString);
+ }
+ }, [policyTagLists, transaction, transactionID, policyTags, canUseViolations]);
+
+ /**
+ */
const selectParticipant = useCallback(
(option: Participant) => {
// Return early if selected option is currently logged in user.
@@ -462,24 +526,37 @@ function MoneyRequestConfirmationList({
* Navigate to report details or profile of selected user
*/
const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => {
- if (option.accountID) {
- const activeRoute = Navigation.getActiveRouteWithoutParams();
+ const activeRoute = Navigation.getActiveRouteWithoutParams();
+ if (option.isSelfDM) {
+ Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute));
+ return;
+ }
+
+ if (option.accountID) {
Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute));
} else if (option.reportID) {
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute));
}
};
+ /**
+ * @param {String} paymentMethod
+ */
const confirm = useCallback(
- (paymentMethod: IouType | PaymentMethodType | undefined) => {
- if (!selectedParticipants.length) {
+ (paymentMethod: PaymentMethodType | undefined) => {
+ if (selectedParticipants.length === 0) {
+ return;
+ }
+ if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) {
+ setMerchantError(true);
return;
}
if (iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) {
setFormError('iou.error.invalidCategoryLength');
return;
}
+
if (iouType === CONST.IOU.TYPE.SEND) {
if (!paymentMethod) {
return;
@@ -490,34 +567,39 @@ function MoneyRequestConfirmationList({
Log.info(`[IOU] Sending money via: ${paymentMethod}`);
onSendMoney?.(paymentMethod);
} else {
- // validate the amount for distance requests
+ // validate the amount for distance expenses
const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode);
if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) {
setFormError('common.error.invalidAmount');
return;
}
- if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) {
+ if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) {
setDidConfirmSplit(true);
+ setFormError('iou.error.genericSmartscanFailureMessage');
return;
}
+ playSound(SOUNDS.DONE);
setDidConfirm(true);
onConfirm?.(selectedParticipants);
}
},
[
selectedParticipants,
- onSendMoney,
- onConfirm,
- isEditingSplitBill,
+ isMerchantRequired,
+ isMerchantEmpty,
+ shouldDisplayFieldError,
+ transaction,
iouType,
+ onSendMoney,
+ iouCurrencyCode,
isDistanceRequest,
iouCategory,
isDistanceRequestWithPendingRoute,
- iouCurrencyCode,
iouAmount,
- transaction,
+ isEditingSplitBill,
+ onConfirm,
],
);
@@ -536,6 +618,7 @@ function MoneyRequestConfirmationList({
onPress={confirm}
enablePaymentsRoute={ROUTES.IOU_SEND_ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
+ shouldShowPersonalBankAccountOption
currency={iouCurrencyCode}
policyID={policyID}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
@@ -547,7 +630,6 @@ function MoneyRequestConfirmationList({
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
}}
- shouldShowPersonalBankAccountOption
enterKeyEventListenerPriority={1}
/>
) : (
@@ -555,33 +637,281 @@ function MoneyRequestConfirmationList({
success
pressOnEnter
isDisabled={shouldDisableButton}
- // eslint-disable-next-line @typescript-eslint/naming-convention
- onPress={(_event, value) => confirm(value)}
+ onPress={(event, value) => confirm(value as PaymentMethodType)}
options={splitOrRequestOptions}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
enterKeyEventListenerPriority={1}
/>
);
+
return (
<>
- {!!formError.length && (
+ {!!formError && (
)}
+
{button}
>
);
- }, [isReadOnly, iouType, bankAccountRoute, iouCurrencyCode, policyID, selectedParticipants.length, confirm, splitOrRequestOptions, formError, styles.ph1, styles.mb2]);
+ }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]);
+
+ // An intermediate structure that helps us classify the fields as "primary" and "supplementary".
+ // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones.
+ const classifiedFields = [
+ {
+ item: (
+ {
+ if (isDistanceRequest) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ }}
+ style={[styles.moneyRequestMenuItem, styles.mt2]}
+ titleStyle={styles.moneyRequestConfirmationAmount}
+ disabled={didConfirm}
+ brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''}
+ />
+ ),
+ shouldShow: shouldShowSmartScanFields,
+ isSupplementary: false,
+ },
+ {
+ item: (
+ {
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID),
+ );
+ }}
+ style={[styles.moneyRequestMenuItem]}
+ titleStyle={styles.flex1}
+ disabled={didConfirm}
+ interactive={!isReadOnly}
+ numberOfLinesTitle={2}
+ />
+ ),
+ shouldShow: true,
+ isSupplementary: false,
+ },
+ {
+ item: (
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ disabled={didConfirm}
+ // todo: handle edit for transaction while moving from track expense
+ interactive={!isReadOnly && !isMovingTransactionFromTrackExpense}
+ />
+ ),
+ shouldShow: isDistanceRequest,
+ isSupplementary: true,
+ },
+ {
+ item: (
+ {
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ }}
+ disabled={didConfirm}
+ interactive={!isReadOnly}
+ brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ error={merchantError ? translate('common.error.fieldRequired') : ''}
+ rightLabel={isMerchantRequired ? translate('common.required') : ''}
+ />
+ ),
+ shouldShow: shouldShowMerchant,
+ isSupplementary: !isMerchantRequired,
+ },
+ {
+ item: (
+ {
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ }}
+ disabled={didConfirm}
+ interactive={!isReadOnly}
+ brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''}
+ />
+ ),
+ shouldShow: shouldShowDate,
+ isSupplementary: true,
+ },
+ {
+ item: (
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID))
+ }
+ style={[styles.moneyRequestMenuItem]}
+ titleStyle={styles.flex1}
+ disabled={didConfirm}
+ interactive={!isReadOnly}
+ rightLabel={isCategoryRequired ? translate('common.required') : ''}
+ />
+ ),
+ shouldShow: shouldShowCategories,
+ isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired,
+ },
+ ...policyTagLists.map(({name, required}, index) => {
+ const isTagRequired = required === undefined ? false : canUseViolations && required;
+ return {
+ item: (
+
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID),
+ )
+ }
+ style={[styles.moneyRequestMenuItem]}
+ disabled={didConfirm}
+ interactive={!isReadOnly}
+ rightLabel={isTagRequired ? translate('common.required') : ''}
+ />
+ ),
+ shouldShow: shouldShowTags,
+ isSupplementary: !isTagRequired,
+ };
+ }),
+ {
+ item: (
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ disabled={didConfirm}
+ interactive={!isReadOnly}
+ />
+ ),
+ shouldShow: shouldShowTax,
+ isSupplementary: true,
+ },
+ {
+ item: (
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ disabled={didConfirm}
+ interactive={!isReadOnly}
+ />
+ ),
+ shouldShow: shouldShowTax,
+ isSupplementary: true,
+ },
+ {
+ item: (
+
+ {translate('common.billable')}
+ onToggleBillable?.(isOn)}
+ />
+
+ ),
+ shouldShow: shouldShowBillable,
+ isSupplementary: true,
+ },
+ ];
+
+ const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item);
+
+ const supplementaryFields = classifiedFields
+ .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary)
+ .map((supplementaryField) => supplementaryField.item);
const {
image: receiptImage,
thumbnail: receiptThumbnail,
isThumbnail,
fileExtension,
- } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI);
+ isLocalFile,
+ } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI);
+
+ const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? '');
+ const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? '');
+
+ const receiptThumbnailContent = useMemo(
+ () =>
+ isLocalFile && Str.isPDF(receiptFilename) ? (
+ setIsAttachmentInvalid(true)}
+ />
+ ) : (
+
+ ),
+ [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension],
+ );
+
return (
// @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q)
{isDistanceRequest && (
-
+
)}
- {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
- {receiptImage || receiptThumbnail ? (
-
- ) : (
- // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
- PolicyUtils.isPaidGroupPolicy(policy) &&
- !isDistanceRequest &&
- iouType === CONST.IOU.TYPE.REQUEST && (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- />
- )
- )}
- {shouldShowSmartScanFields && (
- {
- if (isDistanceRequest) {
- return;
- }
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
- );
- }}
- style={[styles.moneyRequestMenuItem, styles.mt2]}
- titleStyle={styles.moneyRequestConfirmationAmount}
- disabled={didConfirm}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''}
- />
- )}
- {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(
- CONST.IOU.ACTION.EDIT,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- reportActionID,
- ),
- );
- }}
- style={styles.moneyRequestMenuItem}
- titleStyle={styles.flex1}
- disabled={didConfirm}
- interactive={!isReadOnly}
- numberOfLinesTitle={2}
- />
+ {(!isMovingTransactionFromTrackExpense || !hasRoute) &&
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ (receiptImage || receiptThumbnail
+ ? receiptThumbnailContent
+ : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
+ PolicyUtils.isPaidGroupPolicy(policy) &&
+ !isDistanceRequest &&
+ iouType === CONST.IOU.TYPE.REQUEST && (
+
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ )
+ }
+ />
+ ))}
+ {primaryFields}
{!shouldShowAllFields && (
)}
- {shouldShowAllFields && (
- <>
- {shouldShowDate && (
- {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(
- CONST.IOU.ACTION.EDIT,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- );
- }}
- disabled={didConfirm}
- interactive={!isReadOnly}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''}
- />
- )}
- {isDistanceRequest && (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(
- CONST.IOU.ACTION.EDIT,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- disabled={didConfirm}
- interactive={!isReadOnly}
- />
- )}
- {shouldShowMerchant && (
- {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(
- CONST.IOU.ACTION.EDIT,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- );
- }}
- disabled={didConfirm}
- interactive={!isReadOnly}
- brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- error={shouldDisplayMerchantError ? translate('common.error.enterMerchant') : ''}
- />
- )}
- {shouldShowCategories && (
- {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
- CONST.IOU.ACTION.EDIT,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- reportActionID,
- ),
- );
- }}
- style={styles.moneyRequestMenuItem}
- titleStyle={styles.flex1}
- disabled={didConfirm}
- interactive={!isReadOnly}
- rightLabel={canUseViolations && !!policy?.requiresCategory ? translate('common.required') : ''}
- />
- )}
- {shouldShowTags &&
- policyTagLists.map(({name}, index) => (
- {
- if (!isEditingSplitBill) {
- return;
- }
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(
- CONST.IOU.ACTION.EDIT,
- CONST.IOU.TYPE.SPLIT,
- index,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- reportActionID,
- ),
- );
- }}
- style={styles.moneyRequestMenuItem}
- disabled={didConfirm}
- interactive={!isReadOnly}
- rightLabel={canUseViolations && !!policy?.requiresTag ? translate('common.required') : ''}
- />
- ))}
-
- {shouldShowTax && (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- disabled={didConfirm}
- interactive={!isReadOnly}
- />
- )}
-
- {shouldShowTax && (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- disabled={didConfirm}
- interactive={!isReadOnly}
- />
- )}
-
- {shouldShowBillable && (
-
- {translate('common.billable')}
- onToggleBillable?.(isOn)}
- />
-
- )}
- >
- )}
+ {shouldShowAllFields && supplementaryFields}
+
);
}
@@ -900,7 +993,4 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
},
- iou: {
- key: ONYXKEYS.IOU,
- },
})(MoneyRequestConfirmationList);
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index f451f5f15581..da087c57fcba 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -63,7 +63,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
const isOnHold = TransactionUtils.isOnHold(transaction);
const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
- // Only the requestor can take delete the request, admins can only edit it.
+ // Only the requestor can take delete the expense, admins can only edit it.
const isActionOwner = typeof parentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && parentReportAction.actorAccountID === session?.accountID;
const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID;
@@ -96,8 +96,12 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
if (isOnHold) {
IOU.unholdRequest(iouTransactionID, report?.reportID);
} else {
+ if (!policy?.type) {
+ return;
+ }
+
const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams());
- Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? '', iouTransactionID, report?.reportID, activeRoute));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy.type, iouTransactionID, report?.reportID, activeRoute));
}
};
@@ -118,14 +122,14 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
if (isOnHold && (isHoldCreator || (!isRequestIOU && canModifyStatus))) {
threeDotsMenuItems.push({
icon: Expensicons.Stopwatch,
- text: translate('iou.unholdRequest'),
+ text: translate('iou.unholdExpense'),
onSelected: () => changeMoneyRequestStatus(),
});
}
if (!isOnHold && (isRequestIOU || canModifyStatus)) {
threeDotsMenuItems.push({
icon: Expensicons.Stopwatch,
- text: translate('iou.holdRequest'),
+ text: translate('iou.holdExpense'),
onSelected: () => changeMoneyRequestStatus(),
});
}
@@ -196,7 +200,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
{isOnHold && }
setIsDeleteModalVisible(false)}
diff --git a/src/components/MoneyRequestSkeletonView.tsx b/src/components/MoneyRequestSkeletonView.tsx
index e11e7bcecc07..400e3c9534d7 100644
--- a/src/components/MoneyRequestSkeletonView.tsx
+++ b/src/components/MoneyRequestSkeletonView.tsx
@@ -13,8 +13,8 @@ function MoneyRequestSkeletonView() {
animate
width={styles.w100.width}
height={variables.moneyRequestSkeletonHeight}
- backgroundColor={theme.borderLighter}
- foregroundColor={theme.border}
+ backgroundColor={theme.skeletonLHNIn}
+ foregroundColor={theme.skeletonLHNOut}
>
;
-
- /** Collection of tags attached to a policy */
- policyTags: OnyxEntry;
-
- /** The policy of the report */
- policy: OnyxEntry;
-
- /** The session of the logged in user */
- session: OnyxEntry;
-
- /** Unit and rate used for if the money request is a distance request */
- mileageRate: OnyxEntry;
-};
-
-type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & {
- /** Callback to inform parent modal of success */
- onConfirm?: (selectedParticipants: Participant[]) => void;
-
- /** Callback to parent modal to send money */
- onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void;
-
- /** Callback to inform a participant is selected */
- onSelectParticipant?: (option: Participant) => void;
-
- /** Should we request a single or multiple participant selection from user */
- hasMultipleParticipants: boolean;
-
- /** IOU amount */
- iouAmount: number;
-
- /** IOU comment */
- iouComment?: string;
-
- /** IOU currency */
- iouCurrencyCode?: string;
-
- /** IOU type */
- iouType?: ValueOf;
-
- /** IOU date */
- iouCreated?: string;
-
- /** IOU merchant */
- iouMerchant?: string;
-
- /** IOU Category */
- iouCategory?: string;
-
- /** IOU isBillable */
- iouIsBillable?: boolean;
-
- /** Callback to toggle the billable state */
- onToggleBillable?: (isOn: boolean) => void;
-
- /** Selected participants from MoneyRequestModal with login / accountID */
- selectedParticipants: Participant[];
-
- /** Payee of the money request with login */
- payeePersonalDetails?: OnyxTypes.PersonalDetails;
-
- /** Can the participants be modified or not */
- canModifyParticipants?: boolean;
-
- /** Should the list be read only, and not editable? */
- isReadOnly?: boolean;
-
- /** Depending on expense report or personal IOU report, respective bank account route */
- bankAccountRoute?: Route;
-
- /** The policyID of the request */
- policyID?: string;
-
- /** The reportID of the request */
- reportID?: string;
-
- /** File path of the receipt */
- receiptPath?: string;
-
- /** File name of the receipt */
- receiptFilename?: string;
-
- /** List styles for OptionsSelector */
- listStyles?: StyleProp;
-
- /** Transaction that represents the money request */
- transaction?: OnyxEntry;
-
- /** Whether the money request is a distance request */
- isDistanceRequest?: boolean;
-
- /** Whether the money request is a scan request */
- isScanRequest?: boolean;
-
- /** Whether we're editing a split bill */
- isEditingSplitBill?: boolean;
-
- /** Whether we should show the amount, date, and merchant fields. */
- shouldShowSmartScanFields?: boolean;
-
- /** A flag for verifying that the current report is a sub-report of a workspace chat */
- isPolicyExpenseChat?: boolean;
-
- /** Whether smart scan failed */
- hasSmartScanFailed?: boolean;
-
- reportActionID?: string;
-};
-
-const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) || '';
- return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0);
-};
-
-function MoneyTemporaryForRefactorRequestConfirmationList({
- transaction = null,
- onSendMoney,
- onConfirm,
- onSelectParticipant,
- iouType = CONST.IOU.TYPE.REQUEST,
- isScanRequest = false,
- iouAmount,
- policyCategories,
- mileageRate,
- isDistanceRequest = false,
- policy,
- isPolicyExpenseChat = false,
- iouCategory = '',
- shouldShowSmartScanFields = true,
- isEditingSplitBill,
- policyTags,
- iouCurrencyCode,
- iouMerchant,
- hasMultipleParticipants,
- selectedParticipants: pickedParticipants,
- payeePersonalDetails,
- canModifyParticipants = false,
- session,
- isReadOnly = false,
- bankAccountRoute = '',
- policyID = '',
- reportID = '',
- receiptPath = '',
- iouComment,
- receiptFilename = '',
- listStyles,
- iouCreated,
- iouIsBillable = false,
- onToggleBillable,
- hasSmartScanFailed,
- reportActionID,
-}: MoneyRequestConfirmationListProps) {
- const theme = useTheme();
- const styles = useThemeStyles();
- const {translate, toLocaleDigit} = useLocalize();
- const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const {canUseViolations} = usePermissions();
-
- const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
- const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT;
- const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
- const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE;
-
- const {unit, rate, currency} = mileageRate ?? {
- unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES,
- rate: 0,
- currency: 'USD',
- };
- const distance = transaction?.routes?.route0.distance ?? 0;
- const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0;
- const taxRates = policy?.taxRates;
-
- // A flag for showing the categories field
- const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {})));
-
- // A flag and a toggler for showing the rest of the form fields
- const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false);
-
- // Do not hide fields in case of send money request
- const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill;
-
- const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend;
- const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend;
-
- const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
-
- // A flag for showing the tags field
- const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);
-
- // A flag for showing tax rate
- const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy);
-
- // A flag for showing the billable field
- const shouldShowBillable = policy?.disabledFields?.defaultBillable === false;
-
- const hasRoute = TransactionUtils.hasRoute(transaction);
- const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate);
- const formattedAmount = isDistanceRequestWithPendingRoute
- ? ''
- : CurrencyUtils.convertToDisplayString(
- shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount,
- isDistanceRequest ? currency : iouCurrencyCode,
- );
- const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode);
- const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : '';
-
- const previousTransactionAmount = usePrevious(transaction?.amount);
-
- const isFocused = useIsFocused();
- const [formError, setFormError] = useState('');
-
- const [didConfirm, setDidConfirm] = useState(false);
- const [didConfirmSplit, setDidConfirmSplit] = useState(false);
-
- const [merchantError, setMerchantError] = useState(false);
-
- const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
-
- const navigateBack = () => {
- Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID));
- };
-
- const shouldDisplayFieldError: boolean = useMemo(() => {
- if (!isEditingSplitBill) {
- return false;
- }
-
- return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction));
- }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]);
-
- const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
- const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant;
-
- const isCategoryRequired = canUseViolations && !!policy?.requiresCategory;
-
- useEffect(() => {
- if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) {
- return;
- }
- if (!isMerchantEmpty && merchantError) {
- setMerchantError(false);
- if (formError === 'iou.error.invalidMerchant') {
- setFormError('');
- }
- }
- }, [formError, isMerchantEmpty, merchantError, isMerchantRequired]);
-
- useEffect(() => {
- if (shouldDisplayFieldError && hasSmartScanFailed) {
- setFormError('iou.receiptScanningFailed');
- return;
- }
- if (shouldDisplayFieldError && didConfirmSplit) {
- setFormError('iou.error.genericSmartscanFailureMessage');
- return;
- }
- if (merchantError) {
- setFormError('iou.error.invalidMerchant');
- return;
- }
- // reset the form error whenever the screen gains or loses focus
- setFormError('');
- }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]);
-
- useEffect(() => {
- if (!shouldCalculateDistanceAmount) {
- return;
- }
-
- const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0);
- IOU.setMoneyRequestAmount_temporaryForRefactor(transaction?.transactionID ?? '', amount, currency ?? '');
- }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]);
-
- // Calculate and set tax amount in transaction draft
- useEffect(() => {
- const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString();
- const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));
-
- if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) {
- return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true);
- }
-
- IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', amountInSmallestCurrencyUnits, true);
- }, [taxRates?.defaultValue, transaction, previousTransactionAmount]);
-
- /**
- * Returns the participants with amount
- */
- const getParticipantsWithAmount = useCallback(
- (participantsList: Participant[]) => {
- const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? '');
- return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '');
- },
- [iouAmount, iouCurrencyCode],
- );
-
- // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again
- if (isEditingSplitBill && didConfirm) {
- setDidConfirm(false);
- }
-
- const splitOrRequestOptions: Array> = useMemo(() => {
- let text;
- if (isTypeTrackExpense) {
- text = translate('iou.trackExpense');
- } else if (isTypeSplit && iouAmount === 0) {
- text = translate('iou.split');
- } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) {
- text = translate('iou.request');
- if (iouAmount !== 0) {
- text = translate('iou.requestAmount', {amount: formattedAmount});
- }
- } else {
- const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.requestAmount';
- text = translate(translationKey, {amount: formattedAmount});
- }
- return [
- {
- text: text[0].toUpperCase() + text.slice(1),
- value: iouType,
- },
- ];
- }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]);
-
- const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]);
- const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]);
- const userCanModifyParticipants = useRef(!isReadOnly && canModifyParticipants && hasMultipleParticipants);
- useEffect(() => {
- userCanModifyParticipants.current = !isReadOnly && canModifyParticipants && hasMultipleParticipants;
- }, [isReadOnly, canModifyParticipants, hasMultipleParticipants]);
- const shouldDisablePaidBySection = userCanModifyParticipants.current;
-
- const optionSelectorSections = useMemo(() => {
- const sections = [];
- const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected);
- if (hasMultipleParticipants) {
- const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants);
- let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])];
-
- if (!canModifyParticipants) {
- formattedParticipantsList = formattedParticipantsList.map((participant) => ({
- ...participant,
- isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
- }));
- }
-
- const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true);
- const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(
- personalDetailsOfPayee,
- iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '',
- );
-
- sections.push(
- {
- title: translate('moneyRequestConfirmationList.paidBy'),
- data: [formattedPayeeOption],
- shouldShow: true,
- isDisabled: shouldDisablePaidBySection,
- },
- {
- title: translate('moneyRequestConfirmationList.splitWith'),
- data: formattedParticipantsList,
- shouldShow: true,
- },
- );
- } else {
- const formattedSelectedParticipants = selectedParticipants.map((participant) => ({
- ...participant,
- isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
- }));
- sections.push({
- title: translate('common.to'),
- data: formattedSelectedParticipants,
- shouldShow: true,
- });
- }
- return sections;
- }, [
- selectedParticipants,
- pickedParticipants,
- hasMultipleParticipants,
- iouAmount,
- iouCurrencyCode,
- getParticipantsWithAmount,
- personalDetailsOfPayee,
- translate,
- shouldDisablePaidBySection,
- canModifyParticipants,
- ]);
-
- const selectedOptions = useMemo(() => {
- if (!hasMultipleParticipants) {
- return [];
- }
- return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee)];
- }, [selectedParticipants, hasMultipleParticipants, personalDetailsOfPayee]);
-
- useEffect(() => {
- if (!isDistanceRequest) {
- return;
- }
-
- /*
- Set pending waypoints based on the route status. We should handle this dynamically to cover cases such as:
- When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page.
- In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending.
- */
- IOU.setMoneyRequestPendingFields(transaction?.transactionID ?? '', {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
-
- const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? 'USD', translate, toLocaleDigit);
- IOU.setMoneyRequestMerchant(transaction?.transactionID ?? '', distanceMerchant, true);
- }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]);
-
- // 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) {
- return;
- }
- IOU.setMoneyRequestCategory(transaction?.transactionID ?? '', enabledCategories[0].name);
- }, [iouCategory, shouldShowCategories, policyCategories, transaction, isCategoryRequired]);
-
- // Auto select the tag if there is only one enabled tag and it is required
- useEffect(() => {
- let updatedTagsString = TransactionUtils.getTag(transaction);
- policyTagLists.forEach((tagList, index) => {
- const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled);
- const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations;
- if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) {
- return;
- }
- updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index);
- });
- if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) {
- IOU.setMoneyRequestTag(transaction?.transactionID ?? '', updatedTagsString);
- }
- }, [policyTagLists, transaction, policyTags, canUseViolations]);
-
- /**
- */
- const selectParticipant = useCallback(
- (option: Participant) => {
- // Return early if selected option is currently logged in user.
- if (option.accountID === session?.accountID) {
- return;
- }
- onSelectParticipant?.(option);
- },
- [session?.accountID, onSelectParticipant],
- );
-
- /**
- * Navigate to report details or profile of selected user
- */
- const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => {
- const activeRoute = Navigation.getActiveRouteWithoutParams();
-
- if (option.isSelfDM) {
- Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute));
- return;
- }
-
- if (option.accountID) {
- Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute));
- } else if (option.reportID) {
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute));
- }
- };
-
- /**
- * @param {String} paymentMethod
- */
- const confirm = useCallback(
- (paymentMethod: PaymentMethodType | undefined) => {
- if (selectedParticipants.length === 0) {
- return;
- }
- if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) {
- setMerchantError(true);
- return;
- }
-
- if (iouType === CONST.IOU.TYPE.SEND) {
- if (!paymentMethod) {
- return;
- }
-
- setDidConfirm(true);
-
- Log.info(`[IOU] Sending money via: ${paymentMethod}`);
- onSendMoney?.(paymentMethod);
- } else {
- // validate the amount for distance requests
- const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode);
- if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) {
- setFormError('common.error.invalidAmount');
- return;
- }
-
- if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) {
- setDidConfirmSplit(true);
- setFormError('iou.error.genericSmartscanFailureMessage');
- return;
- }
-
- playSound(SOUNDS.DONE);
- setDidConfirm(true);
- onConfirm?.(selectedParticipants);
- }
- },
- [
- selectedParticipants,
- isMerchantRequired,
- isMerchantEmpty,
- shouldDisplayFieldError,
- transaction,
- iouType,
- onSendMoney,
- iouCurrencyCode,
- isDistanceRequest,
- isDistanceRequestWithPendingRoute,
- iouAmount,
- isEditingSplitBill,
- onConfirm,
- ],
- );
-
- const footerContent = useMemo(() => {
- if (isReadOnly) {
- return;
- }
-
- const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND;
- const shouldDisableButton = selectedParticipants.length === 0;
-
- const button = shouldShowSettlementButton ? (
-
- ) : (
- confirm(value as PaymentMethodType)}
- options={splitOrRequestOptions}
- buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
- enterKeyEventListenerPriority={1}
- />
- );
-
- return (
- <>
- {!!formError && (
-
- )}
-
- {button}
- >
- );
- }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]);
-
- // An intermediate structure that helps us classify the fields as "primary" and "supplementary".
- // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones.
- const classifiedFields = [
- {
- item: (
- {
- if (isDistanceRequest) {
- return;
- }
- if (isEditingSplitBill) {
- Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT));
- return;
- }
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
- );
- }}
- style={[styles.moneyRequestMenuItem, styles.mt2]}
- titleStyle={styles.moneyRequestConfirmationAmount}
- disabled={didConfirm}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''}
- />
- ),
- shouldShow: shouldShowSmartScanFields,
- isSupplementary: false,
- },
- {
- item: (
- {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- );
- }}
- style={[styles.moneyRequestMenuItem]}
- titleStyle={styles.flex1}
- disabled={didConfirm}
- interactive={!isReadOnly}
- numberOfLinesTitle={2}
- />
- ),
- shouldShow: true,
- isSupplementary: false,
- },
- {
- item: (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- disabled={didConfirm}
- interactive={!isReadOnly}
- />
- ),
- shouldShow: isDistanceRequest,
- isSupplementary: true,
- },
- {
- item: (
- {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- );
- }}
- disabled={didConfirm}
- interactive={!isReadOnly}
- brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- error={merchantError ? translate('common.error.fieldRequired') : ''}
- rightLabel={isMerchantRequired ? translate('common.required') : ''}
- />
- ),
- shouldShow: shouldShowMerchant,
- isSupplementary: !isMerchantRequired,
- },
- {
- item: (
- {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
- );
- }}
- disabled={didConfirm}
- interactive={!isReadOnly}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''}
- />
- ),
- shouldShow: shouldShowDate,
- isSupplementary: true,
- },
- {
- item: (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- style={[styles.moneyRequestMenuItem]}
- titleStyle={styles.flex1}
- disabled={didConfirm}
- interactive={!isReadOnly}
- rightLabel={isCategoryRequired ? translate('common.required') : ''}
- />
- ),
- shouldShow: shouldShowCategories,
- isSupplementary: !isCategoryRequired,
- },
- ...policyTagLists.map(({name, required}, index) => {
- const isTagRequired = required === undefined ? false : canUseViolations && required;
- return {
- item: (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- index,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- style={[styles.moneyRequestMenuItem]}
- disabled={didConfirm}
- interactive={!isReadOnly}
- rightLabel={isTagRequired ? translate('common.required') : ''}
- />
- ),
- shouldShow: shouldShowTags,
- isSupplementary: !isTagRequired,
- };
- }),
- {
- item: (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- disabled={didConfirm}
- interactive={!isReadOnly}
- />
- ),
- shouldShow: shouldShowTax,
- isSupplementary: true,
- },
- {
- item: (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- disabled={didConfirm}
- interactive={!isReadOnly}
- />
- ),
- shouldShow: shouldShowTax,
- isSupplementary: true,
- },
- {
- item: (
-
- {translate('common.billable')}
- onToggleBillable?.(isOn)}
- />
-
- ),
- shouldShow: shouldShowBillable,
- isSupplementary: true,
- },
- ];
-
- const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item);
-
- const supplementaryFields = classifiedFields
- .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary)
- .map((supplementaryField) => supplementaryField.item);
-
- const {
- image: receiptImage,
- thumbnail: receiptThumbnail,
- isThumbnail,
- fileExtension,
- isLocalFile,
- } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI);
-
- const receiptThumbnailContent = useMemo(
- () =>
- isLocalFile && Str.isPDF(receiptFilename) ? (
- setIsAttachmentInvalid(true)}
- />
- ) : (
-
- ),
- [isLocalFile, receiptFilename, receiptImage, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, receiptThumbnail, fileExtension],
- );
-
- return (
- // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q)
-
- {isDistanceRequest && (
-
-
-
- )}
- {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- receiptImage || receiptThumbnail
- ? receiptThumbnailContent
- : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
- PolicyUtils.isPaidGroupPolicy(policy) &&
- !isDistanceRequest &&
- iouType === CONST.IOU.TYPE.REQUEST && (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
- CONST.IOU.ACTION.CREATE,
- iouType,
- transaction?.transactionID ?? '',
- reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
- )
- }
- />
- )
- }
- {primaryFields}
- {!shouldShowAllFields && (
-
-
-
-
-
- )}
- {shouldShowAllFields && supplementaryFields}
-
-
- );
-}
-
-MoneyTemporaryForRefactorRequestConfirmationList.displayName = 'MoneyTemporaryForRefactorRequestConfirmationList';
-
-export default withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
- policyCategories: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
- },
- policyTags: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
- },
- mileageRate: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- selector: DistanceRequestUtils.getDefaultMileageRate,
- },
- policy: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- },
-})(MoneyTemporaryForRefactorRequestConfirmationList);
diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx
index 436f4c147931..7bbd3e344c3f 100644
--- a/src/components/OptionsList/BaseOptionsList.tsx
+++ b/src/components/OptionsList/BaseOptionsList.tsx
@@ -184,7 +184,7 @@ function BaseOptionsList(
option={item}
showTitleTooltip={showTitleTooltip}
hoverStyle={optionHoveredStyle}
- optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset}
+ optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + (section.indexOffset ?? 0)}
onSelectRow={onSelectRow}
isSelected={isSelected}
showSelectedState={canSelectMultipleOptions}
diff --git a/src/components/OptionsList/types.ts b/src/components/OptionsList/types.ts
index b7180e6281b4..7f23da965f39 100644
--- a/src/components/OptionsList/types.ts
+++ b/src/components/OptionsList/types.ts
@@ -22,7 +22,7 @@ type Section = {
type SectionWithIndexOffset = Section & {
/** The initial index of this section given the total number of options in each section's data array */
- indexOffset: number;
+ indexOffset?: number;
};
type OptionsListProps = {
diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx
index 0ad32f18659b..cbc9e1352f21 100644
--- a/src/components/ParentNavigationSubtitle.tsx
+++ b/src/components/ParentNavigationSubtitle.tsx
@@ -35,7 +35,12 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct
onPress={() => {
const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '');
const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '');
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, isVisibleAction && !isOffline ? parentReportActionID : undefined));
+ // Pop the thread report screen before navigating to the chat report.
+ Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID));
+ if (isVisibleAction && !isOffline) {
+ // Pop the chat report screen before navigating to the linked report action.
+ Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, parentReportActionID));
+ }
}}
accessibilityLabel={translate('threads.parentNavigationSummary', {reportName, workspaceName})}
role={CONST.ROLE.LINK}
diff --git a/src/components/ReportActionItem/ActionableItemButtons.tsx b/src/components/ReportActionItem/ActionableItemButtons.tsx
index 6ead20d3e643..8e5590537f51 100644
--- a/src/components/ReportActionItem/ActionableItemButtons.tsx
+++ b/src/components/ReportActionItem/ActionableItemButtons.tsx
@@ -10,10 +10,12 @@ type ActionableItem = {
key: string;
onPress: () => void;
text: TranslationPaths;
+ isMediumSized?: boolean;
};
type ActionableItemButtonsProps = {
items: ActionableItem[];
+ layout?: 'horizontal' | 'vertical';
};
function ActionableItemButtons(props: ActionableItemButtonsProps) {
@@ -21,14 +23,14 @@ function ActionableItemButtons(props: ActionableItemButtonsProps) {
const {translate} = useLocalize();
return (
-
+
{props.items?.map((item) => (
))}
diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx
index 7d9ba2697c7a..4f91b2084b45 100644
--- a/src/components/ReportActionItem/MoneyRequestAction.tsx
+++ b/src/components/ReportActionItem/MoneyRequestAction.tsx
@@ -37,7 +37,7 @@ type MoneyRequestActionProps = MoneyRequestActionOnyxProps & {
/** The ID of the associated chatReport */
chatReportID: string;
- /** The ID of the associated request report */
+ /** The ID of the associated expense report */
requestReportID: string;
/** The ID of the current report */
@@ -114,10 +114,8 @@ function MoneyRequestAction({
let message: TranslationPaths;
if (isReversedTransaction) {
message = 'parentReportAction.reversedTransaction';
- } else if (isTrackExpenseAction) {
- message = 'parentReportAction.deletedExpense';
} else {
- message = 'parentReportAction.deletedRequest';
+ message = 'parentReportAction.deletedExpense';
}
return ${translate(message)}`} />;
}
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index 97287e64b829..8994d456904a 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -101,7 +101,7 @@ function MoneyRequestPreviewContent({
/*
Show the merchant for IOUs and expenses only if:
- the merchant is not empty, is custom, or is not related to scanning smartscan;
- - the request is not a distance request with a pending route and amount = 0 - in this case,
+ - the expense is not a distance expense with a pending route and amount = 0 - in this case,
the merchant says: "Route pending...", which is already shown in the amount field;
*/
const shouldShowMerchant =
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts
index 3b3eda4ec30a..0e3eb37ce6e3 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts
+++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts
@@ -53,7 +53,7 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & {
/** Extra styles to pass to View wrapper */
containerStyles?: StyleProp;
- /** True if this is this IOU is a split instead of a 1:1 request */
+ /** True if this IOU has a type of split */
isBillSplit: boolean;
/** Whether this IOU is a track expense */
@@ -62,7 +62,7 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & {
/** True if the IOU Preview card is hovered */
isHovered?: boolean;
- /** Whether or not an IOU report contains money requests in a different currency
+ /** Whether or not an IOU report contains expenses in a different currency
* that are either created or cancelled offline, and thus haven't been converted to the report's currency yet
*/
shouldShowPendingConversionMessage?: boolean;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 73fc7e9bae6e..c5cad0eccdeb 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -132,7 +132,7 @@ function MoneyRequestView({
? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction)
: transactionTaxCode && TransactionUtils.getTaxName(taxRates?.taxes, transactionTaxCode));
- // Flags for allowing or disallowing editing a money request
+ // Flags for allowing or disallowing editing an expense
const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID);
const isCancelled = moneyRequestReport && moneyRequestReport.isCancelledIOU;
@@ -220,7 +220,7 @@ function MoneyRequestView({
const getErrorForField = useCallback(
(field: ViolationField, data?: OnyxTypes.TransactionViolation['data']) => {
- // Checks applied when creating a new money request
+ // Checks applied when creating a new expense
// NOTE: receipt field can return multiple violations, so we need to handle it separately
const fieldChecks: Partial> = {
amount: {
diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx
index ee8cb0849ca0..e2bcce9b9f1b 100644
--- a/src/components/ReportActionItem/ReportActionItemImages.tsx
+++ b/src/components/ReportActionItem/ReportActionItemImages.tsx
@@ -30,7 +30,7 @@ type ReportActionItemImagesProps = {
/**
* This component displays a row of images in a report action item like a card, such
- * as report previews or money request previews which contain receipt images. The maximum of images
+ * as report previews or expense previews which contain receipt images. The maximum of images
* shown in this row is dictated by the size prop, which, if not passed, is just the number of images.
* Otherwise, if size is passed and the number of images is over size, we show a small overlay on the
* last image of how many additional images there are. If passed, total prop can be used to change how this
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 190343e48abd..d14d2df1bb43 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -36,7 +36,7 @@ import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import ReportActionItemImages from './ReportActionItemImages';
type ReportPreviewOnyxProps = {
- /** The policy tied to the money request report */
+ /** The policy tied to the expense report */
policy: OnyxEntry;
/** ChatReport associated with iouReport */
@@ -216,11 +216,11 @@ function ReportPreview({
const shouldShowRBR = !iouSettled && hasErrors;
/*
- Show subtitle if at least one of the money requests is not being smart scanned, and either:
- - There is more than one money request – in this case, the "X requests, Y scanning" subtitle is shown;
- - There is only one money request, it has a receipt and is not being smart scanned – in this case, the request merchant or description is shown;
+ Show subtitle if at least one of the expenses is not being smart scanned, and either:
+ - There is more than one expense – in this case, the "X expenses, Y scanning" subtitle is shown;
+ - There is only one expense, it has a receipt and is not being smart scanned – in this case, the expense merchant or description is shown;
- * There is an edge case when there is only one distance request with a pending route and amount = 0.
+ * There is an edge case when there is only one distance expense with a pending route and amount = 0.
In this case, we don't want to show the merchant or description because it says: "Pending route...", which is already displayed in the amount field.
*/
const shouldShowSingleRequestMerchantOrDescription =
@@ -237,7 +237,7 @@ function ReportPreview({
}
return {
isSupportTextHtml: false,
- supportText: translate('iou.requestCount', {
+ supportText: translate('iou.expenseCount', {
count: numberOfRequests - numberOfScanningReceipts - numberOfPendingRequests,
scanningReceipts: numberOfScanningReceipts,
pendingReceipts: numberOfPendingRequests,
diff --git a/src/components/ReportActionItem/TaskAction.tsx b/src/components/ReportActionItem/TaskAction.tsx
index e85a2e708feb..e1b36713592f 100644
--- a/src/components/ReportActionItem/TaskAction.tsx
+++ b/src/components/ReportActionItem/TaskAction.tsx
@@ -1,3 +1,4 @@
+import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
@@ -15,10 +16,15 @@ type TaskActionProps = {
function TaskAction({action}: TaskActionProps) {
const styles = useThemeStyles();
const message = TaskUtils.getTaskReportActionMessage(action);
+ const parser = new ExpensiMark();
return (
- {message.html ? ${message.html}`} /> : {message.text}}
+ {message.html ? (
+ ${parser.replace(message.html)}`} />
+ ) : (
+ {message.text}
+ )}
);
}
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 5f4438f18f60..9e6fb31d0316 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -28,7 +28,9 @@ function BaseListItem({
FooterComponent,
children,
isFocused,
+ shouldSyncFocus = true,
onFocus = () => {},
+ hoverStyle,
}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -37,7 +39,7 @@ function BaseListItem({
const pressableRef = useRef(null);
// Sync focus on an item
- useSyncFocus(pressableRef, Boolean(isFocused));
+ useSyncFocus(pressableRef, Boolean(isFocused && shouldSyncFocus));
const rightHandSideComponentRender = () => {
if (canSelectMultiple || !rightHandSideComponent) {
@@ -68,7 +70,7 @@ function BaseListItem({
accessibilityLabel={item.text ?? ''}
role={CONST.ROLE.BUTTON}
hoverDimmingValue={1}
- hoverStyle={!item.isDisabled && !item.isSelected && styles.hoveredComponentBG}
+ hoverStyle={[!item.isDisabled && styles.hoveredComponentBG, hoverStyle]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined}
nativeID={keyForList ?? ''}
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 62f098e76228..b24fe5351fad 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -2,7 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native';
import isEmpty from 'lodash/isEmpty';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
-import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native';
+import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListData, SectionListRenderItemInfo} from 'react-native';
import {View} from 'react-native';
import Button from '@components/Button';
import Checkbox from '@components/Checkbox';
@@ -52,6 +52,7 @@ function BaseSelectionList(
onConfirm,
headerContent,
footerContent,
+ listFooterContent,
showScrollIndicator = true,
showLoadingPlaceholder = false,
showConfirmButton = false,
@@ -92,6 +93,7 @@ function BaseSelectionList(
const [itemsToHighlight, setItemsToHighlight] = useState | null>(null);
const itemFocusTimeoutRef = useRef(null);
const [currentPage, setCurrentPage] = useState(1);
+ const isTextInputFocusedRef = useRef(false);
const incrementPage = () => setCurrentPage((prev) => prev + 1);
@@ -294,7 +296,7 @@ function BaseSelectionList(
*
* [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}]
*/
- const getItemLayout = (data: Array> | null, flatDataArrayIndex: number) => {
+ const getItemLayout = (data: Array>> | null, flatDataArrayIndex: number) => {
const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex];
if (!targetItem) {
@@ -313,6 +315,10 @@ function BaseSelectionList(
};
const renderSectionHeader = ({section}: {section: SectionListDataType}) => {
+ if (section.CustomSectionHeader) {
+ return ;
+ }
+
if (!section.title || isEmptyObject(section.data)) {
return null;
}
@@ -329,7 +335,7 @@ function BaseSelectionList(
};
const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => {
- const normalizedIndex = index + section.indexOffset;
+ const normalizedIndex = index + (section?.indexOffset ?? 0);
const isDisabled = !!section.isDisabled || item.isDisabled;
const isItemFocused = !isDisabled && (focusedIndex === normalizedIndex || itemsToHighlight?.has(item.keyForList ?? ''));
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
@@ -349,7 +355,8 @@ function BaseSelectionList(
rightHandSideComponent={rightHandSideComponent}
keyForList={item.keyForList ?? ''}
isMultilineSupported={isRowMultilineSupported}
- onFocus={() => setFocusedIndex(index)}
+ onFocus={() => setFocusedIndex(normalizedIndex)}
+ shouldSyncFocus={!isTextInputFocusedRef.current}
/>
);
};
@@ -522,6 +529,8 @@ function BaseSelectionList(
textInputRef.current = element as RNTextInput;
}
}}
+ onFocus={() => (isTextInputFocusedRef.current = true)}
+ onBlur={() => (isTextInputFocusedRef.current = false)}
label={textInputLabel}
accessibilityLabel={textInputLabel}
hint={textInputHint}
@@ -603,7 +612,7 @@ function BaseSelectionList(
testID="selection-list"
onLayout={onSectionListLayout}
style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0}
- ListFooterComponent={ShowMoreButtonInstance}
+ ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance}
/>
{children}
>
diff --git a/src/components/SelectionList/InviteMemberListItem.tsx b/src/components/SelectionList/InviteMemberListItem.tsx
index 03a27c88fa68..69774e24b970 100644
--- a/src/components/SelectionList/InviteMemberListItem.tsx
+++ b/src/components/SelectionList/InviteMemberListItem.tsx
@@ -26,6 +26,8 @@ function InviteMemberListItem({
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
+ onFocus,
+ shouldSyncFocus,
}: InviteMemberListItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -66,6 +68,8 @@ function InviteMemberListItem({
) : undefined
}
keyForList={item.keyForList}
+ onFocus={onFocus}
+ shouldSyncFocus={shouldSyncFocus}
>
{(hovered?: boolean) => (
<>
diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx
index e26926e75e13..7ad4819b9690 100644
--- a/src/components/SelectionList/RadioListItem.tsx
+++ b/src/components/SelectionList/RadioListItem.tsx
@@ -17,6 +17,7 @@ function RadioListItem({
rightHandSideComponent,
isMultilineSupported = false,
onFocus,
+ shouldSyncFocus,
}: RadioListItemProps) {
const styles = useThemeStyles();
const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text;
@@ -36,6 +37,7 @@ function RadioListItem({
rightHandSideComponent={rightHandSideComponent}
keyForList={item.keyForList}
onFocus={onFocus}
+ shouldSyncFocus={shouldSyncFocus}
>
<>
diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx
index c2680c92780a..d07d658f6b12 100644
--- a/src/components/SelectionList/TableListItem.tsx
+++ b/src/components/SelectionList/TableListItem.tsx
@@ -24,6 +24,7 @@ function TableListItem({
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
onFocus,
+ shouldSyncFocus,
}: TableListItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -58,6 +59,8 @@ function TableListItem({
pendingAction={item.pendingAction}
keyForList={item.keyForList}
onFocus={onFocus}
+ shouldSyncFocus={shouldSyncFocus}
+ hoverStyle={item.isSelected && styles.activeComponentBG}
>
{(hovered) => (
<>
diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx
index 940828ebcac3..68349293e134 100644
--- a/src/components/SelectionList/UserListItem.tsx
+++ b/src/components/SelectionList/UserListItem.tsx
@@ -27,6 +27,8 @@ function UserListItem({
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
+ onFocus,
+ shouldSyncFocus,
}: UserListItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -67,6 +69,8 @@ function UserListItem({
) : undefined
}
keyForList={item.keyForList}
+ onFocus={onFocus}
+ shouldSyncFocus={shouldSyncFocus}
>
{(hovered?: boolean) => (
<>
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index d89f4d5b92f3..a96d6c3abb17 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -12,6 +12,11 @@ import type RadioListItem from './RadioListItem';
import type TableListItem from './TableListItem';
import type UserListItem from './UserListItem';
+type TRightHandSideComponent = {
+ /** Component to display on the right side */
+ rightHandSideComponent?: ((item: TItem) => ReactElement | null | undefined) | ReactElement | null;
+};
+
type CommonListItemProps = {
/** Whether this item is focused (for arrow key controls) */
isFocused?: boolean;
@@ -34,9 +39,6 @@ type CommonListItemProps = {
/** Callback to fire when an error is dismissed */
onDismissError?: (item: TItem) => void;
- /** Component to display on the right side */
- rightHandSideComponent?: ((item: TItem) => ReactElement | null) | ReactElement | null;
-
/** Styles for the pressable component */
pressableStyle?: StyleProp;
@@ -54,7 +56,7 @@ type CommonListItemProps = {
/** Handles what to do when the item is focused */
onFocus?: () => void;
-};
+} & TRightHandSideComponent;
type ListItem = {
/** Text to display */
@@ -137,6 +139,12 @@ type ListItemProps = CommonListItemProps & {
/** Key used internally by React */
keyForList?: string;
+
+ /**
+ * Whether the focus on the element should be synchronized. For example it should be set to false when the text input above list items is currently focused.
+ * When we type something into the text input, the first element found is focused, in this situation we should not synchronize the focus on the element because we will lose the focus from the text input.
+ */
+ shouldSyncFocus?: boolean;
};
type BaseListItemProps = CommonListItemProps & {
@@ -147,6 +155,8 @@ type BaseListItemProps = CommonListItemProps & {
pendingAction?: PendingAction | null;
FooterComponent?: ReactElement;
children?: ReactElement> | ((hovered: boolean) => ReactElement>);
+ shouldSyncFocus?: boolean;
+ hoverStyle?: StyleProp;
};
type UserListItemProps = ListItemProps & {
@@ -184,12 +194,12 @@ type Section = {
type SectionWithIndexOffset = Section & {
/** The initial index of this section given the total number of options in each section's data array */
- indexOffset: number;
+ indexOffset?: number;
};
type BaseSelectionListProps = Partial & {
/** Sections for the section list */
- sections: Array>> | typeof CONST.EMPTY_ARRAY;
+ sections: Array> | typeof CONST.EMPTY_ARRAY;
/** Default renderer for every item in the list */
ListItem: ValidListItem;
@@ -281,6 +291,9 @@ type BaseSelectionListProps = Partial & {
/** Custom content to display in the footer */
footerContent?: ReactNode;
+ /** Custom content to display in the footer of list component. If present ShowMore button won't be displayed */
+ listFooterContent?: React.JSX.Element | null;
+
/** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */
shouldUseDynamicMaxToRenderPerBatch?: boolean;
@@ -293,9 +306,6 @@ type BaseSelectionListProps = Partial & {
/** Whether focus event should be delayed */
shouldDelayFocus?: boolean;
- /** Component to display on the right side of each child */
- rightHandSideComponent?: ((item: TItem) => ReactElement | null) | ReactElement | null;
-
/** Whether to show the loading indicator for new options */
isLoadingNewOptions?: boolean;
@@ -322,7 +332,7 @@ type BaseSelectionListProps = Partial & {
* When false, the list will render immediately and scroll to the bottom which works great for small lists.
*/
shouldHideListOnInitialRender?: boolean;
-};
+} & TRightHandSideComponent;
type SelectionListHandle = {
scrollAndHighlightItem?: (items: string[], timeout: number) => void;
@@ -343,7 +353,11 @@ type FlattenedSectionsReturn = {
type ButtonOrCheckBoxRoles = 'button' | 'checkbox';
-type SectionListDataType = SectionListData>;
+type ExtendedSectionListData> = SectionListData & {
+ CustomSectionHeader?: ({section}: {section: TSection}) => ReactElement;
+};
+
+type SectionListDataType = ExtendedSectionListData>;
export type {
BaseSelectionListProps,
diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx
index 84c57ae381e3..f56c4dd1a863 100644
--- a/src/components/SettlementButton.tsx
+++ b/src/components/SettlementButton.tsx
@@ -180,13 +180,13 @@ function SettlementButton({
};
const canUseWallet = !isExpenseReport && currency === CONST.CURRENCY.USD;
- // Only show the Approve button if the user cannot pay the request
+ // Only show the Approve button if the user cannot pay the expense
if (shouldHidePaymentOptions && shouldShowApproveButton) {
return [approveButtonOption];
}
// To achieve the one tap pay experience we need to choose the correct payment type as default.
- // If the user has previously chosen a specific payment option or paid for some request or expense,
+ // If the user has previously chosen a specific payment option or paid for some expense,
// let's use the last payment method or use default.
const paymentMethod = nvpLastPaymentMethod?.[policyID] ?? '';
if (canUseWallet) {
diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx
index f968af4f6030..97cd9aa5c691 100644
--- a/src/components/TagPicker/index.tsx
+++ b/src/components/TagPicker/index.tsx
@@ -32,7 +32,7 @@ type TagPickerProps = TagPickerOnyxProps & {
// eslint-disable-next-line react/no-unused-prop-types
policyID: string;
- /** The selected tag of the money request */
+ /** The selected tag of the expense */
selectedTag: string;
/** The name of tag list we are getting tags for */
diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx
index 2d964f58c253..44f87fd7981f 100644
--- a/src/components/TaskHeaderActionButton.tsx
+++ b/src/components/TaskHeaderActionButton.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useContext} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
@@ -10,6 +10,7 @@ import * as Task from '@userActions/Task';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import Button from './Button';
+import {MenuItemGroupContext} from './MenuItemGroup';
type TaskHeaderActionButtonOnyxProps = {
/** Current user session */
@@ -24,15 +25,25 @@ type TaskHeaderActionButtonProps = TaskHeaderActionButtonOnyxProps & {
function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const {isExecuting, singleExecution} = useContext(MenuItemGroupContext) ?? {};
+
+ const onPressAction = () => {
+ const onPress = () => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report));
+ if (!singleExecution) {
+ onPress();
+ return;
+ }
+ singleExecution(onPress)();
+ };
return (
(ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))}
+ onPress={Session.checkIfActionIsAllowed(onPressAction)}
style={styles.flex1}
/>
diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx
index 5efa9592034f..6827dee44141 100644
--- a/src/components/TestToolMenu.tsx
+++ b/src/components/TestToolMenu.tsx
@@ -10,6 +10,7 @@ import * as Network from '@userActions/Network';
import * as Session from '@userActions/Session';
import * as User from '@userActions/User';
import CONFIG from '@src/CONFIG';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Network as NetworkOnyx, User as UserOnyx} from '@src/types/onyx';
@@ -103,6 +104,16 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) {
}}
/>
+ {/* Navigate to the new Search Page. This button is temporary and should be removed after passing QA tests. */}
+
+ {
+ Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL));
+ }}
+ />
+
>
);
}
diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx
index 0fe0fe378ba6..499dd2b07f67 100644
--- a/src/components/VideoPlayerContexts/PlaybackContext.tsx
+++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx
@@ -10,6 +10,7 @@ const Context = React.createContext(null);
function PlaybackContextProvider({children}: ChildrenProps) {
const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState(null);
+ const [currentlyPlayingURLReportID, setCurrentlyPlayingURLReportID] = useState();
const [sharedElement, setSharedElement] = useState(null);
const [originalParent, setOriginalParent] = useState(null);
const currentVideoPlayerRef = useRef(null);
@@ -21,7 +22,7 @@ function PlaybackContextProvider({children}: ChildrenProps) {
}, [currentVideoPlayerRef]);
const stopVideo = useCallback(() => {
- currentVideoPlayerRef.current?.stopAsync?.();
+ currentVideoPlayerRef.current?.setStatusAsync?.({shouldPlay: false, positionMillis: 0});
}, [currentVideoPlayerRef]);
const playVideo = useCallback(() => {
@@ -43,9 +44,10 @@ function PlaybackContextProvider({children}: ChildrenProps) {
if (currentlyPlayingURL && url !== currentlyPlayingURL) {
pauseVideo();
}
+ setCurrentlyPlayingURLReportID(currentReportID);
setCurrentlyPlayingURL(url);
},
- [currentlyPlayingURL, pauseVideo],
+ [currentlyPlayingURL, currentReportID, pauseVideo],
);
const shareVideoPlayerElements = useCallback(
@@ -91,6 +93,7 @@ function PlaybackContextProvider({children}: ChildrenProps) {
() => ({
updateCurrentlyPlayingURL,
currentlyPlayingURL,
+ currentlyPlayingURLReportID,
originalParent,
sharedElement,
currentVideoPlayerRef,
@@ -101,7 +104,18 @@ function PlaybackContextProvider({children}: ChildrenProps) {
checkVideoPlaying,
videoResumeTryNumber,
}),
- [updateCurrentlyPlayingURL, currentlyPlayingURL, originalParent, sharedElement, shareVideoPlayerElements, playVideo, pauseVideo, checkVideoPlaying, setCurrentlyPlayingURL],
+ [
+ updateCurrentlyPlayingURL,
+ currentlyPlayingURL,
+ currentlyPlayingURLReportID,
+ originalParent,
+ sharedElement,
+ shareVideoPlayerElements,
+ playVideo,
+ pauseVideo,
+ checkVideoPlaying,
+ setCurrentlyPlayingURL,
+ ],
);
return {children};
}
diff --git a/src/components/VideoPlayerContexts/types.ts b/src/components/VideoPlayerContexts/types.ts
index e6a20ec090fe..ff8d9378caf7 100644
--- a/src/components/VideoPlayerContexts/types.ts
+++ b/src/components/VideoPlayerContexts/types.ts
@@ -9,6 +9,7 @@ import type CONST from '@src/CONST';
type PlaybackContext = {
updateCurrentlyPlayingURL: (url: string | null) => void;
currentlyPlayingURL: string | null;
+ currentlyPlayingURLReportID: string | undefined;
originalParent: View | HTMLDivElement | null;
sharedElement: View | HTMLDivElement | null;
videoResumeTryNumber: MutableRefObject;
diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx
index 923ab919a55a..414e95b0ff32 100644
--- a/src/components/VideoPlayerPreview/index.tsx
+++ b/src/components/VideoPlayerPreview/index.tsx
@@ -21,6 +21,9 @@ type VideoPlayerPreviewProps = {
/** Url to a video. */
videoUrl: string;
+ /** reportID of the video */
+ reportID: string;
+
/** Dimension of a video. */
videoDimensions: VideoDimensions;
@@ -37,10 +40,10 @@ type VideoPlayerPreviewProps = {
onShowModalPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise;
};
-function VideoPlayerPreview({videoUrl, thumbnailUrl, fileName, videoDimensions, videoDuration, onShowModalPress}: VideoPlayerPreviewProps) {
+function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDimensions, videoDuration, onShowModalPress}: VideoPlayerPreviewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {currentlyPlayingURL, updateCurrentlyPlayingURL} = usePlaybackContext();
+ const {currentlyPlayingURL, currentlyPlayingURLReportID, updateCurrentlyPlayingURL} = usePlaybackContext();
const {isSmallScreenWidth} = useWindowDimensions();
const [isThumbnail, setIsThumbnail] = useState(true);
const [measuredDimensions, setMeasuredDimensions] = useState(videoDimensions);
@@ -60,11 +63,11 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, fileName, videoDimensions,
};
useEffect(() => {
- if (videoUrl !== currentlyPlayingURL) {
+ if (videoUrl !== currentlyPlayingURL || reportID !== currentlyPlayingURLReportID) {
return;
}
setIsThumbnail(false);
- }, [currentlyPlayingURL, updateCurrentlyPlayingURL, videoUrl]);
+ }, [currentlyPlayingURL, currentlyPlayingURLReportID, updateCurrentlyPlayingURL, videoUrl, reportID]);
return (
diff --git a/src/components/transactionPropTypes.js b/src/components/transactionPropTypes.js
index 7eb1b776358c..f951837503f3 100644
--- a/src/components/transactionPropTypes.js
+++ b/src/components/transactionPropTypes.js
@@ -39,7 +39,7 @@ export default PropTypes.shape({
/** The text of the comment */
comment: PropTypes.string,
- /** The waypoints defining the distance request */
+ /** The waypoints defining the distance expense */
waypoints: PropTypes.shape({
/** The latitude of the waypoint */
lat: PropTypes.number,
diff --git a/src/hooks/useActiveRoute.ts b/src/hooks/useActiveRoute.ts
index 651d00a0c37c..afccc28f8243 100644
--- a/src/hooks/useActiveRoute.ts
+++ b/src/hooks/useActiveRoute.ts
@@ -1,7 +1,8 @@
import {useContext} from 'react';
import ActiveRouteContext from '@libs/Navigation/AppNavigator/Navigators/ActiveRouteContext';
+import type {CentralPaneNavigatorParamList, NavigationPartialRoute} from '@libs/Navigation/types';
-function useActiveRoute(): string {
+function useActiveRoute(): NavigationPartialRoute | undefined {
return useContext(ActiveRouteContext);
}
diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts
index 21c8d02e9194..b1f430e232e4 100644
--- a/src/hooks/useMarkdownStyle.ts
+++ b/src/hooks/useMarkdownStyle.ts
@@ -49,6 +49,10 @@ function useMarkdownStyle(message: string | null = null): MarkdownStyle {
color: theme.mentionText,
backgroundColor: theme.mentionBG,
},
+ mentionReport: {
+ color: theme.mentionText,
+ backgroundColor: theme.mentionBG,
+ },
}),
[theme, emojiFontSize],
);
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 9451407c822f..ed2587e5e2c6 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -50,6 +50,7 @@ import type {
PayerPaidAmountParams,
PayerPaidParams,
PayerSettledParams,
+ PaySomeoneParams,
RemovedTheRequestParams,
RenamedRoomActionParams,
ReportArchiveReasonsClosedParams,
@@ -418,7 +419,7 @@ export default {
},
login: {
hero: {
- header: 'Split bills, request payments, and chat with friends.',
+ header: 'Manage spend, split expenses, and chat with your team.',
body: 'Welcome to the future of Expensify, your new go-to place for financial collaboration with friends and teammates alike.',
},
},
@@ -470,14 +471,9 @@ export default {
copyEmailToClipboard: 'Copy email to clipboard',
markAsUnread: 'Mark as unread',
markAsRead: 'Mark as read',
- editAction: ({action}: EditActionParams) =>
- `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`,
- deleteAction: ({action}: DeleteActionParams) =>
- `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`,
- deleteConfirmation: ({action}: DeleteConfirmationParams) =>
- `Are you sure you want to delete this ${
- action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'
- }?`,
+ editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`,
+ deleteAction: ({action}: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`,
+ deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}?`,
onlyVisible: 'Only visible to',
replyInThread: 'Reply in thread',
joinThread: 'Join thread',
@@ -505,7 +501,7 @@ export default {
beginningOfChatHistory: 'This is the beginning of your chat with ',
beginningOfChatHistoryPolicyExpenseChatPartOne: 'Collaboration between ',
beginningOfChatHistoryPolicyExpenseChatPartTwo: ' and ',
- beginningOfChatHistoryPolicyExpenseChatPartThree: ' starts here! 🎉 This is the place to chat, request money and settle up.',
+ beginningOfChatHistoryPolicyExpenseChatPartThree: ' starts here! 🎉 This is the place to chat, submit expenses and settle up.',
beginningOfChatHistorySelfDM: 'This is your personal space. Use it for notes, tasks, drafts, and reminders.',
chatWithAccountManager: 'Chat with your account manager here',
sayHello: 'Say hello!',
@@ -513,9 +509,9 @@ export default {
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Welcome to ${roomName}!`,
usePlusButton: ({additionalText}: UsePlusButtonParams) => `\nYou can also use the + button to ${additionalText}, or assign a task!`,
iouTypes: {
- send: 'send money',
- split: 'split a bill',
- request: 'request money',
+ send: 'pay expenses',
+ split: 'split an expense',
+ request: 'submit an expense',
// eslint-disable-next-line @typescript-eslint/naming-convention
'track-expense': 'track an expense',
},
@@ -595,15 +591,15 @@ export default {
quickAction: {
scanReceipt: 'Scan receipt',
recordDistance: 'Record distance',
- requestMoney: 'Request money',
- splitBill: 'Split bill',
+ requestMoney: 'Submit expense',
+ splitBill: 'Split expense',
splitScan: 'Split receipt',
splitDistance: 'Split distance',
- sendMoney: 'Send money',
+ sendMoney: 'Pay someone',
assignTask: 'Assign task',
header: 'Quick action',
- trackManual: 'Track manual',
- trackScan: 'Track scan',
+ trackManual: 'Track expense',
+ trackScan: 'Track receipt',
trackDistance: 'Track distance',
},
iou: {
@@ -616,12 +612,13 @@ export default {
card: 'Card',
original: 'Original',
split: 'Split',
- addToSplit: 'Add to split',
- splitBill: 'Split bill',
- request: 'Request',
+ splitExpense: 'Split expense',
+ paySomeone: ({name}: PaySomeoneParams) => `Pay ${name ?? 'someone'}`,
+ expense: 'Expense',
+ categorize: 'Categorize',
+ share: 'Share',
participants: 'Participants',
- requestMoney: 'Request money',
- sendMoney: 'Send money',
+ submitExpense: 'Submit expense',
trackExpense: 'Track expense',
pay: 'Pay',
cancelPayment: 'Cancel payment',
@@ -640,20 +637,20 @@ export default {
receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.",
receiptScanningFailed: 'Receipt scanning failed. Enter the details manually.',
transactionPendingText: 'It takes a few days from the date the card was used for the transaction to post.',
- requestCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) =>
- `${count} ${Str.pluralize('request', 'requests', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${
+ expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) =>
+ `${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${
pendingReceipts > 0 ? `, ${pendingReceipts} pending` : ''
}`,
- deleteRequest: 'Delete request',
- deleteConfirmation: 'Are you sure that you want to delete this request?',
+ deleteExpense: 'Delete expense',
+ deleteConfirmation: 'Are you sure that you want to delete this expense?',
settledExpensify: 'Paid',
settledElsewhere: 'Paid elsewhere',
settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`),
payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} elsewhere` : `Pay elsewhere`),
nextStep: 'Next Steps',
finished: 'Finished',
- requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`,
- requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `requested ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
+ submitAmount: ({amount}: RequestAmountParams) => `submit ${amount}`,
+ submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `submitted ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`,
didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
@@ -678,16 +675,16 @@ export default {
paidWithExpensifyWithAmount: ({payer, amount}: PaidWithExpensifyWithAmountParams) => `${payer ? `${payer} ` : ''}paid ${amount} using Expensify`,
noReimbursableExpenses: 'This report has an invalid amount',
pendingConversionMessage: "Total will update when you're back online",
- changedTheRequest: 'changed the request',
+ changedTheExpense: 'changed the expense',
setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`,
setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `set the distance to ${newDistanceToDisplay}, which set the amount to ${newAmountToDisplay}`,
removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`,
updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`,
updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) =>
`changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`,
- threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'request'}`,
+ threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'expense'}`,
threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`,
- threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`,
+ threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`,
tagSelection: 'Select a tag to better organize your spend.',
categorySelection: 'Select a category to better organize your spend.',
error: {
@@ -696,36 +693,36 @@ export default {
invalidTaxAmount: ({amount}: RequestAmountParams) => `Maximum tax amount is ${amount}`,
invalidSplit: 'Split amounts do not equal total amount',
other: 'Unexpected error, please try again later',
- genericCreateFailureMessage: 'Unexpected error requesting money, please try again later',
+ genericCreateFailureMessage: 'Unexpected error submitting this expense. Please try again later.',
receiptFailureMessage: "The receipt didn't upload. ",
saveFileMessage: 'Download the file ',
loseFileMessage: 'or dismiss this error and lose it',
- genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later',
- genericEditFailureMessage: 'Unexpected error editing the money request, please try again later',
+ genericDeleteFailureMessage: 'Unexpected error deleting this expense, please try again later',
+ genericEditFailureMessage: 'Unexpected error editing this expense, please try again later',
genericSmartscanFailureMessage: 'Transaction is missing fields',
duplicateWaypointsErrorMessage: 'Please remove duplicate waypoints',
atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses',
- splitBillMultipleParticipantsErrorMessage: 'Split bill is only allowed between a single workspace or individual users. Please update your selection.',
+ splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.',
invalidMerchant: 'Please enter a correct merchant.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`,
enableWallet: 'Enable Wallet',
hold: 'Hold',
- holdRequest: 'Hold request',
- unholdRequest: 'Unhold request',
- heldRequest: 'held this request',
- unheldRequest: 'unheld this request',
- explainHold: "Explain why you're holding this request.",
+ holdExpense: 'Hold expense',
+ unholdExpense: 'Unhold expense',
+ heldExpense: 'held this expense',
+ unheldExpense: 'unheld this expense',
+ explainHold: "Explain why you're holding this expense.",
reason: 'Reason',
holdReasonRequired: 'A reason is required when holding.',
- requestOnHold: 'This request was put on hold. Review the comments for next steps.',
+ expenseOnHold: 'This expense was put on hold. Review the comments for next steps.',
confirmApprove: 'Confirm what to approve',
confirmApprovalAmount: 'Approve the entire report total or only the amount not on hold.',
confirmPay: 'Confirm what to pay',
confirmPayAmount: 'Pay all out-of-pocket spend or only the amount not on hold.',
payOnly: 'Pay only',
approveOnly: 'Approve only',
- holdEducationalTitle: 'This request is on',
+ holdEducationalTitle: 'This expense is on',
whatIsHoldTitle: 'What is hold?',
whatIsHoldExplain: 'Hold is our way of streamlining financial collaboration. "Reject" is so harsh!',
holdIsTemporaryTitle: 'Hold is usually temporary',
@@ -936,8 +933,7 @@ export default {
reasonForLeavingPrompt: 'We’d hate to see you go! Would you kindly tell us why, so we can improve?',
enterMessageHere: 'Enter message here',
closeAccountWarning: 'Closing your account cannot be undone.',
- closeAccountPermanentlyDeleteData:
- 'This will permanently delete all of your unsubmitted expense data and will cancel and decline any outstanding money requests. Are you sure you want to delete the account?',
+ closeAccountPermanentlyDeleteData: 'Are you sure you want to delete your account? This will permanently delete any outstanding expenses.',
enterDefaultContactToConfirm: 'Please type your default contact method to confirm you wish to close your account. Your default contact method is:',
enterDefaultContact: 'Enter your default contact method',
defaultContact: 'Default contact method:',
@@ -1265,19 +1261,6 @@ export default {
},
chooseThemeBelowOrSync: 'Choose a theme below, or sync with your device settings.',
},
- signInPage: {
- expensifyDotCash: 'New Expensify',
- theCode: 'the code',
- openJobs: 'open jobs',
- heroHeading: 'Split bills\nand chat with friends.',
- heroDescription: {
- phrase1: "Money talks. And now that chat and payments are in one place, it's also easy. Your payments get to you as fast as you can get your point across.",
- phrase2: 'The New Expensify is open source. View',
- phrase3: 'the code',
- phrase4: 'View',
- phrase5: 'open jobs',
- },
- },
termsOfUse: {
phrase1: 'By logging in, you agree to the',
phrase2: 'Terms of Service',
@@ -1343,7 +1326,7 @@ export default {
[CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Get paid back by my employer',
[CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: "Manage my team's expenses",
[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Track and budget personal spend',
- [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chat and split bills with friends',
+ [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chat and split expenses with friends',
[CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: "I'm just looking around",
},
error: {
@@ -1399,7 +1382,6 @@ export default {
localTime: 'Local time',
},
newChatPage: {
- createChat: 'Create chat',
startGroup: 'Start group',
addToGroup: 'Add to group',
},
@@ -2113,7 +2095,18 @@ export default {
return 'Importing accounts';
case 'quickbooksOnlineImportClasses':
return 'Importing classes';
-
+ case 'quickbooksOnlineImportLocations':
+ return 'Importing locations';
+ case 'quickbooksOnlineImportProcessing':
+ return 'Processing imported data';
+ case 'quickbooksOnlineSyncBillPayments':
+ return 'Synchronizing reimbursed reports and bill Payments';
+ case 'quickbooksOnlineSyncTaxCodes':
+ return 'Importing tax codes';
+ case 'quickbooksOnlineCheckConnection':
+ return 'Checking QuickBooks Online connection';
+ case 'quickbooksOnlineImportMain':
+ return 'Importing your QuickBooks Online data';
default: {
return `Translation missing for stage: ${stage}`;
}
@@ -2527,7 +2520,6 @@ export default {
parentReportAction: {
deletedReport: '[Deleted report]',
deletedMessage: '[Deleted message]',
- deletedRequest: '[Deleted request]',
deletedExpense: '[Deleted expense]',
reversedTransaction: '[Reversed transaction]',
deletedTask: '[Deleted task]',
@@ -2575,6 +2567,12 @@ export default {
accept: 'Accept',
decline: 'Decline',
},
+ actionableMentionTrackExpense: {
+ submit: 'Submit it to someone',
+ categorize: 'Categorize it',
+ share: 'Share it with my accountant',
+ nothing: 'Nothing for now',
+ },
teachersUnitePage: {
teachersUnite: 'Teachers Unite',
joinExpensifyOrg: 'Join Expensify.org in eliminating injustice around the world and help teachers split their expenses for classrooms in need!',
@@ -2649,27 +2647,27 @@ export default {
body: `Get paid to talk to your friends! Start a chat with a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]: {
- buttonText1: 'Request money, ',
+ buttonText1: 'Submit expense, ',
buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`,
- header: `Request money, get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body: `It pays to get paid! Request money from a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`,
+ header: `Submit an expense, get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
+ body: `It pays to get paid! Submit an expense to a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: {
- buttonText1: 'Send money, ',
+ buttonText1: 'Pay Someone, ',
buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`,
- header: `Send money, get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body: `You gotta send money to make money! Send money to a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`,
+ header: `Pay Someone, get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
+ body: `You gotta spend money to make money! Pay someone with Expensify and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]: {
buttonText1: 'Invite a friend, ',
buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`,
header: `Get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body: `Be the first to chat, send or request money, split a bill, or share your invite link with a friend, and you'll get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer. You can post your invite link on social media, too!`,
+ body: `Chat, pay, submit, or split an expense with a friend and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer. Otherwise, just share your invite link!`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]: {
buttonText1: `Get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
header: `Get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body: `Be the first to chat, send or request money, split a bill, or share your invite link with a friend, and you'll get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer. You can post your invite link on social media, too!`,
+ body: `Chat, pay, submit, or split an expense with a friend and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer. Otherwise, just share your invite link!`,
},
copyReferralLink: 'Copy invite link',
},
@@ -2677,7 +2675,7 @@ export default {
[CONST.INTRO_CHOICES.TRACK]: 'Track business spend for taxes',
[CONST.INTRO_CHOICES.SUBMIT]: 'Get paid back by my employer',
[CONST.INTRO_CHOICES.MANAGE_TEAM]: "Manage my team's expenses",
- [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chat and split bills with friends',
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chat and split expenses with friends',
welcomeMessage: 'Welcome to Expensify',
welcomeSubtitle: 'What would you like to do?',
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index a56c8ac2739d..beb654cf0bc4 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -49,6 +49,7 @@ import type {
PayerPaidAmountParams,
PayerPaidParams,
PayerSettledParams,
+ PaySomeoneParams,
RemovedTheRequestParams,
RenamedRoomActionParams,
ReportArchiveReasonsClosedParams,
@@ -409,7 +410,7 @@ export default {
},
login: {
hero: {
- header: 'Divida las facturas, solicite pagos y chatee con sus amigos.',
+ header: 'Gestiona, divide gastos y chatea con tu equipo.',
body: 'Bienvenido al futuro de Expensify, tu nuevo lugar de referencia para la colaboración financiera con amigos y compañeros de equipo por igual.',
},
},
@@ -461,18 +462,10 @@ export default {
copyEmailToClipboard: 'Copiar email al portapapeles',
markAsUnread: 'Marcar como no leído',
markAsRead: 'Marcar como leído',
- editAction: ({action}: EditActionParams) =>
- `Editar ${
- action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario'
- }`,
- deleteAction: ({action}: DeleteActionParams) =>
- `Eliminar ${
- action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario'
- }`,
+ editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`,
+ deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`,
deleteConfirmation: ({action}: DeleteConfirmationParams) =>
- `¿Estás seguro de que quieres eliminar esta ${
- action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario'
- }`,
+ `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gasto' : 'comentario'}`,
onlyVisible: 'Visible sólo para',
replyInThread: 'Responder en el hilo',
joinThread: 'Unirse al hilo',
@@ -501,7 +494,7 @@ export default {
beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ',
beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ',
beginningOfChatHistoryPolicyExpenseChatPartTwo: ' y ',
- beginningOfChatHistoryPolicyExpenseChatPartThree: ' empieza aquí! 🎉 Este es el lugar donde chatear, pedir dinero y pagar.',
+ beginningOfChatHistoryPolicyExpenseChatPartThree: ' empieza aquí! 🎉 Este es el lugar donde chatear y presentar o pagar gastos.',
beginningOfChatHistorySelfDM: 'Este es tu espacio personal. Úsalo para notas, tareas, borradores y recordatorios.',
chatWithAccountManager: 'Chatea con tu gestor de cuenta aquí',
sayHello: '¡Saluda!',
@@ -509,9 +502,9 @@ export default {
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `¡Bienvenido a ${roomName}!`,
usePlusButton: ({additionalText}: UsePlusButtonParams) => `\n¡También puedes usar el botón + de abajo para ${additionalText}, o asignar una tarea!`,
iouTypes: {
- send: 'enviar dinero',
- split: 'dividir una factura',
- request: 'pedir dinero',
+ send: 'pagar gastos',
+ split: 'dividir un gasto',
+ request: 'presentar un gasto',
// eslint-disable-next-line @typescript-eslint/naming-convention
'track-expense': 'rastrear un gasto',
},
@@ -591,11 +584,11 @@ export default {
quickAction: {
scanReceipt: 'Escanear recibo',
recordDistance: 'Grabar distancia',
- requestMoney: 'Solicitar dinero',
- splitBill: 'Dividir cuenta',
+ requestMoney: 'Presentar gasto',
+ splitBill: 'Dividir gasto',
splitScan: 'Dividir recibo',
splitDistance: 'Dividir distancia',
- sendMoney: 'Enviar dinero',
+ sendMoney: 'Pagar a alguien',
assignTask: 'Assignar tarea',
header: 'Acción rápida',
trackManual: 'Crear gasto',
@@ -612,12 +605,13 @@ export default {
card: 'Tarjeta',
original: 'Original',
split: 'Dividir',
- addToSplit: 'Añadir para dividir',
- splitBill: 'Dividir factura',
- request: 'Solicitar',
+ splitExpense: 'Dividir gasto',
+ expense: 'Gasto',
+ categorize: 'Categorizar',
+ share: 'Compartir',
participants: 'Participantes',
- requestMoney: 'Pedir dinero',
- sendMoney: 'Enviar dinero',
+ submitExpense: 'Presentar gasto',
+ paySomeone: ({name}: PaySomeoneParams) => `Pagar a ${name ?? 'alguien'}`,
trackExpense: 'Seguimiento de gastos',
pay: 'Pagar',
cancelPayment: 'Cancelar el pago',
@@ -636,11 +630,11 @@ export default {
receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.',
receiptScanningFailed: 'El escaneo de recibo ha fallado. Introduce los detalles manualmente.',
transactionPendingText: 'La transacción tarda unos días en contabilizarse desde la fecha en que se utilizó la tarjeta.',
- requestCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) =>
- `${count} ${Str.pluralize('solicitude', 'solicitudes', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${
+ expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) =>
+ `${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${
pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : ''
}`,
- deleteRequest: 'Eliminar solicitud',
+ deleteExpense: 'Eliminar gasto',
deleteConfirmation: '¿Estás seguro de que quieres eliminar esta solicitud?',
settledExpensify: 'Pagado',
settledElsewhere: 'Pagado de otra forma',
@@ -648,9 +642,9 @@ export default {
payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`),
nextStep: 'Pasos Siguientes',
finished: 'Finalizado',
- requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`,
- requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicité ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
- trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `seguimiento ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
+ submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`,
+ submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicitó ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
+ trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`,
didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`,
@@ -674,7 +668,7 @@ export default {
paidWithExpensifyWithAmount: ({payer, amount}: PaidWithExpensifyWithAmountParams) => `${payer ? `${payer} ` : ''}pagó ${amount} con Expensify`,
noReimbursableExpenses: 'El importe de este informe no es válido',
pendingConversionMessage: 'El total se actualizará cuando estés online',
- changedTheRequest: 'cambió la solicitud',
+ changedTheExpense: 'cambió el gasto',
setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`,
setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) =>
`estableció la distancia a ${newDistanceToDisplay}, lo que estableció el importe a ${newAmountToDisplay}`,
@@ -683,9 +677,9 @@ export default {
`${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`,
updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) =>
`cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`,
- threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Solicitud de ${formattedAmount}`}`,
+ threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Gasto de ${formattedAmount}`}`,
threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`,
- threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`,
+ threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`,
tagSelection: 'Selecciona una etiqueta para organizar mejor tu dinero.',
categorySelection: 'Seleccione una categoría para organizar mejor tu dinero.',
error: {
@@ -694,28 +688,28 @@ export default {
invalidTaxAmount: ({amount}: RequestAmountParams) => `El importe máximo del impuesto es ${amount}`,
invalidSplit: 'La suma de las partes no equivale al importe total',
other: 'Error inesperado, por favor inténtalo más tarde',
- genericCreateFailureMessage: 'Error inesperado solicitando dinero. Por favor, inténtalo más tarde',
+ genericCreateFailureMessage: 'Error inesperado al enviar este gasto. Por favor, inténtalo más tarde.',
receiptFailureMessage: 'El recibo no se subió. ',
saveFileMessage: 'Guarda el archivo ',
loseFileMessage: 'o descarta este error y piérdelo',
- genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde',
- genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde',
+ genericDeleteFailureMessage: 'Error inesperado al eliminar este gasto. Por favor, inténtalo más tarde',
+ genericEditFailureMessage: 'Error inesperado al editar este gasto. Por favor, inténtalo más tarde',
genericSmartscanFailureMessage: 'La transacción tiene campos vacíos',
duplicateWaypointsErrorMessage: 'Por favor, elimina los puntos de ruta duplicados',
atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes',
- splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.',
+ splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.',
invalidMerchant: 'Por favor, introduce un comerciante correcto.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`,
enableWallet: 'Habilitar Billetera',
- holdRequest: 'Bloquear solicitud',
- unholdRequest: 'Desbloquear solicitud',
- heldRequest: 'bloqueó esta solicitud',
- unheldRequest: 'desbloqueó esta solicitud',
+ holdExpense: 'Bloquear gasto',
+ unholdExpense: 'Desbloquear gasto',
+ heldExpense: 'bloqueó este gasto',
+ unheldExpense: 'desbloqueó este gasto',
explainHold: 'Explica la razón para bloquear esta solicitud.',
reason: 'Razón',
holdReasonRequired: 'Se requiere una razón para bloquear.',
- requestOnHold: 'Este solicitud está bloqueada. Revisa los comentarios para saber como proceder.',
+ expenseOnHold: 'Este gasto está bloqueado. Revisa los comentarios para saber como proceder.',
confirmApprove: 'Confirma que quieres aprobar',
confirmApprovalAmount: 'Aprobar el total o solo la parte no bloqueada.',
confirmPay: 'Confirma que quieres pagar',
@@ -723,7 +717,7 @@ export default {
payOnly: 'Solo pagar',
approveOnly: 'Solo aprobar',
hold: 'Bloqueada',
- holdEducationalTitle: 'Esta solicitud está',
+ holdEducationalTitle: 'Este gasto está',
whatIsHoldTitle: '¿Qué es Bloquear?',
whatIsHoldExplain: 'Bloquear es nuestra forma de agilizar la colaboración financiera. ¡"Rechazar" es tan duro!',
holdIsTemporaryTitle: 'Bloquear suele ser temporal',
@@ -934,8 +928,7 @@ export default {
reasonForLeavingPrompt: '¡Lamentamos verte partir! ¿Serías tan amable de decirnos por qué, para que podamos mejorar?',
enterMessageHere: 'Escribe aquí tu mensaje',
closeAccountWarning: 'Una vez cerrada tu cuenta no se puede revertir.',
- closeAccountPermanentlyDeleteData:
- 'Esta acción eliminará permanentemente toda la información de tus gastos no enviados y cancelará o rechazará cualquier solicitud de dinero pendiente. ¿Estás seguro de que quieres eliminar tu cuenta?',
+ closeAccountPermanentlyDeleteData: '¿Estás seguro de que quieres eliminar tu cuenta? Esta acción eliminará permanentemente toda la información de cualquier gasto pendiente.',
enterDefaultContactToConfirm: 'Por favor, escribe tu método de contacto predeterminado para confirmar que deseas eliminar tu cuenta. Tu método de contacto predeterminado es:',
enterDefaultContact: 'Tu método de contacto predeterminado',
defaultContact: 'Método de contacto predeterminado:',
@@ -1267,19 +1260,6 @@ export default {
},
chooseThemeBelowOrSync: 'Elige un tema a continuación o sincronízalo con los ajustes de tu dispositivo.',
},
- signInPage: {
- expensifyDotCash: 'Nuevo Expensify',
- theCode: 'el código',
- openJobs: 'trabajos disponibles',
- heroHeading: 'Dividir cuentas\ny chatear con amigos.',
- heroDescription: {
- phrase1: 'El dinero habla. Y ahora que el chat y los pagos están en un solo lugar, también es fácil. Tus pagos te llegan tan rápido como puedes hacer llegar tu mensaje',
- phrase2: 'Nuevo Expensify es de código abierto. Vista',
- phrase3: 'el código',
- phrase4: 'Vista',
- phrase5: 'vacantes',
- },
- },
termsOfUse: {
phrase1: 'Al iniciar sesión, estás accediendo a los',
phrase2: 'Términos de Servicio',
@@ -1345,7 +1325,7 @@ export default {
[CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Cobrar de mi empresa',
[CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo',
[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Controlar y presupuestar los gastos personales',
- [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chatea y divide cuentas con tus amigos',
+ [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chatea y divide gastos con tus amigos',
[CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: 'Sólo estoy mirando',
},
error: {
@@ -1403,7 +1383,6 @@ export default {
localTime: 'Hora local',
},
newChatPage: {
- createChat: 'Crear chat',
startGroup: 'Grupo de inicio',
addToGroup: 'Añadir al grupo',
},
@@ -2103,12 +2082,23 @@ export default {
case 'quickbooksOnlineImportCustomers':
return 'Importando clientes';
case 'quickbooksOnlineImportEmployees':
- return 'Importing employees';
+ return 'Importando empleados';
case 'quickbooksOnlineImportAccounts':
- return 'Importing accounts';
+ return 'Importando cuentas';
case 'quickbooksOnlineImportClasses':
- return 'Importing classes';
-
+ return 'Importando clases';
+ case 'quickbooksOnlineImportLocations':
+ return 'Importando localidades';
+ case 'quickbooksOnlineImportProcessing':
+ return 'Procesando datos importados';
+ case 'quickbooksOnlineSyncBillPayments':
+ return 'Sincronizando reportes reembolsados y facturas pagadas';
+ case 'quickbooksOnlineSyncTaxCodes':
+ return 'Importando tipos de impuestos';
+ case 'quickbooksOnlineCheckConnection':
+ return 'Revisando conexión a QuickBooks Online';
+ case 'quickbooksOnlineImportMain':
+ return 'Importando datos desde QuickBooks Online';
default: {
return `Translation missing for stage: ${stage}`;
}
@@ -3019,7 +3009,6 @@ export default {
parentReportAction: {
deletedReport: '[Informe eliminado]',
deletedMessage: '[Mensaje eliminado]',
- deletedRequest: '[Solicitud eliminada]',
deletedExpense: '[Gasto eliminado]',
reversedTransaction: '[Transacción anulada]',
deletedTask: '[Tarea eliminada]',
@@ -3045,6 +3034,12 @@ export default {
accept: 'Aceptar',
decline: 'Rechazar',
},
+ actionableMentionTrackExpense: {
+ submit: 'Pedirle a alguien que lo pague',
+ categorize: 'Categorizarlo',
+ share: 'Compartirlo con mi contador',
+ nothing: 'Por ahora, nada',
+ },
moderation: {
flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.',
chooseAReason: 'Elige abajo un motivo para reportarlo:',
@@ -3142,27 +3137,27 @@ export default {
body: `¡Gana dinero por hablar con tus amigos! Inicia un chat con una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]: {
- buttonText1: 'Pide dinero, ',
+ buttonText1: 'Presentar gasto, ',
buttonText2: `recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- header: `Pide dinero y recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body: `¡Vale la pena cobrar! Pide dinero a una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`,
+ header: `Presenta un gasto y consigue $${CONST.REFERRAL_PROGRAM.REVENUE}`,
+ body: `¡Vale la pena cobrar! Envia un gasto a una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: {
- buttonText1: 'Envía dinero, ',
+ buttonText1: 'Pagar a alguien, ',
buttonText2: `recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- header: `Envía dinero y recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body: `¡Hay que enviar dinero para ganar dinero! Envía dinero a una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`,
+ header: `Paga a alguien y recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
+ body: `¡Hay que gastar dinero para ganar dinero! Paga a alguien con Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]: {
buttonText1: 'Invita a un amigo y ',
buttonText2: `recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
header: `Recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body: `Sé el primero en chatear, enviar o pedir dinero, dividir una factura o compartir tu enlace de invitación con un amigo, y recibirás $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se convierta en cliente. También puedes publicar tu enlace de invitación en las redes sociales.`,
+ body: `Chatea, paga, presenta y divide gastos con un amigo y recibirás $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se convierta en cliente. También puedes publicar tu enlace de invitación en las redes sociales.`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]: {
buttonText1: `Recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
header: `Recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body: `Sé el primero en chatear, enviar o pedir dinero, dividir una factura o compartir tu enlace de invitación con un amigo, y recibirás $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se convierta en cliente. También puedes publicar tu enlace de invitación en las redes sociales.`,
+ body: `Chatea, paga, presenta y divide gastos con un amigo y recibirás $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se convierta en cliente. También puedes publicar tu enlace de invitación en las redes sociales.`,
},
copyReferralLink: 'Copiar enlace de invitación',
},
diff --git a/src/languages/types.ts b/src/languages/types.ts
index c365363f84af..30b7f842db4c 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -247,6 +247,8 @@ type ViolationsTagOutOfPolicyParams = {tagName?: string};
type ViolationsTaxOutOfPolicyParams = {taxName?: string};
+type PaySomeoneParams = {name?: string};
+
type TaskCreatedActionParams = {title: string};
/* Translation Object types */
@@ -400,4 +402,5 @@ export type {
ZipCodeExampleFormatParams,
LogSizeParams,
HeldRequestParams,
+ PaySomeoneParams,
};
diff --git a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
new file mode 100644
index 000000000000..78eb0adecc5e
--- /dev/null
+++ b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
@@ -0,0 +1,25 @@
+import type {Receipt} from '@src/types/onyx/Transaction';
+
+type CategorizeTrackedExpenseParams = {
+ amount: number;
+ currency: string;
+ comment: string;
+ created: string;
+ merchant: string;
+ policyID: string;
+ transactionID: string;
+ moneyRequestPreviewReportActionID: string;
+ moneyRequestReportID: string;
+ moneyRequestCreatedReportActionID: string;
+ actionableWhisperReportActionID: string;
+ modifiedExpenseReportActionID: string;
+ reportPreviewReportActionID: string;
+ category?: string;
+ tag?: string;
+ receipt?: Receipt;
+ taxCode: string;
+ taxAmount: number;
+ billable?: boolean;
+};
+
+export default CategorizeTrackedExpenseParams;
diff --git a/src/libs/API/parameters/ConnectPolicyToQuickbooksOnlineParams.ts b/src/libs/API/parameters/ConnectPolicyToQuickbooksOnlineParams.ts
new file mode 100644
index 000000000000..4d889d69f5ad
--- /dev/null
+++ b/src/libs/API/parameters/ConnectPolicyToQuickbooksOnlineParams.ts
@@ -0,0 +1,5 @@
+type ConnectPolicyToQuickbooksOnlineParams = {
+ policyID: string;
+};
+
+export default ConnectPolicyToQuickbooksOnlineParams;
diff --git a/src/libs/API/parameters/ConvertTrackedExpenseToRequestParams.ts b/src/libs/API/parameters/ConvertTrackedExpenseToRequestParams.ts
new file mode 100644
index 000000000000..c51161b043a8
--- /dev/null
+++ b/src/libs/API/parameters/ConvertTrackedExpenseToRequestParams.ts
@@ -0,0 +1,21 @@
+import type {Receipt} from '@src/types/onyx/Transaction';
+
+type ConvertTrackedExpenseToRequestParams = {
+ amount: number;
+ currency: string;
+ created: string;
+ comment?: string;
+ merchant?: string;
+ payerAccountID: number;
+ chatReportID: string;
+ transactionID: string;
+ actionableWhisperReportActionID: string;
+ createdChatReportActionID: string;
+ receipt?: Receipt;
+ moneyRequestReportID: string;
+ moneyRequestCreatedReportActionID: string;
+ moneyRequestPreviewReportActionID: string;
+ reportPreviewReportActionID: string;
+};
+
+export default ConvertTrackedExpenseToRequestParams;
diff --git a/src/libs/API/parameters/DismissTrackExpenseActionableWhisperParams.ts b/src/libs/API/parameters/DismissTrackExpenseActionableWhisperParams.ts
new file mode 100644
index 000000000000..e441d100784d
--- /dev/null
+++ b/src/libs/API/parameters/DismissTrackExpenseActionableWhisperParams.ts
@@ -0,0 +1,5 @@
+type DismissTrackExpenseActionableWhisperParams = {
+ reportActionID: string;
+};
+
+export default DismissTrackExpenseActionableWhisperParams;
diff --git a/src/libs/API/parameters/OpenPolicyAccountingPageParams.ts b/src/libs/API/parameters/OpenPolicyAccountingPageParams.ts
new file mode 100644
index 000000000000..0ebaa58ef0d1
--- /dev/null
+++ b/src/libs/API/parameters/OpenPolicyAccountingPageParams.ts
@@ -0,0 +1,5 @@
+type OpenPolicyAccountingPageParams = {
+ policyID: string;
+};
+
+export default OpenPolicyAccountingPageParams;
diff --git a/src/libs/API/parameters/RemovePolicyConnectionParams.ts b/src/libs/API/parameters/RemovePolicyConnectionParams.ts
new file mode 100644
index 000000000000..a9a640bc8426
--- /dev/null
+++ b/src/libs/API/parameters/RemovePolicyConnectionParams.ts
@@ -0,0 +1,8 @@
+import type {PolicyConnectionName} from '@src/types/onyx/Policy';
+
+type RemovePolicyConnectionParams = {
+ policyID: string;
+ connectionName: PolicyConnectionName;
+};
+
+export default RemovePolicyConnectionParams;
diff --git a/src/libs/API/parameters/ShareTrackedExpenseParams.ts b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
new file mode 100644
index 000000000000..cee4bc40d9ac
--- /dev/null
+++ b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
@@ -0,0 +1,25 @@
+import type {Receipt} from '@src/types/onyx/Transaction';
+
+type ShareTrackedExpenseParams = {
+ amount: number;
+ currency: string;
+ comment: string;
+ created: string;
+ merchant: string;
+ policyID: string;
+ transactionID: string;
+ moneyRequestPreviewReportActionID: string;
+ moneyRequestReportID: string;
+ moneyRequestCreatedReportActionID: string;
+ actionableWhisperReportActionID: string;
+ modifiedExpenseReportActionID: string;
+ reportPreviewReportActionID: string;
+ category?: string;
+ tag?: string;
+ receipt?: Receipt;
+ taxCode: string;
+ taxAmount: number;
+ billable?: boolean;
+};
+
+export default ShareTrackedExpenseParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 385aabf8acff..bfa89b5d3bd3 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -11,6 +11,7 @@ export type {default as BeginGoogleSignInParams} from './BeginGoogleSignInParams
export type {default as BeginSignInParams} from './BeginSignInParams';
export type {default as CloseAccountParams} from './CloseAccountParams';
export type {default as ConnectBankAccountParams} from './ConnectBankAccountParams';
+export type {default as ConnectPolicyToQuickbooksOnlineParams} from './ConnectPolicyToQuickbooksOnlineParams';
export type {default as DeleteContactMethodParams} from './DeleteContactMethodParams';
export type {default as DeletePaymentBankAccountParams} from './DeletePaymentBankAccountParams';
export type {default as DeletePaymentCardParams} from './DeletePaymentCardParams';
@@ -204,5 +205,11 @@ export type {default as SetPolicyCustomTaxNameParams} from './SetPolicyCustomTax
export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicyForeignCurrencyDefaultParams';
export type {default as SetPolicyCurrencyDefaultParams} from './SetPolicyCurrencyDefaultParams';
export type {default as UpdatePolicyConnectionConfigParams} from './UpdatePolicyConnectionConfigParams';
+export type {default as RemovePolicyConnectionParams} from './RemovePolicyConnectionParams';
export type {default as RenamePolicyTaxParams} from './RenamePolicyTaxParams';
+export type {default as DismissTrackExpenseActionableWhisperParams} from './DismissTrackExpenseActionableWhisperParams';
+export type {default as ConvertTrackedExpenseToRequestParams} from './ConvertTrackedExpenseToRequestParams';
+export type {default as ShareTrackedExpenseParams} from './ShareTrackedExpenseParams';
+export type {default as CategorizeTrackedExpenseParams} from './CategorizeTrackedExpenseParams';
export type {default as LeavePolicyParams} from './LeavePolicyParams';
+export type {default as OpenPolicyAccountingPageParams} from './OpenPolicyAccountingPageParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 91b95dd6327e..f91b694548ba 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -193,6 +193,7 @@ const WRITE_COMMANDS = {
DECLINE_JOIN_REQUEST: 'DeclineJoinRequest',
CREATE_POLICY_TAX: 'CreatePolicyTax',
UPDATE_POLICY_CONNECTION_CONFIG: 'UpdatePolicyConnectionConfig',
+ REMOVE_POLICY_CONNECTION: 'RemovePolicyConnection',
SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled',
DELETE_POLICY_TAXES: 'DeletePolicyTaxes',
UPDATE_POLICY_TAX_VALUE: 'UpdatePolicyTaxValue',
@@ -205,6 +206,10 @@ const WRITE_COMMANDS = {
UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue',
SET_POLICY_DISTANCE_RATES_ENABLED: 'SetPolicyDistanceRatesEnabled',
DELETE_POLICY_DISTANCE_RATES: 'DeletePolicyDistanceRates',
+ DISMISS_TRACK_EXPENSE_ACTIONABLE_WHISPER: 'DismissActionableWhisper',
+ CONVERT_TRACKED_EXPENSE_TO_REQUEST: 'ConvertTrackedExpenseToRequest',
+ CATEGORIZE_TRACKED_EXPENSE: 'CategorizeTrackedExpense',
+ SHARE_TRACKED_EXPENSE: 'ShareTrackedExpense',
LEAVE_POLICY: 'LeavePolicy',
} as const;
@@ -405,15 +410,22 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RENAME_POLICY_TAX]: Parameters.RenamePolicyTaxParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams;
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams;
+ [WRITE_COMMANDS.REMOVE_POLICY_CONNECTION]: Parameters.RemovePolicyConnectionParams;
[WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams;
[WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams;
+ [WRITE_COMMANDS.DISMISS_TRACK_EXPENSE_ACTIONABLE_WHISPER]: Parameters.DismissTrackExpenseActionableWhisperParams;
+ [WRITE_COMMANDS.CONVERT_TRACKED_EXPENSE_TO_REQUEST]: Parameters.ConvertTrackedExpenseToRequestParams;
+ [WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE]: Parameters.CategorizeTrackedExpenseParams;
+ [WRITE_COMMANDS.SHARE_TRACKED_EXPENSE]: Parameters.ShareTrackedExpenseParams;
[WRITE_COMMANDS.LEAVE_POLICY]: Parameters.LeavePolicyParams;
};
const READ_COMMANDS = {
+ CONNECT_POLICY_TO_QUICKBOOKS_ONLINE: 'ConnectPolicyToQuickbooksOnline',
OPEN_REIMBURSEMENT_ACCOUNT_PAGE: 'OpenReimbursementAccountPage',
OPEN_WORKSPACE_VIEW: 'OpenWorkspaceView',
GET_MAPBOX_ACCESS_TOKEN: 'GetMapboxAccessToken',
@@ -449,11 +461,13 @@ const READ_COMMANDS = {
OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage',
OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage',
OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage',
+ OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage',
} as const;
type ReadCommand = ValueOf;
type ReadCommandParameters = {
+ [READ_COMMANDS.CONNECT_POLICY_TO_QUICKBOOKS_ONLINE]: Parameters.ConnectPolicyToQuickbooksOnlineParams;
[READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: Parameters.OpenReimbursementAccountPageParams;
[READ_COMMANDS.OPEN_WORKSPACE_VIEW]: Parameters.OpenWorkspaceViewParams;
[READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: EmptyObject;
@@ -489,6 +503,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams;
[READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams;
[READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE]: Parameters.OpenPolicyMoreFeaturesPageParams;
+ [READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams;
};
const SIDE_EFFECT_REQUEST_COMMANDS = {
diff --git a/src/libs/ApiUtils.ts b/src/libs/ApiUtils.ts
index 0c8fa3f53915..1da9795f333b 100644
--- a/src/libs/ApiUtils.ts
+++ b/src/libs/ApiUtils.ts
@@ -38,12 +38,14 @@ function getApiRoot(request?: Request): string {
const shouldUseSecure = request?.shouldUseSecure ?? false;
if (shouldUseStagingServer) {
- if (CONFIG.IS_USING_WEB_PROXY) {
+ if (CONFIG.IS_USING_WEB_PROXY && !request?.shouldSkipWebProxy) {
return shouldUseSecure ? proxyConfig.STAGING_SECURE : proxyConfig.STAGING;
}
return shouldUseSecure ? CONFIG.EXPENSIFY.STAGING_SECURE_API_ROOT : CONFIG.EXPENSIFY.STAGING_API_ROOT;
}
-
+ if (request?.shouldSkipWebProxy) {
+ return shouldUseSecure ? CONFIG.EXPENSIFY.SECURE_EXPENSIFY_URL : CONFIG.EXPENSIFY.EXPENSIFY_URL;
+ }
return shouldUseSecure ? CONFIG.EXPENSIFY.DEFAULT_SECURE_API_ROOT : CONFIG.EXPENSIFY.DEFAULT_API_ROOT;
}
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index c4d67adcd54a..9e3a7f66131a 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -127,7 +127,7 @@ function maskCard(lastFour = ''): string {
* @returns a physical card object (or undefined if none is found)
*/
function findPhysicalCard(cards: Card[]) {
- return cards.find((card) => !card.isVirtual);
+ return cards.find((card) => !card.nameValuePairs?.isVirtual);
}
/**
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index 44c7682b47f2..9b96bfa009dc 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -1,6 +1,7 @@
import {
addDays,
addHours,
+ addMilliseconds,
addMinutes,
eachDayOfInterval,
eachMonthOfInterval,
@@ -392,6 +393,13 @@ function subtractMillisecondsFromDateTime(dateTime: string, milliseconds: number
return getDBTime(newTimestamp);
}
+function addMillisecondsFromDateTime(dateTime: string, milliseconds: number): string {
+ const date = zonedTimeToUtc(dateTime, 'UTC');
+ const newTimestamp = addMilliseconds(date, milliseconds).valueOf();
+
+ return getDBTime(newTimestamp);
+}
+
/**
* @param isoTimestamp example: 2023-05-16 05:34:14.388
* @returns example: 2023-05-16
@@ -784,6 +792,7 @@ const DateUtils = {
getDBTimeWithSkew,
setLocale,
subtractMillisecondsFromDateTime,
+ addMillisecondsFromDateTime,
getDateStringFromISOTimestamp,
getThirtyMinutesFromNow,
getEndOfToday,
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index 9a7e0a568627..12a240ae9041 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -79,7 +79,7 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string
}
/**
- * @param hasRoute Whether the route exists for the distance request
+ * @param hasRoute Whether the route exists for the distance expense
* @param distanceInMeters Distance traveled
* @param unit Unit that should be used to display the distance
* @param rate Expensable amount allowed per unit
@@ -100,7 +100,7 @@ function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit
}
/**
- * @param hasRoute Whether the route exists for the distance request
+ * @param hasRoute Whether the route exists for the distance expense
* @param distanceInMeters Distance traveled
* @param unit Unit that should be used to display the distance
* @param rate Expensable amount allowed per unit
@@ -133,12 +133,12 @@ function getDistanceMerchant(
}
/**
- * Calculates the request amount based on distance, unit, and rate.
+ * Calculates the expense amount based on distance, unit, and rate.
*
* @param distance - The distance traveled in meters
* @param unit - The unit of measurement for the distance
- * @param rate - Rate used for calculating the request amount
- * @returns The computed request amount (rounded) in "cents".
+ * @param rate - Rate used for calculating the expense amount
+ * @returns The computed expense amount (rounded) in "cents".
*/
function getDistanceRequestAmount(distance: number, unit: Unit, rate: number): number {
const convertedDistance = convertDistanceUnit(distance, unit);
diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts
index 776e3de74f06..9d5b0be0d2e7 100644
--- a/src/libs/E2E/reactNativeLaunchingTest.ts
+++ b/src/libs/E2E/reactNativeLaunchingTest.ts
@@ -35,7 +35,7 @@ if (!appInstanceId) {
// import your test here, define its name and config first in e2e/config.js
const tests: Tests = {
[E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default,
- [E2EConfig.TEST_NAMES.OpenSearchPage]: require('./tests/openSearchPageTest.e2e').default,
+ [E2EConfig.TEST_NAMES.OpenChatFinderPage]: require('./tests/openChatFinderPageTest.e2e').default,
[E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default,
[E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default,
[E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default,
diff --git a/src/libs/E2E/tests/openSearchPageTest.e2e.ts b/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts
similarity index 82%
rename from src/libs/E2E/tests/openSearchPageTest.e2e.ts
rename to src/libs/E2E/tests/openChatFinderPageTest.e2e.ts
index 86da851396f6..9d2b117a7044 100644
--- a/src/libs/E2E/tests/openSearchPageTest.e2e.ts
+++ b/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts
@@ -9,7 +9,7 @@ import ROUTES from '@src/ROUTES';
const test = () => {
// check for login (if already logged in the action will simply resolve)
- console.debug('[E2E] Logging in for search');
+ console.debug('[E2E] Logging in for chat finder');
E2ELogin().then((neededLogin: boolean): Promise | undefined => {
if (neededLogin) {
@@ -19,24 +19,24 @@ const test = () => {
);
}
- console.debug('[E2E] Logged in, getting search metrics and submitting them…');
+ console.debug('[E2E] Logged in, getting chat finder metrics and submitting them…');
Performance.subscribeToMeasurements((entry) => {
if (entry.name === CONST.TIMING.SIDEBAR_LOADED) {
- console.debug(`[E2E] Sidebar loaded, navigating to search route…`);
- Navigation.navigate(ROUTES.SEARCH);
+ console.debug(`[E2E] Sidebar loaded, navigating to chat finder route…`);
+ Navigation.navigate(ROUTES.CHAT_FINDER);
return;
}
console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`);
- if (entry.name !== CONST.TIMING.SEARCH_RENDER) {
+ if (entry.name !== CONST.TIMING.CHAT_FINDER_RENDER) {
return;
}
console.debug(`[E2E] Submitting!`);
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: 'Open Search Page TTI',
+ name: 'Open Chat Finder Page TTI',
duration: entry.duration,
})
.then(() => {
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index 415872750243..6a2051db0ed0 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -1,5 +1,5 @@
import type {OnyxEntry} from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
+import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Report, Transaction} from '@src/types/onyx';
@@ -8,7 +8,11 @@ import * as CurrencyUtils from './CurrencyUtils';
import Navigation from './Navigation/Navigation';
import * as TransactionUtils from './TransactionUtils';
-function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: ValueOf, transactionID: string, reportID: string) {
+function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: IOUType, transactionID: string, reportID: string, iouAction?: IOUAction): void {
+ if (iouAction === CONST.IOU.ACTION.CATEGORIZE || iouAction === CONST.IOU.ACTION.MOVE) {
+ Navigation.goBack();
+ return;
+ }
// If the participants were automatically added to the transaction, then the user needs taken back to the starting step
switch (requestType) {
case CONST.IOU.REQUEST_TYPE.DISTANCE:
@@ -53,8 +57,8 @@ function calculateAmount(numberOfParticipants: number, total: number, currency:
* For example: if user1 owes user2 $10, then we have: {ownerAccountID: user2, managerID: user1, total: $10 (a positive amount, owed to user2)}
* If user1 requests $17 from user2, then we have: {ownerAccountID: user1, managerID: user2, total: $7 (still a positive amount, but now owed to user1)}
*
- * @param isDeleting - whether the user is deleting the request
- * @param isUpdating - whether the user is updating the request
+ * @param isDeleting - whether the user is deleting the expense
+ * @param isUpdating - whether the user is updating the expense
*/
function updateIOUOwnerAndTotal>(
iouReport: TReport,
@@ -92,7 +96,7 @@ function updateIOUOwnerAndTotal>(
}
/**
- * Returns whether or not an IOU report contains money requests in a different currency
+ * Returns whether or not an IOU report contains expenses in a different currency
* that are either created or cancelled offline, and thus haven't been converted to the report's currency yet
*/
function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean {
@@ -124,4 +128,20 @@ function insertTagIntoTransactionTagsString(transactionTags: string, tag: string
return tagArray.join(CONST.COLON).replace(/:*$/, '');
}
-export {calculateAmount, updateIOUOwnerAndTotal, isIOUReportPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, insertTagIntoTransactionTagsString};
+function isMovingTransactionFromTrackExpense(action?: IOUAction) {
+ if (action === CONST.IOU.ACTION.MOVE || action === CONST.IOU.ACTION.SHARE || action === CONST.IOU.ACTION.CATEGORIZE) {
+ return true;
+ }
+
+ return false;
+}
+
+export {
+ calculateAmount,
+ insertTagIntoTransactionTagsString,
+ isIOUReportPendingCurrencyConversion,
+ isMovingTransactionFromTrackExpense,
+ isValidMoneyRequestType,
+ navigateToStartMoneyRequestStep,
+ updateIOUOwnerAndTotal,
+};
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 0d961ea27115..d73771734636 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -253,7 +253,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
buildMessageFragmentForValue(
reportActionOriginalMessage?.billable ?? '',
reportActionOriginalMessage?.oldBillable ?? '',
- Localize.translateLocal('iou.request'),
+ Localize.translateLocal('iou.expense'),
true,
setFragments,
removalFragments,
@@ -266,7 +266,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) +
getMessageLine(`\n${Localize.translateLocal('iou.removed')}`, removalFragments);
if (message === '') {
- return Localize.translateLocal('iou.changedTheRequest');
+ return Localize.translateLocal('iou.changedTheExpense');
}
return `${message.substring(1, message.length)}`;
}
diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts
index 1a573ce74628..4e681e016b6b 100644
--- a/src/libs/MoneyRequestUtils.ts
+++ b/src/libs/MoneyRequestUtils.ts
@@ -1,5 +1,5 @@
import type {OnyxEntry} from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
+import type {IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import type {SelectedTabRequest} from '@src/types/onyx';
@@ -78,17 +78,17 @@ function replaceAllDigits(text: string, convertFn: (char: string) => string): st
}
/**
- * Check if distance request or not
+ * Check if distance expense or not
*/
-function isDistanceRequest(iouType: ValueOf, selectedTab: OnyxEntry): boolean {
+function isDistanceRequest(iouType: IOUType, selectedTab: OnyxEntry): boolean {
return iouType === CONST.IOU.TYPE.REQUEST && selectedTab === CONST.TAB_REQUEST.DISTANCE;
}
/**
- * Check if scan request or not
+ * Check if scan expense or not
*/
function isScanRequest(selectedTab: SelectedTabRequest): boolean {
return selectedTab === CONST.TAB_REQUEST.SCAN;
}
-export {stripCommaFromAmount, stripDecimalsFromAmount, stripSpacesFromAmount, addLeadingZero, validateAmount, replaceAllDigits, isDistanceRequest, isScanRequest};
+export {addLeadingZero, isDistanceRequest, isScanRequest, replaceAllDigits, stripCommaFromAmount, stripDecimalsFromAmount, stripSpacesFromAmount, validateAmount};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index fde0202d3d2f..9157d7486c9e 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -238,7 +238,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
const unsubscribeSearchShortcut = KeyboardShortcut.subscribe(
searchShortcutConfig.shortcutKey,
() => {
- Modal.close(Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.SEARCH)));
+ Modal.close(Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER)));
},
shortcutsOverviewShortcutConfig.descriptionKey,
shortcutsOverviewShortcutConfig.modifiers,
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx
index 2dce4247c7ae..172b62239131 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx
@@ -52,9 +52,9 @@ function WorkspaceSettingsModalStackNavigator() {
/>
require('@pages/workspace/accounting/WorkspaceAccountingPage').default as React.ComponentType}
+ key={SCREENS.WORKSPACE.ACCOUNTING.ROOT}
+ name={SCREENS.WORKSPACE.ACCOUNTING.ROOT}
+ getComponent={() => require('@pages/workspace/accounting/PolicyAccountingPage').default as React.ComponentType}
/>
require('../../../../pages/RoomInvitePage').default as React.ComponentType,
});
-const SearchModalStackNavigator = createModalStackNavigator({
- [SCREENS.SEARCH_ROOT]: () => require('../../../../pages/SearchPage').default as React.ComponentType,
+const ChatFinderModalStackNavigator = createModalStackNavigator({
+ [SCREENS.CHAT_FINDER_ROOT]: () => require('../../../../pages/ChatFinderPage').default as React.ComponentType,
});
const NewChatModalStackNavigator = createModalStackNavigator({
@@ -262,12 +262,13 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/ExitSurvey/ExitSurveyReasonPage').default as React.ComponentType,
[SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: () => require('../../../../pages/settings/ExitSurvey/ExitSurveyResponsePage').default as React.ComponentType,
[SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: () => require('../../../../pages/settings/ExitSurvey/ExitSurveyConfirmPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_IMPORT]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksImportPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CUSTOMERS]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksCustomersPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_TAXES]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksTaxesPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_LOCATIONS]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksLocationsPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CLASSES]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksClassesPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_IMPORT]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksImportPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: () =>
+ require('../../../../pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CUSTOMERS]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksCustomersPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_TAXES]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksTaxesPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksLocationsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES]: () => require('../../../../pages/workspace/accounting/qbo/QuickbooksClassesPage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () =>
require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType,
@@ -342,7 +343,7 @@ export {
ReportDescriptionModalStackNavigator,
RoomInviteModalStackNavigator,
RoomMembersModalStackNavigator,
- SearchModalStackNavigator,
+ ChatFinderModalStackNavigator,
SettingsModalStackNavigator,
SignInModalStackNavigator,
SplitDetailsModalStackNavigator,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/ActiveRouteContext.ts b/src/libs/Navigation/AppNavigator/Navigators/ActiveRouteContext.ts
index d1d14d43af1a..6d0de3211e7c 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/ActiveRouteContext.ts
+++ b/src/libs/Navigation/AppNavigator/Navigators/ActiveRouteContext.ts
@@ -1,5 +1,6 @@
import React from 'react';
+import type {CentralPaneNavigatorParamList, NavigationPartialRoute} from '@libs/Navigation/types';
-const ActiveRouteContext = React.createContext('');
+const ActiveRouteContext = React.createContext | undefined>(undefined);
export default ActiveRouteContext;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
index 87a441f16ddb..6680ea302441 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
@@ -5,6 +5,7 @@ import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/create
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import type {BottomTabNavigatorParamList} from '@libs/Navigation/types';
import SidebarScreen from '@pages/home/sidebar/SidebarScreen';
+import SearchPageBottomTab from '@pages/Search/SearchPageBottomTab';
import SCREENS from '@src/SCREENS';
import ActiveRouteContext from './ActiveRouteContext';
@@ -21,12 +22,16 @@ function BottomTabNavigator() {
const activeRoute = useNavigationState(getTopmostCentralPaneRoute);
return (
-
+
+
();
@@ -41,6 +42,12 @@ function BaseCentralPaneNavigator() {
initialParams={{openOnAdminRoom: openOnAdminRoom === 'true' || undefined}}
component={ReportScreenWrapper}
/>
+
+
{Object.entries(settingsScreens).map(([screenName, componentGetter]) => (
;
- /** Members of all the workspaces the user is member of */
- policyMembers: OnyxCollection;
+ /** The personal details of the person who is logged in */
+ personalDetails: OnyxEntry;
/** Whether user is a new user */
isFirstTimeNewExpensifyUser: OnyxEntry;
@@ -58,7 +58,7 @@ const getLastAccessedReportID = (
};
// This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params
-function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) {
+function ReportScreenIDSetter({route, reports, policies, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID, personalDetails}: ReportScreenIDSetterProps) {
const {canUseDefaultRooms} = usePermissions();
const {activeWorkspaceID} = useActiveWorkspace();
@@ -73,7 +73,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav
return;
}
- const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, accountID);
+ const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID);
// If there is no reportID in route, try to find last accessed and use it for setParams
const reportID = getLastAccessedReportID(
@@ -92,7 +92,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav
if (reportID) {
navigation.setParams({reportID: String(reportID)});
}
- }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, reportMetadata, activeWorkspaceID, policyMembers, accountID]);
+ }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, reportMetadata, activeWorkspaceID, personalDetails, accountID]);
// The ReportScreen without the reportID set will display a skeleton
// until the reportID is loaded and set in the route param
@@ -110,10 +110,6 @@ export default withOnyx session?.accountID,
},
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
})(ReportScreenIDSetter);
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
index 391468578fab..02bfda6ba51b 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
@@ -13,6 +13,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Session from '@libs/actions/Session';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
+import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import Navigation from '@libs/Navigation/Navigation';
import type {RootStackParamList, State} from '@libs/Navigation/types';
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
@@ -64,6 +65,12 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
// Parent navigator of the bottom tab bar is the root navigator.
const currentTabName = useNavigationState((state) => {
+ const topmostCentralPaneRoute = getTopmostCentralPaneRoute(state);
+
+ if (topmostCentralPaneRoute && topmostCentralPaneRoute.name === SCREENS.SEARCH.CENTRAL_PANE) {
+ return SCREENS.SEARCH.CENTRAL_PANE;
+ }
+
const topmostBottomTabRoute = getTopmostBottomTabRoute(state);
return topmostBottomTabRoute?.name ?? SCREENS.HOME;
});
@@ -95,7 +102,6 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
-
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
index fd5282a8cfcd..a06c9d08c529 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
@@ -66,7 +66,7 @@ function TopBar({policy, session}: TopBarProps) {
Navigation.navigate(ROUTES.SEARCH))}
+ onPress={Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER))}
>
,
- ResponsiveStackNavigatorRouterOptions,
- StackActionHelpers,
- StackNavigationOptions,
- StackNavigationEventMap
- >(CustomRouter, {
- children: props.children,
- screenOptions: props.screenOptions,
- initialRouteName: props.initialRouteName,
- });
-
- return (
-
-
-
- );
-}
-
-ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator';
-
-export default createNavigatorFactory, StackNavigationOptions, StackNavigationEventMap, typeof ResponsiveStackNavigator>(ResponsiveStackNavigator);
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx
index 2a517c45eb0d..29b9b1072d3d 100644
--- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx
@@ -3,21 +3,26 @@ import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/na
import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack';
import {StackView} from '@react-navigation/stack';
import React, {useEffect, useMemo} from 'react';
+import {View} from 'react-native';
+import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import navigationRef from '@libs/Navigation/navigationRef';
+import type {RootStackParamList, State} from '@libs/Navigation/types';
import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
import CustomRouter from './CustomRouter';
import type {ResponsiveStackNavigatorProps, ResponsiveStackNavigatorRouterOptions} from './types';
type Routes = StackNavigationState['routes'];
-function reduceReportRoutes(routes: Routes): Routes {
+function reduceCentralPaneRoutes(routes: Routes): Routes {
const result: Routes = [];
let count = 0;
const reverseRoutes = [...routes].reverse();
reverseRoutes.forEach((route) => {
if (route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR) {
- // Remove all report routes except the last 3. This will improve performance.
+ // Remove all central pane routes except the last 3. This will improve performance.
if (count < 3) {
result.push(route);
count++;
@@ -32,6 +37,7 @@ function reduceReportRoutes(routes: Routes): Routes {
function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) {
const {isSmallScreenWidth} = useWindowDimensions();
+ const styles = useThemeStyles();
const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder<
StackNavigationState,
@@ -52,15 +58,35 @@ function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) {
navigationRef.resetRoot(navigationRef.getRootState());
}, [isSmallScreenWidth]);
- const stateToRender = useMemo(() => {
- const result = reduceReportRoutes(state.routes);
+ const {stateToRender, searchRoute} = useMemo(() => {
+ const routes = reduceCentralPaneRoutes(state.routes);
+
+ const lastRoute = routes[routes.length - 1];
+ const isLastRouteSearchRoute = getTopmostCentralPaneRoute({routes: [lastRoute]} as State)?.name === SCREENS.SEARCH.CENTRAL_PANE;
+
+ const firstRoute = routes[0];
+
+ // On narrow layout, if we are on /search route we want to hide all central pane routes and show only the bottom tab navigator.
+ if (isSmallScreenWidth && isLastRouteSearchRoute) {
+ return {
+ stateToRender: {
+ ...state,
+ index: 0,
+ routes: [firstRoute],
+ },
+ searchRoute: lastRoute,
+ };
+ }
return {
- ...state,
- index: result.length - 1,
- routes: [...result],
+ stateToRender: {
+ ...state,
+ index: routes.length - 1,
+ routes: [...routes],
+ },
+ searchRoute: undefined,
};
- }, [state]);
+ }, [state, isSmallScreenWidth]);
return (
@@ -71,6 +97,7 @@ function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) {
descriptors={descriptors}
navigation={navigation}
/>
+ {searchRoute && {descriptors[searchRoute.key].render()}}
);
}
diff --git a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
index 5732cb93f19c..4c18e161c9a9 100644
--- a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
+++ b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
@@ -3,20 +3,9 @@ import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRo
import getTopmostFullScreenRoute from '@libs/Navigation/getTopmostFullScreenRoute';
import type {Metainfo} from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
+import shallowCompare from '@libs/ObjectUtils';
import NAVIGATORS from '@src/NAVIGATORS';
-// eslint-disable-next-line @typescript-eslint/ban-types
-const shallowCompare = (obj1?: object, obj2?: object) => {
- if (!obj1 && !obj2) {
- return true;
- }
- if (obj1 && obj2) {
- // @ts-expect-error we know that obj1 and obj2 are params of a route.
- return Object.keys(obj1).length === Object.keys(obj2).length && Object.keys(obj1).every((key) => obj1[key] === obj2[key]);
- }
- return false;
-};
-
type GetPartialStateDiffReturnType = {
[NAVIGATORS.BOTTOM_TAB_NAVIGATOR]?: NavigationPartialRoute;
[NAVIGATORS.CENTRAL_PANE_NAVIGATOR]?: NavigationPartialRoute;
diff --git a/src/libs/Navigation/dismissModalWithReport.ts b/src/libs/Navigation/dismissModalWithReport.ts
index 2622cc2b9855..c0405c2c9da0 100644
--- a/src/libs/Navigation/dismissModalWithReport.ts
+++ b/src/libs/Navigation/dismissModalWithReport.ts
@@ -3,7 +3,7 @@ import type {NavigationContainerRef} from '@react-navigation/native';
import {StackActions} from '@react-navigation/native';
import {findLastIndex} from 'lodash';
import Log from '@libs/Log';
-import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils';
+import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
import {doesReportBelongToWorkspace} from '@libs/ReportUtils';
import NAVIGATORS from '@src/NAVIGATORS';
import ROUTES from '@src/ROUTES';
@@ -47,7 +47,7 @@ function dismissModalWithReport(targetReport: Report | EmptyObject, navigationRe
if (targetReport.reportID !== getTopmostReportId(state)) {
const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReport.reportID));
const policyID = getPolicyIDFromState(state as State);
- const policyMemberAccountIDs = getPolicyMemberAccountIDs(policyID);
+ const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID);
const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID);
if (shouldOpenAllWorkspace) {
diff --git a/src/libs/Navigation/getTopmostBottomTabRoute.ts b/src/libs/Navigation/getTopmostBottomTabRoute.ts
index a589f2cbc837..48a8d80f4096 100644
--- a/src/libs/Navigation/getTopmostBottomTabRoute.ts
+++ b/src/libs/Navigation/getTopmostBottomTabRoute.ts
@@ -1,10 +1,11 @@
+import NAVIGATORS from '@src/NAVIGATORS';
import type {BottomTabName, NavigationPartialRoute, RootStackParamList, State} from './types';
function getTopmostBottomTabRoute(state: State | undefined): NavigationPartialRoute | undefined {
- const bottomTabNavigatorRoute = state?.routes[0];
+ const bottomTabNavigatorRoute = state?.routes.findLast((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR);
// The bottomTabNavigatorRoute state may be empty if we just logged in.
- if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.name !== 'BottomTabNavigator' || bottomTabNavigatorRoute.state === undefined) {
+ if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR || bottomTabNavigatorRoute.state === undefined) {
return undefined;
}
diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts
index afa8fb56069a..863cb102add4 100644
--- a/src/libs/Navigation/linkTo.ts
+++ b/src/libs/Navigation/linkTo.ts
@@ -2,6 +2,7 @@ import {getActionFromState} from '@react-navigation/core';
import type {NavigationAction, NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native';
import type {Writable} from 'type-fest';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
+import shallowCompare from '@libs/ObjectUtils';
import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
@@ -13,7 +14,6 @@ import getPolicyIDFromState from './getPolicyIDFromState';
import getStateFromPath from './getStateFromPath';
import getTopmostBottomTabRoute from './getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute';
-import getTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
import getAdaptedStateFromPath from './linkingConfig/getAdaptedStateFromPath';
import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState';
@@ -152,16 +152,15 @@ export default function linkTo(navigation: NavigationContainerRef> = {
SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET,
SCREENS.WORKSPACE.WORKFLOWS_PAYER,
],
- [SCREENS.WORKSPACE.ACCOUNTING]: [
- SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_IMPORT,
- SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS,
- SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CLASSES,
- SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_TAXES,
- SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_LOCATIONS,
- SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CUSTOMERS,
+ [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: [
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_IMPORT,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_TAXES,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CUSTOMERS,
],
[SCREENS.WORKSPACE.TAXES]: [
SCREENS.WORKSPACE.TAXES_SETTINGS,
diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
index 747f6e38968b..9e40a3dc0d4c 100755
--- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
@@ -3,6 +3,7 @@ import SCREENS from '@src/SCREENS';
const TAB_TO_CENTRAL_PANE_MAPPING: Record = {
[SCREENS.HOME]: [SCREENS.REPORT],
+ [SCREENS.SEARCH.BOTTOM_TAB]: [SCREENS.SEARCH.CENTRAL_PANE],
[SCREENS.SETTINGS.ROOT]: [
SCREENS.SETTINGS.PROFILE.ROOT,
SCREENS.SETTINGS.PREFERENCES.ROOT,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 6165ccb16fa3..05b7190fa181 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -65,15 +65,16 @@ const config: LinkingOptions['config'] = {
exact: true,
},
[SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES,
+ [SCREENS.SEARCH.CENTRAL_PANE]: ROUTES.SEARCH.route,
[SCREENS.SETTINGS.SAVE_THE_WORLD]: ROUTES.SETTINGS_SAVE_THE_WORLD,
},
},
[SCREENS.NOT_FOUND]: '*',
[NAVIGATORS.LEFT_MODAL_NAVIGATOR]: {
screens: {
- [SCREENS.LEFT_MODAL.SEARCH]: {
+ [SCREENS.LEFT_MODAL.CHAT_FINDER]: {
screens: {
- [SCREENS.SEARCH_ROOT]: ROUTES.SEARCH,
+ [SCREENS.CHAT_FINDER_ROOT]: ROUTES.CHAT_FINDER,
},
},
[SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: {
@@ -263,12 +264,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.CURRENCY]: {
path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route,
},
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_IMPORT]: {path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT.route},
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: {path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS.route},
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CLASSES]: {path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES.route},
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CUSTOMERS]: {path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS.route},
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_LOCATIONS]: {path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS.route},
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_TAXES]: {path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_TAXES.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CUSTOMERS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_TAXES]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_TAXES.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
},
@@ -669,8 +670,8 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.MEMBERS]: {
path: ROUTES.WORKSPACE_MEMBERS.route,
},
- [SCREENS.WORKSPACE.ACCOUNTING]: {
- path: ROUTES.WORKSPACE_ACCOUNTING.route,
+ [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: {
+ path: ROUTES.POLICY_ACCOUNTING.route,
},
[SCREENS.WORKSPACE.CATEGORIES]: {
path: ROUTES.WORKSPACE_CATEGORIES.route,
diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index d7bcfbb68952..f18637d8e52e 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -319,7 +319,9 @@ function getAdaptedState(state: PartialState
// Routes
// - found bottom tab
// - matching central pane on desktop layout
- if (isNarrowLayout) {
+
+ // We want to make sure that the bottom tab search page is always pushed with matching central pane page. Even on the narrow layout.
+ if (isNarrowLayout && bottomTabNavigator.state?.routes[0].name !== SCREENS.SEARCH.BOTTOM_TAB) {
return {
adaptedState: state,
metainfo,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 89bcddfe6daf..54eb5f4663bc 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -13,7 +13,7 @@ import type {
import type {ValueOf} from 'type-fest';
import type {IOURequestType} from '@libs/actions/IOU';
import type CONST from '@src/CONST';
-import type {Country} from '@src/CONST';
+import type {Country, IOUAction, IOUType} from '@src/CONST';
import type NAVIGATORS from '@src/NAVIGATORS';
import type {HybridAppRoute, Route as Routes} from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -61,6 +61,9 @@ type CentralPaneNavigatorParamList = {
[SCREENS.SETTINGS.ABOUT]: undefined;
[SCREENS.SETTINGS.TROUBLESHOOT]: undefined;
[SCREENS.SETTINGS.WORKSPACES]: undefined;
+ [SCREENS.SEARCH.CENTRAL_PANE]: {
+ query: string;
+ };
[SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined;
};
@@ -244,22 +247,22 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: {
policyID: string;
};
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_IMPORT]: {
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_IMPORT]: {
policyID: string;
};
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: {
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: {
policyID: string;
};
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_LOCATIONS]: {
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS]: {
policyID: string;
};
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CLASSES]: {
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES]: {
policyID: string;
};
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_CUSTOMERS]: {
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CUSTOMERS]: {
policyID: string;
};
- [SCREENS.WORKSPACE.QUICKBOOKS_ONLINE_TAXES]: {
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_TAXES]: {
policyID: string;
};
[SCREENS.GET_ASSISTANCE]: {
@@ -300,8 +303,8 @@ type NewChatNavigatorParamList = {
};
};
-type SearchNavigatorParamList = {
- [SCREENS.SEARCH_ROOT]: undefined;
+type ChatFinderNavigatorParamList = {
+ [SCREENS.CHAT_FINDER_ROOT]: undefined;
};
type DetailsNavigatorParamList = {
@@ -361,6 +364,7 @@ type RoomMembersNavigatorParamList = {
type RoomInviteNavigatorParamList = {
[SCREENS.ROOM_INVITE_ROOT]: {
reportID: string;
+ role?: 'accountant';
};
};
@@ -370,39 +374,39 @@ type MoneyRequestNavigatorParamList = {
reportID: string;
};
[SCREENS.MONEY_REQUEST.STEP_DATE]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
backTo: Routes;
reportActionID: string;
};
[SCREENS.MONEY_REQUEST.STEP_CATEGORY]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportActionID: string;
reportID: string;
backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
backTo: Routes;
currency?: string;
};
[SCREENS.MONEY_REQUEST.STEP_TAG]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
backTo: Routes;
@@ -410,23 +414,23 @@ type MoneyRequestNavigatorParamList = {
orderWeight: string;
};
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: {
- iouType: ValueOf;
+ iouType: IOUType;
reportID: string;
backTo: Routes | undefined;
- action: ValueOf;
+ action: IOUAction;
pageIndex: string;
transactionID: string;
};
[SCREENS.MONEY_REQUEST.STEP_MERCHANT]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
backTo: Routes;
@@ -441,8 +445,8 @@ type MoneyRequestNavigatorParamList = {
threadReportID: number;
};
[SCREENS.MONEY_REQUEST.STEP_DISTANCE]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
backTo: Routes;
@@ -452,7 +456,7 @@ type MoneyRequestNavigatorParamList = {
reportID: string;
};
[SCREENS.MONEY_REQUEST.CREATE]: {
- iouType: ValueOf;
+ iouType: IOUType;
reportID: string;
transactionID: string;
@@ -463,43 +467,44 @@ type MoneyRequestNavigatorParamList = {
currency: never;
};
[SCREENS.MONEY_REQUEST.START]: {
- iouType: ValueOf;
+ iouType: IOUType;
reportID: string;
transactionID: string;
iouRequestType: IOURequestType;
};
[SCREENS.MONEY_REQUEST.STEP_AMOUNT]: {
- iouType: ValueOf;
+ iouType: IOUType;
reportID: string;
transactionID: string;
backTo: Routes;
- action: ValueOf;
+ action: IOUAction;
currency?: string;
};
[SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: {
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
};
[SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
pageIndex?: string;
backTo?: string;
};
[SCREENS.MONEY_REQUEST.STEP_SCAN]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
pageIndex: number;
backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_CURRENCY]: {
- action: ValueOf;
- iouType: ValueOf;
+ action: IOUAction;
+ iouType: IOUType;
transactionID: string;
reportID: string;
pageIndex?: string;
@@ -612,7 +617,7 @@ type PrivateNotesNavigatorParamList = {
};
type LeftModalNavigatorParamList = {
- [SCREENS.LEFT_MODAL.SEARCH]: NavigatorScreenParams;
+ [SCREENS.LEFT_MODAL.CHAT_FINDER]: NavigatorScreenParams;
[SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: NavigatorScreenParams;
};
@@ -679,7 +684,7 @@ type WorkspacesCentralPaneNavigatorParamList = {
[SCREENS.WORKSPACE.MEMBERS]: {
policyID: string;
};
- [SCREENS.WORKSPACE.ACCOUNTING]: {
+ [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: {
policyID: string;
};
[SCREENS.WORKSPACE.CATEGORIES]: {
@@ -698,7 +703,7 @@ type WorkspacesCentralPaneNavigatorParamList = {
[SCREENS.WORKSPACE.DISTANCE_RATES]: {
policyID: string;
};
- [SCREENS.WORKSPACE.ACCOUNTING]: {
+ [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: {
policyID: string;
};
};
@@ -722,6 +727,7 @@ type WelcomeVideoModalNavigatorParamList = {
type BottomTabNavigatorParamList = {
[SCREENS.HOME]: undefined;
+ [SCREENS.SEARCH.BOTTOM_TAB]: undefined;
[SCREENS.SETTINGS.ROOT]: undefined;
};
@@ -788,7 +794,7 @@ type AuthScreensParamList = SharedScreensParamList & {
};
};
-type RootStackParamList = PublicScreensParamList & AuthScreensParamList & SearchNavigatorParamList;
+type RootStackParamList = PublicScreensParamList & AuthScreensParamList & ChatFinderNavigatorParamList;
type BottomTabName = keyof BottomTabNavigatorParamList;
@@ -803,54 +809,54 @@ type SwitchPolicyIDParams = {
};
export type {
- NavigationRef,
- StackNavigationAction,
- CentralPaneNavigatorParamList,
+ AddPersonalBankAccountNavigatorParamList,
+ AuthScreensParamList,
+ BackToParams,
BottomTabName,
+ BottomTabNavigatorParamList,
CentralPaneName,
+ CentralPaneNavigatorParamList,
+ ChatFinderNavigatorParamList,
+ DetailsNavigatorParamList,
+ EditRequestNavigatorParamList,
+ EnablePaymentsNavigatorParamList,
+ FlagCommentNavigatorParamList,
FullScreenName,
- RootStackParamList,
- StateOrRoute,
- NavigationStateRoute,
+ FullScreenNavigatorParamList,
+ LeftModalNavigatorParamList,
+ MoneyRequestNavigatorParamList,
NavigationPartialRoute,
+ NavigationRef,
NavigationRoot,
- AuthScreensParamList,
- BottomTabNavigatorParamList,
- LeftModalNavigatorParamList,
- RightModalNavigatorParamList,
+ NavigationStateRoute,
+ NewChatNavigatorParamList,
+ NewTaskNavigatorParamList,
+ OnboardEngagementNavigatorParamList,
OnboardingModalNavigatorParamList,
- WelcomeVideoModalNavigatorParamList,
- PublicScreensParamList,
- MoneyRequestNavigatorParamList,
- SplitDetailsNavigatorParamList,
- DetailsNavigatorParamList,
+ ParticipantsNavigatorParamList,
+ PrivateNotesNavigatorParamList,
ProfileNavigatorParamList,
+ PublicScreensParamList,
+ ReferralDetailsNavigatorParamList,
+ ReimbursementAccountNavigatorParamList,
+ ReportDescriptionNavigatorParamList,
ReportDetailsNavigatorParamList,
ReportSettingsNavigatorParamList,
- TaskDetailsNavigatorParamList,
- ReportDescriptionNavigatorParamList,
- ParticipantsNavigatorParamList,
- RoomMembersNavigatorParamList,
+ RightModalNavigatorParamList,
RoomInviteNavigatorParamList,
- SearchNavigatorParamList,
- NewChatNavigatorParamList,
- NewTaskNavigatorParamList,
- TeachersUniteNavigatorParamList,
+ RoomMembersNavigatorParamList,
+ RootStackParamList,
SettingsNavigatorParamList,
- EnablePaymentsNavigatorParamList,
- AddPersonalBankAccountNavigatorParamList,
- WalletStatementNavigatorParamList,
- FlagCommentNavigatorParamList,
- EditRequestNavigatorParamList,
- PrivateNotesNavigatorParamList,
SignInNavigatorParamList,
- ReferralDetailsNavigatorParamList,
- ReimbursementAccountNavigatorParamList,
+ SplitDetailsNavigatorParamList,
+ StackNavigationAction,
State,
- WorkspaceSwitcherNavigatorParamList,
- OnboardEngagementNavigatorParamList,
+ StateOrRoute,
SwitchPolicyIDParams,
- FullScreenNavigatorParamList,
+ TaskDetailsNavigatorParamList,
+ TeachersUniteNavigatorParamList,
+ WalletStatementNavigatorParamList,
+ WelcomeVideoModalNavigatorParamList,
+ WorkspaceSwitcherNavigatorParamList,
WorkspacesCentralPaneNavigatorParamList,
- BackToParams,
};
diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts
index 0e76596ba8fa..3b8fce748f45 100644
--- a/src/libs/NextStepUtils.ts
+++ b/src/libs/NextStepUtils.ts
@@ -230,7 +230,7 @@ function buildNextStep(
};
// Self review & another reviewer
- if (isOwner) {
+ if (!isSelfApproval || (isSelfApproval && isOwner)) {
optimisticNextStep.message = [
{
text: 'Waiting for ',
diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
index 36051fa35c56..4b17adf86841 100644
--- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
+++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
@@ -3,7 +3,7 @@ import * as OnyxUpdates from '@libs/actions/OnyxUpdates';
import * as ActiveClientManager from '@libs/ActiveClientManager';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
-import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils';
+import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
import {extractPolicyIDFromPath} from '@libs/PolicyUtils';
import {doesReportBelongToWorkspace, getReport} from '@libs/ReportUtils';
import Visibility from '@libs/Visibility';
@@ -65,9 +65,9 @@ export default function subscribeToReportCommentPushNotifications() {
const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath);
const report = getReport(reportID.toString());
- const policyMembersAccountIDs = policyID ? getPolicyMemberAccountIDs(policyID) : [];
+ const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : [];
- const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyMembersAccountIDs, policyID);
+ const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID);
Log.info('[PushNotification] onSelected() - called', false, {reportID, reportActionID});
Navigation.isNavigationReady()
diff --git a/src/libs/ObjectUtils.ts b/src/libs/ObjectUtils.ts
new file mode 100644
index 000000000000..9ffa461506c8
--- /dev/null
+++ b/src/libs/ObjectUtils.ts
@@ -0,0 +1,13 @@
+// eslint-disable-next-line @typescript-eslint/ban-types
+const shallowCompare = (obj1?: object, obj2?: object) => {
+ if (!obj1 && !obj2) {
+ return true;
+ }
+ if (obj1 && obj2) {
+ // @ts-expect-error we know that obj1 and obj2 are params of a route.
+ return Object.keys(obj1).length === Object.keys(obj2).length && Object.keys(obj1).every((key) => obj1[key] === obj2[key]);
+ }
+ return false;
+};
+
+export default shallowCompare;
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index aa16d7b2dc5a..c11a1499a88f 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -117,6 +117,7 @@ type TaxSection = {
type CategoryTreeSection = CategorySectionBase & {
data: OptionTree[];
+ indexOffset?: number;
};
type Category = {
@@ -1023,11 +1024,13 @@ function getCategoryListSections(
const numberOfEnabledCategories = enabledCategories.length;
if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) {
+ const data = getCategoryOptionTree(selectedOptions, true);
categorySections.push({
// "Selected" section
title: '',
shouldShow: false,
- data: getCategoryOptionTree(selectedOptions, true),
+ data,
+ indexOffset: data.length,
});
return categorySections;
@@ -1046,22 +1049,26 @@ function getCategoryListSections(
});
});
+ const data = getCategoryOptionTree(searchCategories, true);
categorySections.push({
// "Search" section
title: '',
shouldShow: true,
- data: getCategoryOptionTree(searchCategories, true),
+ data,
+ indexOffset: data.length,
});
return categorySections;
}
if (selectedOptions.length > 0) {
+ const data = getCategoryOptionTree(selectedOptions, true);
categorySections.push({
// "Selected" section
title: '',
shouldShow: false,
- data: getCategoryOptionTree(selectedOptions, true),
+ data,
+ indexOffset: data.length,
});
}
@@ -1069,11 +1076,13 @@ function getCategoryListSections(
const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name));
if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) {
+ const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames);
categorySections.push({
// "All" section when items amount less than the threshold
title: '',
shouldShow: false,
- data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames),
+ data,
+ indexOffset: data.length,
});
return categorySections;
@@ -1089,19 +1098,23 @@ function getCategoryListSections(
if (filteredRecentlyUsedCategories.length > 0) {
const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow);
+ const data = getCategoryOptionTree(cutRecentlyUsedCategories, true);
categorySections.push({
// "Recent" section
title: Localize.translateLocal('common.recent'),
shouldShow: true,
- data: getCategoryOptionTree(cutRecentlyUsedCategories, true),
+ data,
+ indexOffset: data.length,
});
}
+ const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames);
categorySections.push({
// "All" section when items amount more than the threshold
title: Localize.translateLocal('common.all'),
shouldShow: true,
- data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames),
+ data,
+ indexOffset: data.length,
});
return categorySections;
@@ -1707,7 +1720,7 @@ function getOptions(
return;
}
- // In case user needs to add credit bank account, don't allow them to request more money from the workspace.
+ // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace.
if (includeOwnedWorkspaceChats && ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(report)) {
return;
}
@@ -2356,4 +2369,4 @@ export {
getFirstKeyForList,
};
-export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option};
+export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree};
diff --git a/src/libs/PolicyEmployeeListUtils.ts b/src/libs/PolicyEmployeeListUtils.ts
new file mode 100644
index 000000000000..305ffda87e28
--- /dev/null
+++ b/src/libs/PolicyEmployeeListUtils.ts
@@ -0,0 +1,25 @@
+import type {OnyxCollection} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy} from '@src/types/onyx';
+import {getCurrentUserAccountID} from './actions/Report';
+import {getPolicyEmployeeListByIdWithoutCurrentUser} from './PolicyUtils';
+
+let allPolicies: OnyxCollection = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.POLICY,
+ waitForCollectionCallback: true,
+ callback: (value) => (allPolicies = value),
+});
+
+function getPolicyEmployeeAccountIDs(policyID?: string) {
+ if (!policyID) {
+ return [];
+ }
+
+ const currentUserAccountID = getCurrentUserAccountID();
+
+ return getPolicyEmployeeListByIdWithoutCurrentUser(allPolicies, policyID, currentUserAccountID);
+}
+
+export default getPolicyEmployeeAccountIDs;
diff --git a/src/libs/PolicyMembersUtils.ts b/src/libs/PolicyMembersUtils.ts
deleted file mode 100644
index 4376de150f17..000000000000
--- a/src/libs/PolicyMembersUtils.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type {OnyxCollection} from 'react-native-onyx';
-import Onyx from 'react-native-onyx';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {PolicyMembers} from '@src/types/onyx';
-import {getCurrentUserAccountID} from './actions/Report';
-import {getPolicyMembersByIdWithoutCurrentUser} from './PolicyUtils';
-
-let policyMembers: OnyxCollection;
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.POLICY_MEMBERS,
- waitForCollectionCallback: true,
- callback: (value) => (policyMembers = value),
-});
-
-function getPolicyMemberAccountIDs(policyID?: string) {
- if (!policyID) {
- return [];
- }
-
- const currentUserAccountID = getCurrentUserAccountID();
-
- return getPolicyMembersByIdWithoutCurrentUser(policyMembers, policyID, currentUserAccountID);
-}
-
-export default getPolicyMemberAccountIDs;
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index b162a7c6df93..b4cf4b164a19 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -4,13 +4,14 @@ import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx';
+import type {Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx';
import type {PolicyFeatureName, Rate} from '@src/types/onyx/Policy';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import getPolicyIDFromState from './Navigation/getPolicyIDFromState';
import Navigation, {navigationRef} from './Navigation/Navigation';
import type {RootStackParamList, State} from './Navigation/types';
+import {getPersonalDetailByEmail} from './PersonalDetailsUtils';
type MemberEmailsToAccountIDs = Record;
@@ -25,11 +26,10 @@ function getActivePolicies(policies: OnyxCollection): Policy[] | undefin
}
/**
- * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not.
- * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...}
+ * Checks if we have any errors stored within the policy?.employeeList. Determines whether we should show a red brick road error or not.
*/
-function hasPolicyMemberError(policyMembers: OnyxEntry): boolean {
- return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0);
+function hasEmployeeListError(policy: OnyxEntry): boolean {
+ return Object.values(policy?.employeeList ?? {}).some((employee) => Object.keys(employee?.errors ?? {}).length > 0);
}
/**
@@ -90,9 +90,8 @@ function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate
/**
* Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error.
*/
-function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMembersCollection: OnyxCollection): ValueOf | undefined {
- const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`] ?? {};
- if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) {
+function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry): ValueOf | undefined {
+ if (hasEmployeeListError(policy) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) {
return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
}
return undefined;
@@ -128,7 +127,7 @@ const isPolicyAdmin = (policy: OnyxEntry | EmptyObject): boolean => poli
*/
const isFreeGroupPolicy = (policy: OnyxEntry | EmptyObject): boolean => policy?.type === CONST.POLICY.TYPE.FREE;
-const isPolicyMember = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID);
+const isPolicyEmployee = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID);
/**
* Checks if the current user is an owner (creator) of the policy.
@@ -140,18 +139,19 @@ const isPolicyOwner = (policy: OnyxEntry, currentUserAccountID: number):
*
* We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error.
*/
-function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs {
+function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | undefined): MemberEmailsToAccountIDs {
+ const members = employeeList ?? {};
const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {};
- Object.keys(policyMembers ?? {}).forEach((accountID) => {
- const member = policyMembers?.[accountID];
+ Object.keys(members).forEach((email) => {
+ const member = members?.[email];
if (Object.keys(member?.errors ?? {})?.length > 0) {
return;
}
- const personalDetail = personalDetails?.[accountID];
+ const personalDetail = getPersonalDetailByEmail(email);
if (!personalDetail?.login) {
return;
}
- memberEmailsToAccountIDs[personalDetail.login] = Number(accountID);
+ memberEmailsToAccountIDs[email] = Number(personalDetail.accountID);
});
return memberEmailsToAccountIDs;
}
@@ -159,19 +159,19 @@ function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry
/**
* Get login list that we should not show in the workspace invite options
*/
-function getIneligibleInvitees(policyMembers: OnyxEntry, personalDetails: OnyxEntry): string[] {
+function getIneligibleInvitees(employeeList?: PolicyEmployeeList): string[] {
+ const policyEmployeeList = employeeList ?? {};
const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS];
- Object.keys(policyMembers ?? {}).forEach((accountID) => {
- const policyMember = policyMembers?.[accountID];
+ Object.keys(policyEmployeeList).forEach((email) => {
+ const policyEmployee = policyEmployeeList?.[email];
// Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them).
- if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) {
+ if (policyEmployee?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyEmployee?.errors ?? {}).length > 0) {
return;
}
- const memberEmail = personalDetails?.[accountID]?.login;
- if (!memberEmail) {
+ if (!email) {
return;
}
- memberEmailsToExclude.push(memberEmail);
+ memberEmailsToExclude.push(email);
});
return memberEmailsToExclude;
@@ -279,12 +279,12 @@ function getPathWithoutPolicyID(path: string) {
return path.replace(CONST.REGEX.PATH_WITHOUT_POLICY_ID, '/');
}
-function getPolicyMembersByIdWithoutCurrentUser(policyMembers: OnyxCollection, currentPolicyID?: string, currentUserAccountID?: number) {
- return policyMembers
- ? Object.keys(policyMembers[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${currentPolicyID}`] ?? {})
- .map((policyMemberAccountID) => Number(policyMemberAccountID))
- .filter((policyMemberAccountID) => policyMemberAccountID !== currentUserAccountID)
- : [];
+function getPolicyEmployeeListByIdWithoutCurrentUser(policies: OnyxCollection>, currentPolicyID?: string, currentUserAccountID?: number) {
+ const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${currentPolicyID}`] ?? null;
+ const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList);
+ return Object.values(policyMemberEmailsToAccountIDs)
+ .map((policyMemberAccountID) => Number(policyMemberAccountID))
+ .filter((policyMemberAccountID) => policyMemberAccountID !== currentUserAccountID);
}
function goBackFromInvalidPolicy() {
@@ -321,7 +321,7 @@ function getPolicyIDFromNavigationState() {
export {
getActivePolicies,
hasAccountingConnections,
- hasPolicyMemberError,
+ hasEmployeeListError,
hasPolicyError,
hasPolicyErrorFields,
hasCustomUnitsError,
@@ -345,12 +345,12 @@ export {
getCleanedTagName,
getCountOfEnabledTagsOfList,
isPendingDeletePolicy,
- isPolicyMember,
+ isPolicyEmployee,
isPolicyOwner,
isPaidGroupPolicy,
extractPolicyIDFromPath,
getPathWithoutPolicyID,
- getPolicyMembersByIdWithoutCurrentUser,
+ getPolicyEmployeeListByIdWithoutCurrentUser,
goBackFromInvalidPolicy,
isPolicyFeatureEnabled,
hasTaxRateError,
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index e8f2189f5f7d..d4a2afafb420 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -20,7 +20,6 @@ import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/Rep
import type ReportAction from '@src/types/onyx/ReportAction';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import * as CollectionUtils from './CollectionUtils';
import * as Environment from './Environment/Environment';
import isReportMessageAttachment from './isReportMessageAttachment';
import * as Localize from './Localize';
@@ -59,16 +58,16 @@ Onyx.connect({
},
});
-const allReportActions: OnyxCollection = {};
+let allReportActions: OnyxCollection;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
- callback: (actions, key) => {
- if (!key || !actions) {
+ waitForCollectionCallback: true,
+ callback: (actions) => {
+ if (!actions) {
return;
}
- const reportID = CollectionUtils.extractCollectionItemID(key);
- allReportActions[reportID] = actions;
+ allReportActions = actions;
},
});
@@ -195,7 +194,7 @@ function getParentReportAction(report: OnyxEntry | EmptyObject): ReportA
if (!report?.parentReportID || !report.parentReportActionID) {
return {};
}
- return allReportActions?.[report.parentReportID]?.[report.parentReportActionID] ?? {};
+ return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID] ?? {};
}
/**
@@ -225,7 +224,7 @@ function isTransactionThread(parentReportAction: OnyxEntry | Empty
/**
* Returns the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions with a childReportID. Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
*/
-function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[]): string | null {
+function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | null {
// If the report is not an IOU or Expense report, it shouldn't be treated as one-transaction report.
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE) {
@@ -250,7 +249,7 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn
action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
(iouRequestTypes.includes(action.originalMessage.type) ?? []) &&
action.childReportID &&
- action.originalMessage.IOUTransactionID,
+ (Boolean(action.originalMessage.IOUTransactionID) || (action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && (isOffline ?? isNetworkOffline))),
);
// If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report
@@ -518,7 +517,7 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key:
return false;
}
- // Ignore markedAsReimbursed action here since we're already display message that explains the request was paid
+ // Ignore markedAsReimbursed action here since we're already display message that explains the expense was paid
// elsewhere in the IOU reportAction
if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED) {
return false;
@@ -593,7 +592,7 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo
}
function getLastVisibleAction(reportID: string, actionsToMerge: OnyxCollection = {}): OnyxEntry {
- const reportActions = Object.values(fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge ?? {}, true));
+ const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true));
const visibleReportActions = Object.values(reportActions ?? {}).filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action));
const sortedReportActions = getSortedReportActions(visibleReportActions, true);
if (sortedReportActions.length === 0) {
@@ -714,7 +713,7 @@ function getLatestReportActionFromOnyxData(onyxData: OnyxUpdate[] | null): OnyxE
* Find the transaction associated with this reportAction, if one exists.
*/
function getLinkedTransactionID(reportActionOrID: string | OnyxEntry, reportID?: string): string | null {
- const reportAction = typeof reportActionOrID === 'string' ? allReportActions?.[reportID ?? '']?.[reportActionOrID] : reportActionOrID;
+ const reportAction = typeof reportActionOrID === 'string' ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]?.[reportActionOrID] : reportActionOrID;
if (!reportAction || reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) {
return null;
}
@@ -722,7 +721,7 @@ function getLinkedTransactionID(reportActionOrID: string | OnyxEntry {
- return allReportActions?.[reportID]?.[reportActionID] ?? null;
+ return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]?.[reportActionID] ?? null;
}
function getMostRecentReportActionLastModified(): string {
@@ -769,7 +768,7 @@ function getMostRecentReportActionLastModified(): string {
*/
function getReportPreviewAction(chatReportID: string, iouReportID: string): OnyxEntry {
return (
- Object.values(allReportActions?.[chatReportID] ?? {}).find(
+ Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`] ?? {}).find(
(reportAction) => reportAction && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && reportAction.originalMessage.linkedReportID === iouReportID,
) ?? null
);
@@ -794,7 +793,7 @@ function isMessageDeleted(reportAction: OnyxEntry): boolean {
}
/**
- * Returns the number of money requests associated with a report preview
+ * Returns the number of expenses associated with a report preview
*/
function getNumberOfMoneyRequests(reportPreviewAction: OnyxEntry): number {
return reportPreviewAction?.childMoneyRequestCount ?? 0;
@@ -823,7 +822,7 @@ function isTaskAction(reportAction: OnyxEntry): boolean {
* If there are no visible actions left (including system messages), we can hide the report from view entirely
*/
function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportActions = {}): boolean {
- const reportActions = Object.values(fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge, true));
+ const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge, true));
const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action));
// Exclude the task system message and the created message
@@ -832,7 +831,7 @@ function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportAc
}
function getAllReportActions(reportID: string): ReportActions {
- return allReportActions?.[reportID] ?? {};
+ return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {};
}
/**
@@ -997,7 +996,7 @@ function getMemberChangeMessagePlainText(reportAction: OnyxEntry):
}
/**
- * Helper method to determine if the provided accountID has made a request on the specified report.
+ * Helper method to determine if the provided accountID has submitted an expense on the specified report.
*
* @param reportID
* @param currentAccountID
@@ -1080,6 +1079,10 @@ function isActionableJoinRequest(reportAction: OnyxEntry): boolean
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST;
}
+function isActionableTrackExpense(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLETRACKEXPENSEWHISPER;
+}
+
/**
* Checks if any report actions correspond to a join request action that is still pending.
* @param reportID
@@ -1163,6 +1166,7 @@ export {
isCurrentActionUnread,
isActionableJoinRequest,
isActionableJoinRequestPending,
+ isActionableTrackExpense,
};
export type {LastVisibleMessage};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index f7b160bd67e2..36037414761b 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -13,6 +13,7 @@ import type {FileObject} from '@components/AttachmentModal';
import * as Expensicons from '@components/Icon/Expensicons';
import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars';
import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars';
+import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -53,8 +54,8 @@ import type {Receipt, TransactionChanges, WaypointCollection} from '@src/types/o
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
+import * as IOU from './actions/IOU';
import * as store from './actions/ReimbursementAccount/store';
-import * as CollectionUtils from './CollectionUtils';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import {hasValidDraftComment} from './DraftCommentUtils';
@@ -63,7 +64,6 @@ import isReportMessageAttachment from './isReportMessageAttachment';
import localeCompare from './LocaleCompare';
import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
-import Log from './Log';
import {isEmailPublicDomain} from './LoginUtils';
import ModifiedExpenseMessage from './ModifiedExpenseMessage';
import linkingConfig from './Navigation/linkingConfig';
@@ -190,6 +190,8 @@ type OptimisticIOUReportAction = Pick<
| 'receipt'
| 'whisperedToAccountIDs'
| 'childReportID'
+ | 'childVisibleActionCount'
+ | 'childCommenterCount'
>;
type ReportRouteParams = {
@@ -534,16 +536,15 @@ Onyx.connect({
},
});
-const reportActionsByReport: OnyxCollection = {};
+let allReportActions: OnyxCollection;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
- callback: (actions, key) => {
- if (!key || !actions) {
+ waitForCollectionCallback: true,
+ callback: (actions) => {
+ if (!actions) {
return;
}
-
- const reportID = CollectionUtils.extractCollectionItemID(key);
- reportActionsByReport[reportID] = actions;
+ allReportActions = actions;
},
});
@@ -1169,6 +1170,15 @@ function isJoinRequestInAdminRoom(report: OnyxEntry): boolean {
if (!report) {
return false;
}
+ // If this policy isn't owned by Expensify,
+ // Account manager/guide should not have the workspace join request pinned to their LHN,
+ // since they are not a part of the company, and should not action it on their behalf.
+ if (report.policyID) {
+ const policy = getPolicy(report.policyID);
+ if (!PolicyUtils.isExpensifyTeam(policy.owner) && PolicyUtils.isExpensifyTeam(currentUserPersonalDetails?.login)) {
+ return false;
+ }
+ }
return ReportActionsUtils.isActionableJoinRequestPending(report.reportID);
}
@@ -1317,7 +1327,7 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | EmptyObject | stri
* Checks if a report has only one transaction associated with it
*/
function isOneTransactionReport(reportID: string): boolean {
- const reportActions = reportActionsByReport?.[reportID] ?? ([] as ReportAction[]);
+ const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]);
return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null;
}
@@ -1325,7 +1335,7 @@ function isOneTransactionReport(reportID: string): boolean {
* Checks if a report is a transaction thread associated with a report that has only one transaction
*/
function isOneTransactionThread(reportID: string, parentReportID: string): boolean {
- const parentReportActions = reportActionsByReport?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]);
+ const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]);
const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportID, parentReportActions);
return reportID === transactionThreadReportID;
}
@@ -1349,7 +1359,7 @@ function isOneOnOneChat(report: OnyxEntry): boolean {
}
/**
- * Checks if the current user is a payer of the request
+ * Checks if the current user is a payer of the expense
*/
function isPayer(session: OnyxEntry, iouReport: OnyxEntry) {
@@ -1439,9 +1449,10 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID:
return false;
}
+ const linkedReport = isThreadFirstChat(reportAction, reportID) ? getReport(report?.parentReportID) : report;
if (isActionOwner) {
- if (!isEmptyObject(report) && isMoneyRequestReport(report)) {
- return canAddOrDeleteTransactions(report);
+ if (!isEmptyObject(linkedReport) && isMoneyRequestReport(linkedReport)) {
+ return canAddOrDeleteTransactions(linkedReport);
}
return true;
}
@@ -2127,7 +2138,7 @@ function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): bo
* Determines if the option requires action from the current user. This can happen when it:
* - is unread and the user was mentioned in one of the unread comments
* - is for an outstanding task waiting on the user
- * - has an outstanding child money request that is waiting for an action from the current user (e.g. pay, approve, add bank account)
+ * - has an outstanding child expense that is waiting for an action from the current user (e.g. pay, approve, add bank account)
*
* @param option (report or optionItem)
* @param parentReportAction (the report action the current report is a thread of)
@@ -2387,7 +2398,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry<
}
/**
- * Gets transaction created, amount, currency, comment, and waypoints (for distance request)
+ * Gets transaction created, amount, currency, comment, and waypoints (for distance expense)
* into a flat object. Used for displaying transactions and sending them in API commands
*/
@@ -2475,11 +2486,11 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean {
}
/**
- * Checks if the current user can edit the provided property of a money request
+ * Checks if the current user can edit the provided property of an expense
*
*/
function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, fieldToEdit: ValueOf): boolean {
- // A list of fields that cannot be edited by anyone, once a money request has been settled
+ // A list of fields that cannot be edited by anyone, once an expense has been settled
const restrictedFields: string[] = [
CONST.EDIT_REQUEST_FIELD.AMOUNT,
CONST.EDIT_REQUEST_FIELD.CURRENCY,
@@ -2533,7 +2544,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, field
*
* - It was written by the current user
* - It's an ADDCOMMENT that is not an attachment
- * - It's money request where conditions for editability are defined in canEditMoneyRequest method
+ * - It's an expense where conditions for editability are defined in canEditMoneyRequest method
* - It's not pending deletion
*/
function canEditReportAction(reportAction: OnyxEntry