diff --git a/.eslintrc.js b/.eslintrc.js
index 5451cfff6534..0661183101ab 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -194,6 +194,7 @@ module.exports = {
{
selector: ['parameter', 'method'],
format: ['camelCase', 'PascalCase'],
+ leadingUnderscore: 'allow',
},
],
'@typescript-eslint/ban-types': [
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index d34e4ebbf895..36b921570e7f 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -103,9 +103,9 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c
- [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases)
- [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
- [ ] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
-- [ ] If the PR modifies the form input styles:
+- [ ] If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
- [ ] I verified that all the inputs inside a form are aligned with each other.
- - [ ] I added `Design` label so the design team can review the changes.
+ - [ ] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes.
- [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page.
- [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps.
diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml
index 8a47ea4bb220..fd814ad69a7c 100644
--- a/.github/workflows/e2ePerformanceTests.yml
+++ b/.github/workflows/e2ePerformanceTests.yml
@@ -183,7 +183,7 @@ jobs:
run: npm run e2e-test-runner-build
- name: Copy e2e code into zip folder
- run: cp tests/e2e/dist/index.js zip/testRunner.js
+ run: cp tests/e2e/dist/index.js zip/testRunner.ts
- name: Zip everything in the zip directory up
run: zip -qr App.zip ./zip
diff --git a/android/app/build.gradle b/android/app/build.gradle
index ee8aa986ddf2..b792f7830ea4 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 1001045005
- versionName "1.4.50-5"
+ versionCode 1001045103
+ versionName "1.4.51-3"
}
flavorDimensions "default"
diff --git a/assets/fonts/web/ExpensifyNewKansas-Medium.woff b/assets/fonts/web/ExpensifyNewKansas-Medium.woff
index bd842c5ecb1d..9e4258763f58 100644
Binary files a/assets/fonts/web/ExpensifyNewKansas-Medium.woff and b/assets/fonts/web/ExpensifyNewKansas-Medium.woff differ
diff --git a/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 b/assets/fonts/web/ExpensifyNewKansas-Medium.woff2
index dba1df7e971e..1f65d0df0fcb 100644
Binary files a/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 and b/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 differ
diff --git a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff
index d3e7d9e82e15..5bab939ee71d 100644
Binary files a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff and b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff differ
diff --git a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2
index 94a0e04fa3b2..589edf3bc922 100644
Binary files a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 and b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 differ
diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md
index ab4b215516b9..4ff1f01b1475 100644
--- a/contributingGuides/REVIEWER_CHECKLIST.md
+++ b/contributingGuides/REVIEWER_CHECKLIST.md
@@ -51,9 +51,9 @@
- [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases)
- [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
- [ ] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
-- [ ] If the PR modifies the form input styles:
+- [ ] If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
- [ ] I verified that all the inputs inside a form are aligned with each other.
- - [ ] I added `Design` label so the design team can review the changes.
+ - [ ] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes.
- [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page.
- [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps.
- [ ] I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.
diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss
index ea18acef7c23..ec0f76801bc7 100644
--- a/docs/_sass/_main.scss
+++ b/docs/_sass/_main.scss
@@ -508,6 +508,7 @@ button {
.info {
padding: 12px;
+ margin-bottom: 20px;
border-radius: 8px;
background-color: $color-highlightBG;
color: $color-text;
diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
index 9940535e1fad..724745f458ef 100644
--- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
+++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
@@ -38,6 +38,10 @@ If you need to cancel your Expensify Card and cannot access the website or mobil
It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account.
+# Card Expiration Date
+
+If you notice that your card expiration date is soon, it's time for a new Expensify card. Expensify will automatically input a notification in your account's Home (Inbox) tab. This notice will ask you to input your address, but this is more if you have changed your address since your card was issued to you. You can ignore it and do nothing; the new Expensify card will ship to your address on file. The new Expensify card will have a new, unique card number and will not be associated with the old one.
+
{% include faq-begin.md %}
## What if I haven’t received my card after multiple weeks?
diff --git a/docs/expensify-classic/hubs/expense-and-report-features/index.html b/docs/expensify-classic/hubs/copilots-and-delegates/index.html
similarity index 58%
rename from docs/expensify-classic/hubs/expense-and-report-features/index.html
rename to docs/expensify-classic/hubs/copilots-and-delegates/index.html
index 44afa4b18b51..a2147a6a61ae 100644
--- a/docs/expensify-classic/hubs/expense-and-report-features/index.html
+++ b/docs/expensify-classic/hubs/copilots-and-delegates/index.html
@@ -1,6 +1,6 @@
---
layout: default
-title: Expense & Report Settings
+title: Copilots & Delegates
---
{% include hub.html %}
\ No newline at end of file
diff --git a/docs/expensify-classic/hubs/get-paid-back/index.html b/docs/expensify-classic/hubs/domains/index.html
similarity index 69%
rename from docs/expensify-classic/hubs/get-paid-back/index.html
rename to docs/expensify-classic/hubs/domains/index.html
index 1f84c1510b92..fd3b4727cce7 100644
--- a/docs/expensify-classic/hubs/get-paid-back/index.html
+++ b/docs/expensify-classic/hubs/domains/index.html
@@ -1,6 +1,6 @@
---
layout: default
-title: Get Paid Back
+title: Domains
---
{% include hub.html %}
\ No newline at end of file
diff --git a/docs/expensify-classic/hubs/get-paid-back/expenses.html b/docs/expensify-classic/hubs/expenses/expenses.html
similarity index 100%
rename from docs/expensify-classic/hubs/get-paid-back/expenses.html
rename to docs/expensify-classic/hubs/expenses/expenses.html
diff --git a/docs/expensify-classic/hubs/workspace-and-domain-settings/index.html b/docs/expensify-classic/hubs/expenses/index.html
similarity index 58%
rename from docs/expensify-classic/hubs/workspace-and-domain-settings/index.html
rename to docs/expensify-classic/hubs/expenses/index.html
index ffd514fcb6fa..66aa2c74ac57 100644
--- a/docs/expensify-classic/hubs/workspace-and-domain-settings/index.html
+++ b/docs/expensify-classic/hubs/expenses/index.html
@@ -1,6 +1,6 @@
---
layout: default
-title: Policy And Domain Settings
+title: Expenses
---
{% include hub.html %}
\ No newline at end of file
diff --git a/docs/expensify-classic/hubs/get-paid-back/reports.html b/docs/expensify-classic/hubs/expenses/reports.html
similarity index 100%
rename from docs/expensify-classic/hubs/get-paid-back/reports.html
rename to docs/expensify-classic/hubs/expenses/reports.html
diff --git a/docs/expensify-classic/hubs/manage-employees-and-report-approvals/index.html b/docs/expensify-classic/hubs/reports/index.html
similarity index 51%
rename from docs/expensify-classic/hubs/manage-employees-and-report-approvals/index.html
rename to docs/expensify-classic/hubs/reports/index.html
index 788e445ebc91..627274fc2391 100644
--- a/docs/expensify-classic/hubs/manage-employees-and-report-approvals/index.html
+++ b/docs/expensify-classic/hubs/reports/index.html
@@ -1,6 +1,6 @@
---
layout: default
-title: Manage Employees And Report Approvals
+title: Reports
---
{% include hub.html %}
\ No newline at end of file
diff --git a/docs/expensify-classic/hubs/workspaces/index.html b/docs/expensify-classic/hubs/workspaces/index.html
new file mode 100644
index 000000000000..436c9fcfecb1
--- /dev/null
+++ b/docs/expensify-classic/hubs/workspaces/index.html
@@ -0,0 +1,6 @@
+---
+layout: default
+title: Workspaces
+---
+
+{% include hub.html %}
\ No newline at end of file
diff --git a/docs/expensify-classic/hubs/workspace-and-domain-settings/reports.html b/docs/expensify-classic/hubs/workspaces/reports.html
similarity index 100%
rename from docs/expensify-classic/hubs/workspace-and-domain-settings/reports.html
rename to docs/expensify-classic/hubs/workspaces/reports.html
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 3c89e920e3f7..df4e2a45dce3 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -65,3 +65,4 @@ https://help.expensify.com/articles/expensify-classic/expensify-billing/Pay-Per-
https://help.expensify.com/articles/expensify-classic/expensify-billing/Individual-Subscription,https://use.expensify.com/
https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
+https://help.expensify.com/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager,https://use.expensify.com/support
diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg
index 9ca8eba7c94f..c4412cf650ee 100644
Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ
diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg
index b5751f0986fa..999442b550da 100644
Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 127f98edc070..ab5d359a5460 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.50
+ 1.4.51
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.50.5
+ 1.4.51.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 8a2fef8c99c6..ca9200c78376 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.50
+ 1.4.51
CFBundleSignature
????
CFBundleVersion
- 1.4.50.5
+ 1.4.51.3
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 34ef35759e15..f20b520b1480 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 1.4.50
+ 1.4.51
CFBundleVersion
- 1.4.50.5
+ 1.4.51.3
NSExtension
NSExtensionPointIdentifier
diff --git a/jest.config.js b/jest.config.js
index 5b36e44c7581..13645e720c8e 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -23,7 +23,7 @@ module.exports = {
},
testEnvironment: 'jsdom',
setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'],
- setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'],
+ setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.ts'],
cacheDirectory: '/.jest-cache',
moduleNameMapper: {
'\\.(lottie)$': '/__mocks__/fileMock.ts',
diff --git a/package-lock.json b/package-lock.json
index bc373abcd9b0..3083c15c0196 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.50-5",
+ "version": "1.4.51-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.50-5",
+ "version": "1.4.51-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -52,7 +52,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#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.10.1",
@@ -8045,153 +8045,6 @@
"integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==",
"license": "MIT"
},
- "node_modules/@onfido/active-video-capture": {
- "version": "0.28.6",
- "resolved": "https://registry.npmjs.org/@onfido/active-video-capture/-/active-video-capture-0.28.6.tgz",
- "integrity": "sha512-RFUeKaOSjj/amPp6VzhVkq/7kIkutEnnttT9n5KDeD3Vx8a09KD3a/xvxdQppveHlDAYsdBP6LrJwSSpjXiprg==",
- "dependencies": {
- "@mediapipe/face_detection": "^0.4.1646425229",
- "@mediapipe/face_mesh": "^0.4.1633559619",
- "@onfido/castor": "^2.2.2",
- "@onfido/castor-icons": "^2.12.0",
- "@tensorflow-models/face-detection": "^1.0.1",
- "@tensorflow-models/face-landmarks-detection": "^1.0.2",
- "@tensorflow/tfjs-backend-wasm": "3.20.0",
- "@tensorflow/tfjs-backend-webgl": "3.20.0",
- "@tensorflow/tfjs-converter": "3.20.0",
- "@tensorflow/tfjs-core": "3.20.0",
- "preact": "10.11.3",
- "react-webcam": "^7.2.0"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@onfido/active-video-capture/node_modules/@tensorflow-models/face-landmarks-detection": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@tensorflow-models/face-landmarks-detection/-/face-landmarks-detection-1.0.5.tgz",
- "integrity": "sha512-54XJPi8g29/MknJ33ZBrLsEzr9kw/dJtrJMMD3xrCrnRlfFQPIKQ5PI2Wml55Fz2p4U2hemzBB0/H+S94JddIQ==",
- "dependencies": {
- "rimraf": "^3.0.2"
- },
- "peerDependencies": {
- "@mediapipe/face_mesh": "~0.4.0",
- "@tensorflow-models/face-detection": "~1.0.0",
- "@tensorflow/tfjs-backend-webgl": "^3.12.0",
- "@tensorflow/tfjs-converter": "^3.12.0",
- "@tensorflow/tfjs-core": "^3.12.0"
- }
- },
- "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-cpu": {
- "version": "3.20.0",
- "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.20.0.tgz",
- "integrity": "sha512-gf075YaBLwSAAiUwa0D4GvYyUBhbJ1BVSivUNQmUfGKvIr2lIhF0qstBr033YTc3lhkbFSHEEPAHh/EfpqyjXQ==",
- "dependencies": {
- "@types/seedrandom": "^2.4.28",
- "seedrandom": "^3.0.5"
- },
- "engines": {
- "yarn": ">= 1.3.2"
- },
- "peerDependencies": {
- "@tensorflow/tfjs-core": "3.20.0"
- }
- },
- "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-wasm": {
- "version": "3.20.0",
- "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-wasm/-/tfjs-backend-wasm-3.20.0.tgz",
- "integrity": "sha512-k+sDcrcPtGToLjKRffgtSqlcN4MC6g4hXWRarZfgvvyvFqpxVfVqrGYHGTirXdN47sKYhmcTSMvbM2quGaaQnA==",
- "dependencies": {
- "@tensorflow/tfjs-backend-cpu": "3.20.0",
- "@types/emscripten": "~0.0.34"
- },
- "peerDependencies": {
- "@tensorflow/tfjs-core": "3.20.0"
- }
- },
- "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-webgl": {
- "version": "3.20.0",
- "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.20.0.tgz",
- "integrity": "sha512-SucbyQ08re3HvRgVfarRtKFIjNM4JvIAzcXmw4vaE/HrCtPEePkGO1VrmfQoN470EdUmGiwgqAjoyBvM2VOlVg==",
- "dependencies": {
- "@tensorflow/tfjs-backend-cpu": "3.20.0",
- "@types/offscreencanvas": "~2019.3.0",
- "@types/seedrandom": "^2.4.28",
- "@types/webgl-ext": "0.0.30",
- "@types/webgl2": "0.0.6",
- "seedrandom": "^3.0.5"
- },
- "engines": {
- "yarn": ">= 1.3.2"
- },
- "peerDependencies": {
- "@tensorflow/tfjs-core": "3.20.0"
- }
- },
- "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-converter": {
- "version": "3.20.0",
- "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.20.0.tgz",
- "integrity": "sha512-8EIYqtQwvSYw9GFNW2OFU8Qnl/FQF/kKAsQJoORYaZ419WJo+FIZWbAWDtCpJSAgkgoHH1jYWgV9H313cVmqxg==",
- "peerDependencies": {
- "@tensorflow/tfjs-core": "3.20.0"
- }
- },
- "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-core": {
- "version": "3.20.0",
- "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.20.0.tgz",
- "integrity": "sha512-L16JyVA4a8jFJXFgB9/oYZxcGq/GfLypt5dMVTyedznARZZ9SiY/UMMbo3IKl9ZylG1dOVVTpjzV3EvBYfeJXw==",
- "dependencies": {
- "@types/long": "^4.0.1",
- "@types/offscreencanvas": "~2019.3.0",
- "@types/seedrandom": "^2.4.28",
- "@types/webgl-ext": "0.0.30",
- "@webgpu/types": "0.1.16",
- "long": "4.0.0",
- "node-fetch": "~2.6.1",
- "seedrandom": "^3.0.5"
- },
- "engines": {
- "yarn": ">= 1.3.2"
- }
- },
- "node_modules/@onfido/active-video-capture/node_modules/@webgpu/types": {
- "version": "0.1.16",
- "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz",
- "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A=="
- },
- "node_modules/@onfido/castor": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@onfido/castor/-/castor-2.3.0.tgz",
- "integrity": "sha512-FkydkjedS6b2g3SqgZMYnVRZvUs/MkaEuXXJWG9+LNc7DMFT1K8smOnNuHzkiM3cJhXL6yAADdKE0mg+ZIrucQ==",
- "dependencies": {
- "@onfido/castor-tokens": "^1.0.0-beta.6",
- "csstype": "^3.1.1"
- },
- "peerDependencies": {
- "@onfido/castor-icons": ">=1.0.0"
- }
- },
- "node_modules/@onfido/castor-icons": {
- "version": "2.22.0",
- "resolved": "https://registry.npmjs.org/@onfido/castor-icons/-/castor-icons-2.22.0.tgz",
- "integrity": "sha512-7OnCvu5xqVWcBLqovZyb99NP0oHw7sjkVYXZhi438i0U6Pgecrhu/14Gc/IN/kvgDxWj9qmiYdd0qdjNaVckrQ==",
- "peerDependencies": {
- "react": ">=17 || ^16.14 || ^15.7 || ^0.14.10"
- }
- },
- "node_modules/@onfido/castor-tokens": {
- "version": "1.0.0-beta.6",
- "resolved": "https://registry.npmjs.org/@onfido/castor-tokens/-/castor-tokens-1.0.0-beta.6.tgz",
- "integrity": "sha512-MfwuSlNdM0Ay0cI3LLyqZGsHW0e1Y1R/0IdQKVU575PdWQx1Q/538aOZMo/a3/oSW0pMEgfOm+mNqPx057cvWA=="
- },
- "node_modules/@onfido/opencv": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/@onfido/opencv/-/opencv-2.1.1.tgz",
- "integrity": "sha512-Bwo0YsZrrdm+p5hpNFZ7yrqNVWJxOUbQW9aWDEUtkDWUL+nX2RHIR6F4lBGVmbqnG24anadS/+nEvy80SwD3tQ==",
- "dependencies": {
- "mirada": "^0.0.15"
- }
- },
"node_modules/@onfido/react-native-sdk": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-10.6.0.tgz",
@@ -30717,8 +30570,8 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167",
- "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9",
+ "integrity": "sha512-v6UnN9yAW6p2996Fvd4AZnMRnisVfjg6ijWzUQue/6JsjSY+MW10oP74hSjD6x32fRrNmMctjy6d5a79bQFdPA==",
"license": "MIT",
"dependencies": {
"classnames": "2.5.0",
diff --git a/package.json b/package.json
index 1c4f23700bc4..dc5b261c3b40 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.50-5",
+ "version": "1.4.51-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -52,13 +52,13 @@
"symbolicate:ios": "npx metro-symbolicate main.jsbundle.map",
"symbolicate-release:ios": "scripts/release-profile.js --platform=ios",
"symbolicate-release:android": "scripts/release-profile.js --platform=android",
- "test:e2e": "ts-node tests/e2e/testRunner.js --config ./config.local.ts",
- "test:e2e:dev": "ts-node tests/e2e/testRunner.js --config ./config.dev.js",
+ "test:e2e": "ts-node tests/e2e/testRunner.ts --config ./config.local.ts",
+ "test:e2e:dev": "ts-node tests/e2e/testRunner.ts --config ./config.dev.ts",
"gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh",
"workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh",
"workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js",
"setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1",
- "e2e-test-runner-build": "ncc build tests/e2e/testRunner.js -o tests/e2e/dist/"
+ "e2e-test-runner-build": "ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/"
},
"dependencies": {
"@dotlottie/react-player": "^1.6.3",
@@ -103,7 +103,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#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.10.1",
diff --git a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch b/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch
index 877521094cd4..c65ebbb98007 100644
--- a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch
+++ b/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch
@@ -43,7 +43,7 @@ index 7558eb3..b7bb75e 100644
}) : STATE_TRANSITIONING_OR_BELOW_TOP;
}
+
-+ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Settings_Root') && isScreenActive !== STATE_ON_TOP;
++ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Workspace_Initial') && isScreenActive !== STATE_ON_TOP;
+
const {
headerShown = true,
diff --git a/patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch
similarity index 100%
rename from patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch
rename to patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch
diff --git a/patches/react-native-reanimated+3.7.1.patch b/patches/react-native-reanimated+3.7.2.patch
similarity index 100%
rename from patches/react-native-reanimated+3.7.1.patch
rename to patches/react-native-reanimated+3.7.2.patch
diff --git a/src/CONST.ts b/src/CONST.ts
index a163c63404a7..d4fbd0ff6ef3 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1498,8 +1498,6 @@ const CONST = {
ALPHABETIC_AND_LATIN_CHARS: /^[\p{Script=Latin} ]*$/u,
NON_ALPHABETIC_AND_NON_LATIN_CHARS: /[^\p{Script=Latin}]/gu,
ACCENT_LATIN_CHARS: /[\u00C0-\u017F]/g,
- INVALID_DISPLAY_NAME_LHN: /[^\p{L}\p{N}\u00C0-\u017F\s-]/gu,
- INVALID_DISPLAY_NAME_ONLY_LHN: /^[^\p{L}\p{N}\u00C0-\u017F]$/gu,
POSITIVE_INTEGER: /^\d+$/,
PO_BOX: /\b[P|p]?(OST|ost)?\.?\s*[O|o|0]?(ffice|FFICE)?\.?\s*[B|b][O|o|0]?[X|x]?\.?\s+[#]?(\d+)\b/,
ANY_VALUE: /^.+$/,
@@ -1663,6 +1661,8 @@ const CONST = {
LOGIN_CHARACTER_LIMIT: 254,
CATEGORY_NAME_LIMIT: 256,
+ TAG_NAME_LIMIT: 256,
+
TITLE_CHARACTER_LIMIT: 100,
DESCRIPTION_LIMIT: 500,
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index f822862ec434..5681be838ca8 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -183,12 +183,12 @@ function Expensify({
// If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report
Linking.getInitialURL().then((url) => {
setInitialUrl(url);
- Report.openReportFromDeepLink(url ?? '', isAuthenticated);
+ Report.openReportFromDeepLink(url ?? '');
});
// Open chat report from a deep link (only mobile native)
Linking.addEventListener('url', (state) => {
- Report.openReportFromDeepLink(state.url, isAuthenticated);
+ Report.openReportFromDeepLink(state.url);
});
return () => {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index b9e7c4d5d274..8c48cbad561f 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -332,6 +332,8 @@ const ONYXKEYS = {
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate',
WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft',
+ WORKSPACE_TAG_CREATE_FORM: 'workspaceTagCreate',
+ WORKSPACE_TAG_CREATE_FORM_DRAFT: 'workspaceTagCreateDraft',
WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft',
WORKSPACE_DESCRIPTION_FORM: 'workspaceDescriptionForm',
WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft',
@@ -416,6 +418,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm;
[ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm;
[ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm;
+ [ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM]: FormTypes.WorkspaceTagCreateForm;
[ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
[ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index d9f0c6658a2b..defb945ba8c2 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -450,140 +450,148 @@ const ROUTES = {
WORKSPACE_NEW: 'workspace/new',
WORKSPACE_NEW_ROOM: 'workspace/new-room',
WORKSPACE_INITIAL: {
- route: 'workspace/:policyID',
- getRoute: (policyID: string) => `workspace/${policyID}` as const,
+ route: 'settings/workspaces/:policyID',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}` as const,
},
WORKSPACE_INVITE: {
- route: 'workspace/:policyID/invite',
- getRoute: (policyID: string) => `workspace/${policyID}/invite` as const,
+ route: 'settings/workspaces/:policyID/invite',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/invite` as const,
},
WORKSPACE_INVITE_MESSAGE: {
- route: 'workspace/:policyID/invite-message',
- getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const,
+ route: 'settings/workspaces/:policyID/invite-message',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/invite-message` as const,
},
WORKSPACE_PROFILE: {
- route: 'workspace/:policyID/profile',
- getRoute: (policyID: string) => `workspace/${policyID}/profile` as const,
+ route: 'settings/workspaces/:policyID/profile',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile` as const,
},
WORKSPACE_PROFILE_CURRENCY: {
- route: 'workspace/:policyID/profile/currency',
- getRoute: (policyID: string) => `workspace/${policyID}/profile/currency` as const,
+ route: 'settings/workspaces/:policyID/profile/currency',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/currency` as const,
},
WORKSPACE_PROFILE_NAME: {
- route: 'workspace/:policyID/profile/name',
- getRoute: (policyID: string) => `workspace/${policyID}/profile/name` as const,
+ route: 'settings/workspaces/:policyID/profile/name',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/name` as const,
},
WORKSPACE_PROFILE_DESCRIPTION: {
- route: 'workspace/:policyID/profile/description',
- getRoute: (policyID: string) => `workspace/${policyID}/profile/description` as const,
+ route: 'settings/workspaces/:policyID/profile/description',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/description` as const,
},
WORKSPACE_PROFILE_SHARE: {
- route: 'workspace/:policyID/profile/share',
- getRoute: (policyID: string) => `workspace/${policyID}/profile/share` as const,
+ route: 'settings/workspaces/:policyID/profile/share',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/share` as const,
},
WORKSPACE_AVATAR: {
- route: 'workspace/:policyID/avatar',
- getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const,
+ route: 'settings/workspaces/:policyID/avatar',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/avatar` as const,
},
WORKSPACE_JOIN_USER: {
- route: 'workspace/:policyID/join',
- getRoute: (policyID: string, inviterEmail: string) => `workspace/${policyID}/join?email=${inviterEmail}` as const,
+ route: 'settings/workspaces/:policyID/join',
+ getRoute: (policyID: string, inviterEmail: string) => `settings/workspaces/${policyID}/join?email=${inviterEmail}` as const,
},
WORKSPACE_SETTINGS_CURRENCY: {
- route: 'workspace/:policyID/settings/currency',
- getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const,
+ route: 'settings/workspaces/:policyID/settings/currency',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/currency` as const,
},
WORKSPACE_WORKFLOWS: {
- route: 'workspace/:policyID/workflows',
- getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const,
+ route: 'settings/workspaces/:policyID/workflows',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/workflows` as const,
+ },
+ WORKSPACE_WORKFLOWS_PAYER: {
+ route: 'workspace/:policyID/settings/workflows/payer',
+ getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/payer` as const,
},
WORKSPACE_WORKFLOWS_APPROVER: {
- route: 'workspace/:policyID/settings/workflows/approver',
- getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/approver` as const,
+ route: 'settings/workspaces/:policyID/settings/workflows/approver',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/approver` as const,
},
WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY: {
- route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency',
- getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency` as const,
+ route: 'settings/workspaces/:policyID/settings/workflows/auto-reporting-frequency',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/auto-reporting-frequency` as const,
},
WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET: {
- route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency/monthly-offset',
- getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency/monthly-offset` as const,
+ route: 'settings/workspaces/:policyID/settings/workflows/auto-reporting-frequency/monthly-offset',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/auto-reporting-frequency/monthly-offset` as const,
},
WORKSPACE_CARD: {
- route: 'workspace/:policyID/card',
- getRoute: (policyID: string) => `workspace/${policyID}/card` as const,
+ route: 'settings/workspaces/:policyID/card',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/card` as const,
},
WORKSPACE_REIMBURSE: {
- route: 'workspace/:policyID/reimburse',
- getRoute: (policyID: string) => `workspace/${policyID}/reimburse` as const,
+ route: 'settings/workspaces/:policyID/reimburse',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/reimburse` as const,
},
WORKSPACE_RATE_AND_UNIT: {
- route: 'workspace/:policyID/rateandunit',
- getRoute: (policyID: string) => `workspace/${policyID}/rateandunit` as const,
+ route: 'settings/workspaces/:policyID/rateandunit',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit` as const,
},
WORKSPACE_RATE_AND_UNIT_RATE: {
- route: 'workspace/:policyID/rateandunit/rate',
- getRoute: (policyID: string) => `workspace/${policyID}/rateandunit/rate` as const,
+ route: 'settings/workspaces/:policyID/rateandunit/rate',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit/rate` as const,
},
WORKSPACE_RATE_AND_UNIT_UNIT: {
- route: 'workspace/:policyID/rateandunit/unit',
- getRoute: (policyID: string) => `workspace/${policyID}/rateandunit/unit` as const,
+ route: 'settings/workspaces/:policyID/rateandunit/unit',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit/unit` as const,
},
WORKSPACE_BILLS: {
- route: 'workspace/:policyID/bills',
- getRoute: (policyID: string) => `workspace/${policyID}/bills` as const,
+ route: 'settings/workspaces/:policyID/bills',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/bills` as const,
},
WORKSPACE_INVOICES: {
- route: 'workspace/:policyID/invoices',
- getRoute: (policyID: string) => `workspace/${policyID}/invoices` as const,
+ route: 'settings/workspaces/:policyID/invoices',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/invoices` as const,
},
WORKSPACE_TRAVEL: {
- route: 'workspace/:policyID/travel',
- getRoute: (policyID: string) => `workspace/${policyID}/travel` as const,
+ route: 'settings/workspaces/:policyID/travel',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/travel` as const,
},
WORKSPACE_MEMBERS: {
- route: 'workspace/:policyID/members',
- getRoute: (policyID: string) => `workspace/${policyID}/members` as const,
+ route: 'settings/workspaces/:policyID/members',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const,
},
WORKSPACE_CATEGORIES: {
- route: 'workspace/:policyID/categories',
- getRoute: (policyID: string) => `workspace/${policyID}/categories` as const,
+ route: 'settings/workspaces/:policyID/categories',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const,
},
WORKSPACE_CATEGORY_SETTINGS: {
- route: 'workspace/:policyID/categories/:categoryName',
- getRoute: (policyID: string, categoryName: string) => `workspace/${policyID}/categories/${encodeURI(categoryName)}` as const,
+ route: 'settings/workspaces/:policyID/categories/:categoryName',
+ getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURI(categoryName)}` as const,
},
WORKSPACE_CATEGORIES_SETTINGS: {
- route: 'workspace/:policyID/categories/settings',
- getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const,
+ route: 'settings/workspaces/:policyID/categories/settings',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const,
},
WORKSPACE_MORE_FEATURES: {
route: 'workspace/:policyID/more-features',
getRoute: (policyID: string) => `workspace/${policyID}/more-features` as const,
},
WORKSPACE_CATEGORY_CREATE: {
- route: 'workspace/:policyID/categories/new',
- getRoute: (policyID: string) => `workspace/${policyID}/categories/new` as const,
+ route: 'settings/workspaces/:policyID/categories/new',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const,
},
WORKSPACE_TAGS: {
- route: 'workspace/:policyID/tags',
- getRoute: (policyID: string) => `workspace/${policyID}/tags` as const,
+ route: 'settings/workspaces/:policyID/tags',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const,
+ },
+ WORKSPACE_TAG_CREATE: {
+ route: 'settings/workspaces/:policyID/tags/new',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/new` as const,
},
WORKSPACE_TAGS_SETTINGS: {
- route: 'workspace/:policyID/tags/settings',
- getRoute: (policyID: string) => `workspace/${policyID}/tags/settings` as const,
+ route: 'settings/workspaces/:policyID/tags/settings',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/settings` as const,
},
WORKSPACE_EDIT_TAGS: {
- route: 'workspace/:policyID/tags/edit',
- getRoute: (policyID: string) => `workspace/${policyID}/tags/edit` as const,
+ route: 'settings/workspaces/:policyID/tags/edit',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/edit` as const,
},
WORKSPACE_MEMBER_DETAILS: {
- route: 'workspace/:policyID/members/:accountID',
- getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo),
+ route: 'settings/workspaces/:policyID/members/:accountID',
+ getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/members/${accountID}`, backTo),
},
WORKSPACE_MEMBER_ROLE_SELECTION: {
- route: 'workspace/:policyID/members/:accountID/role-selection',
- getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo),
+ route: 'settings/workspaces/:policyID/members/:accountID/role-selection',
+ getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/members/${accountID}/role-selection`, backTo),
},
WORKSPACE_DISTANCE_RATES: {
route: 'workspace/:policyID/distance-rates',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index a0e06b98da2b..4db5fd9115a5 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -22,6 +22,7 @@ const SCREENS = {
VALIDATE_LOGIN: 'ValidateLogin',
UNLINK_LOGIN: 'UnlinkLogin',
SETTINGS_CENTRAL_PANE: 'SettingsCentralPane',
+ WORKSPACES_CENTRAL_PANE: 'WorkspacesCentralPane',
SETTINGS: {
ROOT: 'Settings_Root',
SHARE_CODE: 'Settings_Share_Code',
@@ -218,8 +219,10 @@ const SCREENS = {
TAGS: 'Workspace_Tags',
TAGS_SETTINGS: 'Tags_Settings',
TAGS_EDIT: 'Tags_Edit',
+ TAG_CREATE: 'Tag_Create',
CURRENCY: 'Workspace_Profile_Currency',
WORKFLOWS: 'Workspace_Workflows',
+ WORKFLOWS_PAYER: 'Workspace_Workflows_Payer',
WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver',
WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency',
WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset',
diff --git a/src/components/AvatarSkeleton.tsx b/src/components/AvatarSkeleton.tsx
index 0887830aa07a..a6781448c3ba 100644
--- a/src/components/AvatarSkeleton.tsx
+++ b/src/components/AvatarSkeleton.tsx
@@ -1,21 +1,24 @@
import React from 'react';
import {Circle} from 'react-native-svg';
import useTheme from '@hooks/useTheme';
+import variables from '@styles/variables';
import SkeletonViewContentLoader from './SkeletonViewContentLoader';
function AvatarSkeleton() {
const theme = useTheme();
+ const skeletonCircleRadius = variables.componentSizeSmall / 2;
+
return (
);
diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx
index 2fd733d4b072..42b91b3d2d71 100644
--- a/src/components/AvatarWithIndicator.tsx
+++ b/src/components/AvatarWithIndicator.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import * as UserUtils from '@libs/UserUtils';
+import CONST from '@src/CONST';
import Avatar from './Avatar';
import AvatarSkeleton from './AvatarSkeleton';
import * as Expensicons from './Icon/Expensicons';
@@ -33,6 +34,7 @@ function AvatarWithIndicator({source, tooltipText = '', fallbackIcon = Expensico
) : (
<>
diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts
index fcf1baaa6aed..798369292958 100644
--- a/src/components/ButtonWithDropdownMenu/types.ts
+++ b/src/components/ButtonWithDropdownMenu/types.ts
@@ -15,7 +15,7 @@ type WorkspaceDistanceRatesBulkActionType = DeepValueOf = {
value: TValueType;
text: string;
- icon: IconAsset;
+ icon?: IconAsset;
iconWidth?: number;
iconHeight?: number;
iconDescription?: string;
@@ -58,7 +58,7 @@ type ButtonWithDropdownMenuProps = {
anchorAlignment?: AnchorAlignment;
/* ref for the button */
- buttonRef: RefObject;
+ buttonRef?: RefObject;
/** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */
enterKeyEventListenerPriority?: number;
diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx
index 7e7720b57a6e..5445816f067b 100644
--- a/src/components/Checkbox.tsx
+++ b/src/components/Checkbox.tsx
@@ -127,3 +127,5 @@ function Checkbox(
Checkbox.displayName = 'Checkbox';
export default forwardRef(Checkbox);
+
+export type {CheckboxProps};
diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx
index 7f05b45bca30..17c5097b8154 100644
--- a/src/components/ConfirmedRoute.tsx
+++ b/src/components/ConfirmedRoute.tsx
@@ -25,13 +25,13 @@ type ConfirmedRoutePropsOnyxProps = {
type ConfirmedRouteProps = ConfirmedRoutePropsOnyxProps & {
/** Transaction that stores the distance request data */
- transaction: Transaction;
+ transaction: OnyxEntry;
};
function ConfirmedRoute({mapboxAccessToken, transaction}: ConfirmedRouteProps) {
const {isOffline} = useNetwork();
- const {route0: route} = transaction.routes ?? {};
- const waypoints = transaction.comment?.waypoints ?? {};
+ const {route0: route} = transaction?.routes ?? {};
+ const waypoints = transaction?.comment?.waypoints ?? {};
const coordinates = route?.geometry?.coordinates ?? [];
const theme = useTheme();
const styles = useThemeStyles();
diff --git a/src/components/EReceipt.tsx b/src/components/EReceipt.tsx
index 183d88ba1c6a..40f5d242d005 100644
--- a/src/components/EReceipt.tsx
+++ b/src/components/EReceipt.tsx
@@ -105,3 +105,4 @@ export default withOnyx({
key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
},
})(EReceipt);
+export type {EReceiptProps, EReceiptOnyxProps};
diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx
index f5f9b5fc5f06..023dcc16e696 100644
--- a/src/components/EReceiptThumbnail.tsx
+++ b/src/components/EReceiptThumbnail.tsx
@@ -114,3 +114,4 @@ export default withOnyx({
key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
},
})(EReceiptThumbnail);
+export type {EReceiptThumbnailProps, EReceiptThumbnailOnyxProps};
diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx
index 88938f31cd79..b9c52ad397ec 100644
--- a/src/components/FloatingActionButton.tsx
+++ b/src/components/FloatingActionButton.tsx
@@ -104,13 +104,13 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
};
return (
-
-
-
+
+
+
{
fabPressable.current = el ?? null;
@@ -136,9 +136,9 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
/>
-
-
-
+
+
+
);
}
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index ad09b68a5f39..ee3b3607401e 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -394,3 +394,5 @@ export default withOnyx({
key: (props) => `${props.formID}Draft` as any,
},
})(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode;
+
+export type {FormProviderProps};
diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx
index 9968bb0e0772..270d476f4f79 100644
--- a/src/components/FormAlertWithSubmitButton.tsx
+++ b/src/components/FormAlertWithSubmitButton.tsx
@@ -120,3 +120,5 @@ function FormAlertWithSubmitButton({
FormAlertWithSubmitButton.displayName = 'FormAlertWithSubmitButton';
export default FormAlertWithSubmitButton;
+
+export type {FormAlertWithSubmitButtonProps};
diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js b/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js
deleted file mode 100644
index 2432d1b1748c..000000000000
--- a/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import PropTypes from 'prop-types';
-import {Animated} from 'react-native';
-
-const propTypes = {
- /** GrowlNotification content */
- children: PropTypes.node.isRequired,
-
- /** GrowlNotification Y postion, required to show or hide with fling animation */
- translateY: PropTypes.instanceOf(Animated.Value).isRequired,
-};
-
-export default propTypes;
diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.js
deleted file mode 100644
index ccc404d415d7..000000000000
--- a/src/components/GrowlNotification/GrowlNotificationContainer/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import {Animated} from 'react-native';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
-import useThemeStyles from '@hooks/useThemeStyles';
-import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes';
-
-const propTypes = {
- ...growlNotificationContainerPropTypes,
- ...windowDimensionsPropTypes,
-};
-
-function GrowlNotificationContainer(props) {
- const styles = useThemeStyles();
- return (
-
- {props.children}
-
- );
-}
-
-GrowlNotificationContainer.propTypes = propTypes;
-GrowlNotificationContainer.displayName = 'GrowlNotificationContainer';
-
-export default withWindowDimensions(GrowlNotificationContainer);
diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js
deleted file mode 100644
index 207033f8fac2..000000000000
--- a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react';
-import {Animated} from 'react-native';
-import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
-import useStyleUtils from '@hooks/useStyleUtils';
-import useThemeStyles from '@hooks/useThemeStyles';
-import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes';
-
-const propTypes = {
- ...growlNotificationContainerPropTypes,
-};
-
-function GrowlNotificationContainer(props) {
- const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
- const insets = useSafeAreaInsets;
-
- return (
-
- {props.children}
-
- );
-}
-
-GrowlNotificationContainer.propTypes = propTypes;
-GrowlNotificationContainer.displayName = 'GrowlNotificationContainer';
-
-export default GrowlNotificationContainer;
diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx
new file mode 100644
index 000000000000..efd143c9487c
--- /dev/null
+++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import {Animated} from 'react-native';
+import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type GrowlNotificationContainerProps from './types';
+
+function GrowlNotificationContainer({children, translateY}: GrowlNotificationContainerProps) {
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const insets = useSafeAreaInsets();
+
+ return {children};
+}
+
+GrowlNotificationContainer.displayName = 'GrowlNotificationContainer';
+
+export default GrowlNotificationContainer;
diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx
new file mode 100644
index 000000000000..3bbd0303906d
--- /dev/null
+++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import {Animated} from 'react-native';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import type GrowlNotificationContainerProps from './types';
+
+function GrowlNotificationContainer({children, translateY}: GrowlNotificationContainerProps) {
+ const styles = useThemeStyles();
+ const {isSmallScreenWidth} = useWindowDimensions();
+
+ return (
+
+ {children}
+
+ );
+}
+
+GrowlNotificationContainer.displayName = 'GrowlNotificationContainer';
+
+export default GrowlNotificationContainer;
diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/types.ts b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts
new file mode 100644
index 000000000000..91a48437dbd9
--- /dev/null
+++ b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts
@@ -0,0 +1,8 @@
+import type {Animated} from 'react-native';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+
+type GrowlNotificationContainerProps = ChildrenProps & {
+ translateY: Animated.Value;
+};
+
+export default GrowlNotificationContainerProps;
diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.tsx
similarity index 82%
rename from src/components/GrowlNotification/index.js
rename to src/components/GrowlNotification/index.tsx
index ed0dd302f705..d0846dcf7a42 100644
--- a/src/components/GrowlNotification/index.js
+++ b/src/components/GrowlNotification/index.tsx
@@ -1,6 +1,8 @@
+import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {Animated, View} from 'react-native';
import {Directions, Gesture, GestureDetector} from 'react-native-gesture-handler';
+import type {SvgProps} from 'react-native-svg';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Pressables from '@components/Pressable';
@@ -8,6 +10,7 @@ import Text from '@components/Text';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Growl from '@libs/Growl';
+import type {GrowlRef} from '@libs/Growl';
import useNativeDriver from '@libs/useNativeDriver';
import CONST from '@src/CONST';
import GrowlNotificationContainer from './GrowlNotificationContainer';
@@ -16,15 +19,29 @@ const INACTIVE_POSITION_Y = -255;
const PressableWithoutFeedback = Pressables.PressableWithoutFeedback;
-function GrowlNotification(_, ref) {
+function GrowlNotification(_: unknown, ref: ForwardedRef) {
const translateY = useRef(new Animated.Value(INACTIVE_POSITION_Y)).current;
const [bodyText, setBodyText] = useState('');
const [type, setType] = useState('success');
- const [duration, setDuration] = useState();
+ const [duration, setDuration] = useState();
const theme = useTheme();
const styles = useThemeStyles();
- const types = {
+ type GrowlIconTypes = Record<
+ /** String representing the growl type, all type strings
+ * for growl notifications are stored in CONST.GROWL
+ */
+ string,
+ {
+ /** Expensicon for the page */
+ icon: React.FC;
+
+ /** Color for the icon (should be from theme) */
+ iconColor: string;
+ }
+ >;
+
+ const types: GrowlIconTypes = {
[CONST.GROWL.SUCCESS]: {
icon: Expensicons.Checkmark,
iconColor: theme.success,
@@ -46,7 +63,7 @@ function GrowlNotification(_, ref) {
* @param {String} type
* @param {Number} duration
*/
- const show = useCallback((text, growlType, growlDuration) => {
+ const show = useCallback((text: string, growlType: string, growlDuration: number) => {
setBodyText(text);
setType(growlType);
setDuration(growlDuration);
@@ -61,7 +78,6 @@ function GrowlNotification(_, ref) {
(val = INACTIVE_POSITION_Y) => {
Animated.spring(translateY, {
toValue: val,
- duration: 80,
useNativeDriver,
}).start();
},
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
index 5d8c0f6ef81e..0327b6bc6f56 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
@@ -1,3 +1,4 @@
+import Str from 'expensify-common/lib/str';
import cloneDeep from 'lodash/cloneDeep';
import isEmpty from 'lodash/isEmpty';
import React from 'react';
@@ -65,10 +66,11 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona
// We need to remove the LTR unicode and leading @ from data as it is not part of the login
displayNameOrLogin = tnodeClone.data.replace(CONST.UNICODE.LTR, '').slice(1);
// We need to replace tnode.data here because we will pass it to TNodeChildrenRenderer below
- asMutable(tnodeClone).data = tnodeClone.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID));
+ asMutable(tnodeClone).data = tnodeClone.data.replace(displayNameOrLogin, Str.removeSMSDomain(getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID)));
accountID = PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])?.[0];
navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin);
+ displayNameOrLogin = Str.removeSMSDomain(displayNameOrLogin);
} else {
// If neither an account ID or email is provided, don't render anything
return null;
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 25532107016f..68c445cc944c 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -55,3 +55,5 @@ function Header({title = '', subtitle = '', textStyles = [], shouldShowEnvironme
Header.displayName = 'Header';
export default Header;
+
+export type {HeaderProps};
diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx
index 3a1c35d46c94..21f3e9a3b605 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import {Keyboard, StyleSheet, View} from 'react-native';
+import Avatar from '@components/Avatar';
import AvatarWithDisplayName from '@components/AvatarWithDisplayName';
import Header from '@components/Header';
import Icon from '@components/Icon';
@@ -32,7 +33,8 @@ function HeaderWithBackButton({
onThreeDotsButtonPress = () => {},
report = null,
policy,
- shouldShowAvatarWithDisplay = false,
+ policyAvatar,
+ shouldShowReportAvatarWithDisplay = false,
shouldShowBackButton = true,
shouldShowBorderBottom = false,
shouldShowCloseButton = false,
@@ -58,6 +60,7 @@ function HeaderWithBackButton({
shouldOverlay = false,
singleExecution = (func) => func,
shouldNavigateToTopMostReport = false,
+ style,
}: HeaderWithBackButtonProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -81,6 +84,7 @@ function HeaderWithBackButton({
shouldShowBorderBottom && styles.borderBottom,
shouldShowBackButton && styles.pl2,
shouldOverlay && StyleSheet.absoluteFillObject,
+ style,
]}
>
@@ -118,7 +122,15 @@ function HeaderWithBackButton({
additionalStyles={[styles.mr2]}
/>
)}
- {shouldShowAvatarWithDisplay ? (
+ {policyAvatar && (
+
+ )}
+ {shouldShowReportAvatarWithDisplay ? (
& {
/** Data to display a step counter in the header */
stepCounter?: StepCounterParams;
- /** Whether we should show an avatar */
- shouldShowAvatarWithDisplay?: boolean;
+ /** Whether we should show a report avatar */
+ shouldShowReportAvatarWithDisplay?: boolean;
/** Parent report, if provided it will override props.report for AvatarWithDisplay */
parentReport?: OnyxEntry;
@@ -121,6 +123,12 @@ type HeaderWithBackButtonProps = Partial & {
/** Whether we should overlay the 3 dots menu */
shouldOverlayDots?: boolean;
+
+ /** Policy avatar to display in the header */
+ policyAvatar?: Icon;
+
+ /** Additional styles to add to the component */
+ style?: StyleProp;
};
export type {ThreeDotsMenuItem};
diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx
index 1420a6abe189..e3d226a17999 100644
--- a/src/components/Indicator.tsx
+++ b/src/components/Indicator.tsx
@@ -1,17 +1,24 @@
import React from 'react';
import {StyleSheet, View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+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, UserWallet, WalletTerms} from '@src/types/onyx';
+import type {BankAccountList, FundList, LoginList, Policy, PolicyMembers, 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;
+
/** List of bank accounts */
bankAccountList: OnyxEntry;
@@ -21,6 +28,9 @@ type IndicatorOnyxProps = {
/** The user's wallet (coming from Onyx) */
userWallet: OnyxEntry;
+ /** Bank account attached to free plan */
+ reimbursementAccount: OnyxEntry;
+
/** Information about the user accepting the terms for payments */
walletTerms: OnyxEntry;
@@ -30,16 +40,25 @@ type IndicatorOnyxProps = {
type IndicatorProps = IndicatorOnyxProps;
-function Indicator({bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) {
+function Indicator({reimbursementAccount, allPolicyMembers, 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
// we only care if a single error / info condition exists anywhere.
const errorCheckingMethods: CheckingMethod[] = [
() => Object.keys(userWallet?.errors ?? {}).length > 0,
() => PaymentMethods.hasPaymentMethodError(bankAccountList, fundList),
+ () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError),
+ () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError),
+ () => Object.values(cleanAllPolicyMembers).some(PolicyUtils.hasPolicyMemberError),
+ () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
() => !!loginList && UserUtils.hasLoginListError(loginList),
// Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
@@ -58,9 +77,19 @@ function Indicator({bankAccountList, fundList, userWallet, walletTerms, loginLis
Indicator.displayName = 'Indicator';
export default withOnyx({
+ allPolicyMembers: {
+ key: ONYXKEYS.COLLECTION.POLICY_MEMBERS,
+ },
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
bankAccountList: {
key: ONYXKEYS.BANK_ACCOUNT_LIST,
},
+ // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
+ reimbursementAccount: {
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ },
fundList: {
key: ONYXKEYS.FUND_LIST,
},
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index f5545f402b14..27f424ad1b70 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -32,7 +32,6 @@ function LHNOptionsList({
draftComments = {},
transactionViolations = {},
onFirstItemRendered = () => {},
- reportIDsWithErrors = {},
}: LHNOptionsListProps) {
const styles = useThemeStyles();
const {canUseViolations} = usePermissions();
@@ -64,7 +63,6 @@ function LHNOptionsList({
const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? '';
const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions);
const lastReportAction = sortedReportActions[0];
- const reportErrors = reportIDsWithErrors[reportID] ?? {};
// Get the transaction for the last report action
let lastReportActionTransactionID = '';
@@ -93,7 +91,6 @@ function LHNOptionsList({
transactionViolations={transactionViolations}
canUseViolations={canUseViolations}
onLayout={onLayoutItem}
- reportErrors={reportErrors}
/>
);
},
@@ -112,7 +109,6 @@ function LHNOptionsList({
transactionViolations,
canUseViolations,
onLayoutItem,
- reportIDsWithErrors,
],
);
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index a3394190d0c1..a18d5a8ec1ec 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -28,7 +28,6 @@ function OptionRowLHNData({
lastReportActionTransaction = {},
transactionViolations,
canUseViolations,
- reportErrors,
...propsToForward
}: OptionRowLHNDataProps) {
const reportID = propsToForward.reportID;
@@ -41,11 +40,11 @@ function OptionRowLHNData({
// Note: ideally we'd have this as a dependent selector in onyx!
const item = SidebarUtils.getOptionData({
report: fullReport,
+ reportActions,
personalDetails,
preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT,
policy,
parentReportAction,
- reportErrors,
hasViolations: !!hasViolations,
});
if (deepEqual(item, optionItemRef.current)) {
@@ -70,7 +69,6 @@ function OptionRowLHNData({
transactionViolations,
canUseViolations,
receiptTransactions,
- reportErrors,
]);
useEffect(() => {
diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts
index c122ab018392..58bea97f04c9 100644
--- a/src/components/LHNOptionsList/types.ts
+++ b/src/components/LHNOptionsList/types.ts
@@ -7,7 +7,6 @@ import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'
import type CONST from '@src/CONST';
import type {OptionData} from '@src/libs/ReportUtils';
import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx';
-import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
type OptionMode = ValueOf;
@@ -59,9 +58,6 @@ type CustomLHNOptionsListProps = {
/** Callback to fire when the list is laid out */
onFirstItemRendered: () => void;
-
- /** Report IDs with errors mapping to their corresponding error objects */
- reportIDsWithErrors: Record;
};
type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps;
@@ -117,9 +113,6 @@ type OptionRowLHNDataProps = {
/** Callback to execute when the OptionList lays out */
onLayout?: (event: LayoutChangeEvent) => void;
-
- /** The report errors */
- reportErrors: OnyxCommon.Errors | undefined;
};
type OptionRowLHNProps = {
diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx
index 5c672cf7cab6..08e7613dc7a9 100644
--- a/src/components/Lottie/index.tsx
+++ b/src/components/Lottie/index.tsx
@@ -4,6 +4,7 @@ import type {ForwardedRef} from 'react';
import React, {forwardRef} from 'react';
import {View} from 'react-native';
import type DotLottieAnimation from '@components/LottieAnimations/types';
+import useAppState from '@hooks/useAppState';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -12,6 +13,7 @@ type Props = {
} & Omit;
function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef) {
+ const appState = useAppState();
const styles = useThemeStyles();
const [isError, setIsError] = React.useState(false);
@@ -19,8 +21,10 @@ function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef;
}
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index e0d6c39623ed..74fec2c606af 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -97,7 +97,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
return (
;
+
+type MoneyRequestConfirmationListOnyxProps = {
+ /** Collection of categories attached to a policy */
+ policyCategories: OnyxEntry;
+
+ /** Collection of tags attached to a policy */
+ policyTags: OnyxEntry;
+
+ /** 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 */
+ mileageRate: OnyxEntry;
+};
+type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & {
/** Callback to inform parent modal of success */
- onConfirm: PropTypes.func,
+ onConfirm?: (selectedParticipants: Participant[]) => void;
/** Callback to parent modal to send money */
- onSendMoney: PropTypes.func,
+ onSendMoney?: (paymentMethod: IouType | PaymentMethodType | undefined) => void;
/** Callback to inform a participant is selected */
- onSelectParticipant: PropTypes.func,
+ onSelectParticipant?: (option: Participant) => void;
/** Should we request a single or multiple participant selection from user */
- hasMultipleParticipants: PropTypes.bool.isRequired,
+ hasMultipleParticipants: boolean;
/** IOU amount */
- iouAmount: PropTypes.number.isRequired,
+ iouAmount: number;
/** IOU comment */
- iouComment: PropTypes.string,
+ iouComment?: string;
/** IOU currency */
- iouCurrencyCode: PropTypes.string,
+ iouCurrencyCode?: string;
/** IOU type */
- iouType: PropTypes.string,
+ iouType?: IouType;
/** IOU date */
- iouCreated: PropTypes.string,
+ iouCreated?: string;
/** IOU merchant */
- iouMerchant: PropTypes.string,
+ iouMerchant?: string;
/** IOU Category */
- iouCategory: PropTypes.string,
+ iouCategory?: string;
/** IOU Tag */
- iouTag: PropTypes.string,
+ iouTag?: string;
/** IOU isBillable */
- iouIsBillable: PropTypes.bool,
+ iouIsBillable?: boolean;
/** Callback to toggle the billable state */
- onToggleBillable: PropTypes.func,
+ onToggleBillable?: (isOn: boolean) => void;
/** Selected participants from MoneyRequestModal with login / accountID */
- selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired,
+ selectedParticipants: Participant[];
/** Payee of the money request with login */
- payeePersonalDetails: optionPropTypes,
+ payeePersonalDetails?: OnyxTypes.PersonalDetails;
/** Can the participants be modified or not */
- canModifyParticipants: PropTypes.bool,
+ canModifyParticipants?: boolean;
/** Should the list be read only, and not editable? */
- isReadOnly: PropTypes.bool,
+ isReadOnly?: boolean;
/** Depending on expense report or personal IOU report, respective bank account route */
- bankAccountRoute: PropTypes.string,
-
- ...withCurrentUserPersonalDetailsPropTypes,
-
- /** Current user session */
- session: PropTypes.shape({
- email: PropTypes.string.isRequired,
- }),
+ bankAccountRoute?: AllRoutes;
/** The policyID of the request */
- policyID: PropTypes.string,
+ policyID?: string;
/** The reportID of the request */
- reportID: PropTypes.string,
+ reportID?: string;
/** File path of the receipt */
- receiptPath: PropTypes.string,
+ receiptPath?: string;
/** File name of the receipt */
- receiptFilename: PropTypes.string,
+ receiptFilename?: string;
/** List styles for OptionsSelector */
- listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+ listStyles?: StyleProp;
/** ID of the transaction that represents the money request */
- transactionID: PropTypes.string,
+ transactionID?: string;
/** Transaction that represents the money request */
- transaction: transactionPropTypes,
-
- /** Unit and rate used for if the money request is a distance request */
- mileageRate: PropTypes.shape({
- /** Unit used to represent distance */
- unit: PropTypes.oneOf([CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]),
-
- /** Rate used to calculate the distance request amount */
- rate: PropTypes.number,
-
- /** The currency of the rate */
- currency: PropTypes.string,
- }),
+ transaction?: OnyxEntry;
/** Whether the money request is a distance request */
- isDistanceRequest: PropTypes.bool,
+ isDistanceRequest?: boolean;
/** Whether the money request is a scan request */
- isScanRequest: PropTypes.bool,
+ isScanRequest?: boolean;
/** Whether we're editing a split bill */
- isEditingSplitBill: PropTypes.bool,
+ isEditingSplitBill?: boolean;
/** Whether we should show the amount, date, and merchant fields. */
- shouldShowSmartScanFields: PropTypes.bool,
+ shouldShowSmartScanFields?: boolean;
/** A flag for verifying that the current report is a sub-report of a workspace chat */
- isPolicyExpenseChat: PropTypes.bool,
+ isPolicyExpenseChat?: boolean;
- /* Onyx Props */
- /** Collection of categories attached to a policy */
- policyCategories: PropTypes.objectOf(categoryPropTypes),
-
- /** Collection of tags attached to a policy */
- policyTags: tagPropTypes,
+ /** Whether smart scan failed */
+ hasSmartScanFailed?: boolean;
- /* Onyx Props */
- /** The policy of the report */
- policy: policyPropTypes.policy,
-
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- iou: iouPropTypes,
+ /** The ID of the report action */
+ reportActionID?: string;
};
-const defaultProps = {
- onConfirm: () => {},
- onSendMoney: () => {},
- onSelectParticipant: () => {},
- iouType: CONST.IOU.TYPE.REQUEST,
- iouCategory: '',
- iouTag: '',
- iouIsBillable: false,
- onToggleBillable: () => {},
- payeePersonalDetails: null,
- canModifyParticipants: false,
- isReadOnly: false,
- bankAccountRoute: '',
- session: {
- email: null,
+function MoneyRequestConfirmationList({
+ 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: selectedParticipantsProp,
+ payeePersonalDetails: payeePersonalDetailsProp,
+ iou = {
+ id: '',
+ amount: 0,
+ currency: CONST.CURRENCY.USD,
+ comment: '',
+ merchant: '',
+ category: '',
+ tag: '',
+ billable: false,
+ created: '',
+ participants: [],
+ receiptPath: '',
},
- policyID: '',
- reportID: '',
- ...withCurrentUserPersonalDetailsDefaultProps,
- receiptPath: '',
- receiptFilename: '',
- listStyles: [],
- policy: {},
- policyCategories: {},
- policyTags: {},
- transactionID: '',
- transaction: {},
- mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'},
- isDistanceRequest: false,
- isScanRequest: false,
- shouldShowSmartScanFields: true,
- isPolicyExpenseChat: false,
- iou: iouDefaultProps,
-};
-
-function MoneyRequestConfirmationList(props) {
+ canModifyParticipants: canModifyParticipantsProp = false,
+ session,
+ isReadOnly = false,
+ bankAccountRoute = '',
+ policyID = '',
+ reportID = '',
+ receiptPath = '',
+ iouComment,
+ receiptFilename = '',
+ listStyles,
+ iouCreated,
+ iouIsBillable = false,
+ onToggleBillable,
+ iouTag = '',
+ transactionID = '',
+ hasSmartScanFailed,
+ reportActionID,
+}: MoneyRequestConfirmationListProps) {
const theme = useTheme();
const styles = useThemeStyles();
- // Destructure functions from props to pass it as a dependecy to useCallback/useMemo hooks.
- // Prop functions pass props itself as a "this" value to the function which means they change every time props change.
- const {onSendMoney, onConfirm, onSelectParticipant} = props;
const {translate, toLocaleDigit} = useLocalize();
- const transaction = props.transaction;
const {canUseP2PDistanceRequests, canUseViolations} = usePermissions();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST;
- const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT;
- const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND;
+ const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
+ const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
+ const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isSplitBill);
- const isSplitWithScan = isSplitBill && props.isScanRequest;
+ const isSplitWithScan = isSplitBill && isScanRequest;
- const {unit, rate, currency} = props.mileageRate;
- const distance = lodashGet(transaction, 'routes.route0.distance', 0);
- const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0;
- const taxRates = lodashGet(props.policy, 'taxRates', {});
+ const {unit, rate, currency} = mileageRate ?? {
+ unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES,
+ rate: 0,
+ currency: CONST.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 = props.isPolicyExpenseChat && (props.iouCategory || OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories)));
+ 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 = props.isDistanceRequest || shouldExpandFields || !props.shouldShowSmartScanFields || isTypeSend || props.isEditingSplitBill;
+ 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 && !props.isDistanceRequest && !isSplitWithScan;
+ const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !isDistanceRequest && !isSplitWithScan;
- const policyTagLists = useMemo(() => PolicyUtils.getTagLists(props.policyTags), [props.policyTags]);
+ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
// A flag for showing the tags field
- const shouldShowTags = props.isPolicyExpenseChat && (props.iouTag || OptionsListUtils.hasEnabledTags(policyTagLists));
+ const shouldShowTags = isPolicyExpenseChat && (!!iouTag || OptionsListUtils.hasEnabledTags(policyTagLists));
// A flag for showing tax fields - tax rate and tax amount
- const shouldShowTax = props.isPolicyExpenseChat && lodashGet(props.policy, 'tax.trackingEnabled', props.policy.isTaxTrackingEnabled);
+ const shouldShowTax = isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled);
// A flag for showing the billable field
- const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true);
+ const shouldShowBillable = !(policy?.disabledFields?.defaultBillable ?? true);
const hasRoute = TransactionUtils.hasRoute(transaction);
- const isDistanceRequestWithPendingRoute = props.isDistanceRequest && (!hasRoute || !rate);
+ const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate);
const formattedAmount = isDistanceRequestWithPendingRoute
? ''
: CurrencyUtils.convertToDisplayString(
- shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount,
- props.isDistanceRequest ? currency : props.iouCurrencyCode,
+ shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount,
+ isDistanceRequest ? currency : iouCurrencyCode,
);
- const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode);
+ const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode);
- const defaultTaxKey = taxRates.defaultExternalID;
- const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || '';
- const taxRateTitle = (props.transaction.taxRate && props.transaction.taxRate.text) || defaultTaxName;
+ const defaultTaxKey = taxRates?.defaultExternalID;
+ const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${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 isFocused = useIsFocused();
const [formError, setFormError] = useState('');
@@ -274,67 +285,65 @@ function MoneyRequestConfirmationList(props) {
const [didConfirm, setDidConfirm] = useState(false);
const [didConfirmSplit, setDidConfirmSplit] = useState(false);
- const shouldDisplayFieldError = useMemo(() => {
- if (!props.isEditingSplitBill) {
+ const shouldDisplayFieldError: boolean = useMemo(() => {
+ if (!isEditingSplitBill) {
return false;
}
- return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction));
- }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]);
+ return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction));
+ }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]);
- const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
- const shouldDisplayMerchantError = props.isPolicyExpenseChat && shouldDisplayFieldError && isMerchantEmpty;
+ const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
+ const shouldDisplayMerchantError = isPolicyExpenseChat && shouldDisplayFieldError && isMerchantEmpty;
useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
return;
}
- if (shouldDisplayFieldError && props.hasSmartScanFailed) {
+ if (shouldDisplayFieldError && hasSmartScanFailed) {
setFormError('iou.receiptScanningFailed');
return;
}
// reset the form error whenever the screen gains or loses focus
setFormError('');
- }, [isFocused, transaction, shouldDisplayFieldError, props.hasSmartScanFailed, didConfirmSplit]);
+ }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]);
useEffect(() => {
if (!shouldCalculateDistanceAmount) {
return;
}
- const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate);
+ const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0);
IOU.setMoneyRequestAmount(amount);
}, [shouldCalculateDistanceAmount, distance, rate, unit]);
/**
* Returns the participants with amount
- * @param {Array} participants
- * @returns {Array}
*/
const getParticipantsWithAmount = useCallback(
- (participantsList) => {
- const iouAmount = IOUUtils.calculateAmount(participantsList.length, props.iouAmount, props.iouCurrencyCode);
+ (participantsList: Participant[]): Participant[] => {
+ const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? '');
return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(
participantsList,
- props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(iouAmount, props.iouCurrencyCode) : '',
+ iouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '',
);
},
- [props.iouAmount, props.iouCurrencyCode],
+ [iouAmount, iouCurrencyCode],
);
// If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again
- if (props.isEditingSplitBill && didConfirm) {
+ if (isEditingSplitBill && didConfirm) {
setDidConfirm(false);
}
- const splitOrRequestOptions = useMemo(() => {
+ const splitOrRequestOptions: Array> = useMemo(() => {
let text;
- if (isSplitBill && props.iouAmount === 0) {
+ if (isSplitBill && iouAmount === 0) {
text = translate('iou.split');
- } else if ((props.receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) {
+ } else if ((!!receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) {
text = translate('iou.request');
- if (props.iouAmount !== 0) {
+ if (iouAmount !== 0) {
text = translate('iou.requestAmount', {amount: formattedAmount});
}
} else {
@@ -344,34 +353,34 @@ function MoneyRequestConfirmationList(props) {
return [
{
text: text[0].toUpperCase() + text.slice(1),
- value: props.iouType,
+ value: iouType,
},
];
- }, [isSplitBill, isTypeRequest, props.iouType, props.iouAmount, props.receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]);
+ }, [isSplitBill, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]);
- const selectedParticipants = useMemo(() => _.filter(props.selectedParticipants, (participant) => participant.selected), [props.selectedParticipants]);
- const payeePersonalDetails = useMemo(() => props.payeePersonalDetails || props.currentUserPersonalDetails, [props.payeePersonalDetails, props.currentUserPersonalDetails]);
- const canModifyParticipants = !props.isReadOnly && props.canModifyParticipants && props.hasMultipleParticipants;
+ const selectedParticipants: Participant[] = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]);
+ const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]);
+ const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants;
const shouldDisablePaidBySection = canModifyParticipants;
- const optionSelectorSections = useMemo(() => {
+ const optionSelectorSections: OptionsListUtils.CategorySection[] = useMemo(() => {
const sections = [];
- const unselectedParticipants = _.filter(props.selectedParticipants, (participant) => !participant.selected);
- if (props.hasMultipleParticipants) {
+ const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected);
+ if (hasMultipleParticipants) {
const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants);
- let formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants);
+ let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])];
if (!canModifyParticipants) {
- formattedParticipantsList = _.map(formattedParticipantsList, (participant) => ({
+ formattedParticipantsList = formattedParticipantsList.map((participant) => ({
...participant,
- isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID),
+ isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
}));
}
- const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, props.iouAmount, props.iouCurrencyCode, true);
+ const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true);
const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(
payeePersonalDetails,
- props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, props.iouCurrencyCode) : '',
+ iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '',
);
sections.push(
@@ -390,9 +399,9 @@ function MoneyRequestConfirmationList(props) {
},
);
} else {
- const formattedSelectedParticipants = _.map(props.selectedParticipants, (participant) => ({
+ const formattedSelectedParticipants = selectedParticipantsProp.map((participant) => ({
...participant,
- isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID),
+ isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
}));
sections.push({
title: translate('common.to'),
@@ -403,27 +412,27 @@ function MoneyRequestConfirmationList(props) {
}
return sections;
}, [
- props.selectedParticipants,
- props.hasMultipleParticipants,
- props.iouAmount,
- props.iouCurrencyCode,
- getParticipantsWithAmount,
selectedParticipants,
+ hasMultipleParticipants,
+ iouAmount,
+ iouCurrencyCode,
+ getParticipantsWithAmount,
+ selectedParticipantsProp,
payeePersonalDetails,
translate,
shouldDisablePaidBySection,
canModifyParticipants,
]);
- const selectedOptions = useMemo(() => {
- if (!props.hasMultipleParticipants) {
+ const selectedOptions: Array = useMemo(() => {
+ if (!hasMultipleParticipants) {
return [];
}
return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)];
- }, [selectedParticipants, props.hasMultipleParticipants, payeePersonalDetails]);
+ }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]);
useEffect(() => {
- if (!props.isDistanceRequest) {
+ if (!isDistanceRequest) {
return;
}
@@ -432,31 +441,27 @@ function MoneyRequestConfirmationList(props) {
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(props.transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
+ IOU.setMoneyRequestPendingFields(transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
- const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit);
- IOU.setMoneyRequestMerchant(props.transactionID, distanceMerchant, false);
- }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]);
+ 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]);
- /**
- * @param {Object} option
- */
const selectParticipant = useCallback(
- (option) => {
+ (option: Participant) => {
// Return early if selected option is currently logged in user.
- if (option.accountID === props.session.accountID) {
+ if (option.accountID === session?.accountID) {
return;
}
- onSelectParticipant(option);
+ onSelectParticipant?.(option);
},
- [props.session.accountID, onSelectParticipant],
+ [session?.accountID, onSelectParticipant],
);
/**
* Navigate to report details or profile of selected user
- * @param {Object} option
*/
- const navigateToReportOrUserDetail = (option) => {
+ const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => {
if (option.accountID) {
const activeRoute = Navigation.getActiveRouteWithoutParams();
@@ -466,19 +471,16 @@ function MoneyRequestConfirmationList(props) {
}
};
- /**
- * @param {String} paymentMethod
- */
const confirm = useCallback(
- (paymentMethod) => {
- if (_.isEmpty(selectedParticipants)) {
+ (paymentMethod: IouType | PaymentMethodType | undefined) => {
+ if (!selectedParticipants.length) {
return;
}
- if (props.iouCategory && props.iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) {
+ if (iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) {
setFormError('iou.error.invalidCategoryLength');
return;
}
- if (props.iouType === CONST.IOU.TYPE.SEND) {
+ if (iouType === CONST.IOU.TYPE.SEND) {
if (!paymentMethod) {
return;
}
@@ -486,45 +488,45 @@ function MoneyRequestConfirmationList(props) {
setDidConfirm(true);
Log.info(`[IOU] Sending money via: ${paymentMethod}`);
- onSendMoney(paymentMethod);
+ onSendMoney?.(paymentMethod);
} else {
// validate the amount for distance requests
- const decimals = CurrencyUtils.getCurrencyDecimals(props.iouCurrencyCode);
- if (props.isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(props.iouAmount), decimals)) {
+ const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode);
+ if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) {
setFormError('common.error.invalidAmount');
return;
}
- if (props.isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) {
+ if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) {
setDidConfirmSplit(true);
return;
}
setDidConfirm(true);
- onConfirm(selectedParticipants);
+ onConfirm?.(selectedParticipants);
}
},
[
selectedParticipants,
onSendMoney,
onConfirm,
- props.isEditingSplitBill,
- props.iouType,
- props.isDistanceRequest,
- props.iouCategory,
+ isEditingSplitBill,
+ iouType,
+ isDistanceRequest,
+ iouCategory,
isDistanceRequestWithPendingRoute,
- props.iouCurrencyCode,
- props.iouAmount,
+ iouCurrencyCode,
+ iouAmount,
transaction,
],
);
const footerContent = useMemo(() => {
- if (props.isReadOnly) {
+ if (isReadOnly) {
return;
}
- const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND;
+ const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND;
const shouldDisableButton = selectedParticipants.length === 0 || shouldDisplayMerchantError;
const button = shouldShowSettlementButton ? (
@@ -533,10 +535,10 @@ function MoneyRequestConfirmationList(props) {
isDisabled={shouldDisableButton}
onPress={confirm}
enablePaymentsRoute={ROUTES.IOU_SEND_ENABLE_PAYMENTS}
- addBankAccountRoute={props.bankAccountRoute}
+ addBankAccountRoute={bankAccountRoute}
addDebitCardRoute={ROUTES.IOU_SEND_ADD_DEBIT_CARD}
- currency={props.iouCurrencyCode}
- policyID={props.policyID}
+ currency={iouCurrencyCode}
+ policyID={policyID}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
kycWallAnchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
@@ -554,16 +556,16 @@ function MoneyRequestConfirmationList(props) {
success
pressOnEnter
isDisabled={shouldDisableButton}
+ // eslint-disable-next-line @typescript-eslint/naming-convention
onPress={(_event, value) => confirm(value)}
options={splitOrRequestOptions}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
enterKeyEventListenerPriority={1}
/>
);
-
return (
<>
- {!_.isEmpty(formError) && (
+ {!!formError.length && (
);
}, [
- props.isReadOnly,
- props.iouType,
- props.bankAccountRoute,
- props.iouCurrencyCode,
- props.policyID,
+ isReadOnly,
+ iouType,
+ bankAccountRoute,
+ iouCurrencyCode,
+ policyID,
selectedParticipants.length,
shouldDisplayMerchantError,
confirm,
@@ -589,8 +591,9 @@ function MoneyRequestConfirmationList(props) {
]);
const {image: receiptImage, thumbnail: receiptThumbnail} =
- props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, props.receiptPath, props.receiptFilename) : {};
+ receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI);
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)
- {props.isDistanceRequest && (
+ {isDistanceRequest && (
-
+
)}
+
{receiptImage || receiptThumbnail ? (
) : (
// The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
- PolicyUtils.isPaidGroupPolicy(props.policy) &&
- !props.isDistanceRequest &&
- props.iouType === CONST.IOU.TYPE.REQUEST && (
+ PolicyUtils.isPaidGroupPolicy(policy) &&
+ !isDistanceRequest &&
+ iouType === CONST.IOU.TYPE.REQUEST && (
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
CONST.IOU.ACTION.CREATE,
- props.iouType,
- transaction.transactionID,
- props.reportID,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
Navigation.getActiveRouteWithoutParams(),
),
)
@@ -643,49 +648,43 @@ function MoneyRequestConfirmationList(props) {
/>
)
)}
- {props.shouldShowSmartScanFields && (
+ {shouldShowSmartScanFields && (
{
- if (props.isDistanceRequest) {
+ if (isDistanceRequest) {
return;
}
- if (props.isEditingSplitBill) {
- Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.AMOUNT));
+ if (isEditingSplitBill) {
+ Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT));
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(props.iouType, props.reportID));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(iouType, reportID));
}}
style={[styles.moneyRequestMenuItem, styles.mt2]}
titleStyle={styles.moneyRequestConfirmationAmount}
disabled={didConfirm}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ 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,
- props.iouType,
- transaction.transactionID,
- props.reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
+ ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
);
}}
- style={[styles.moneyRequestMenuItem]}
+ style={styles.moneyRequestMenuItem}
titleStyle={styles.flex1}
disabled={didConfirm}
- interactive={!props.isReadOnly}
+ interactive={!isReadOnly}
numberOfLinesTitle={2}
/>
{!shouldShowAllFields && (
@@ -698,162 +697,162 @@ function MoneyRequestConfirmationList(props) {
<>
{shouldShowDate && (
{
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(
CONST.IOU.ACTION.EDIT,
- props.iouType,
- transaction.transactionID,
- props.reportID,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
Navigation.getActiveRouteWithoutParams(),
),
);
}}
disabled={didConfirm}
- interactive={!props.isReadOnly}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ interactive={!isReadOnly}
+ brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''}
/>
)}
- {props.isDistanceRequest && (
+ {isDistanceRequest && (
Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))}
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(iouType, reportID))}
disabled={didConfirm || !canEditDistance}
- interactive={!props.isReadOnly}
+ interactive={!isReadOnly}
/>
)}
{shouldShowMerchant && (
{
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(
CONST.IOU.ACTION.EDIT,
- props.iouType,
- transaction.transactionID,
- props.reportID,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
Navigation.getActiveRouteWithoutParams(),
),
);
}}
disabled={didConfirm}
- interactive={!props.isReadOnly}
- brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ 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,
- props.iouType,
- props.transaction.transactionID,
- props.reportID,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
Navigation.getActiveRouteWithoutParams(),
),
);
}}
- style={[styles.moneyRequestMenuItem]}
+ style={styles.moneyRequestMenuItem}
titleStyle={styles.flex1}
disabled={didConfirm}
- interactive={!props.isReadOnly}
- rightLabel={canUseViolations && Boolean(props.policy.requiresCategory) ? translate('common.required') : ''}
+ interactive={!isReadOnly}
+ rightLabel={canUseViolations && !!policy?.requiresCategory ? translate('common.required') : ''}
/>
)}
{shouldShowTags &&
- _.map(policyTagLists, ({name}, index) => (
+ policyTagLists.map(({name}, index) => (
{
- if (props.isEditingSplitBill) {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(
- CONST.IOU.ACTION.EDIT,
- CONST.IOU.TYPE.SPLIT,
- index,
- props.transaction.transactionID,
- props.reportID,
- Navigation.getActiveRouteWithoutParams(),
- props.reportActionID,
- ),
- );
+ if (!isEditingSplitBill) {
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(props.iouType, props.reportID));
+
+ 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]}
+ style={styles.moneyRequestMenuItem}
disabled={didConfirm}
- interactive={!props.isReadOnly}
- rightLabel={canUseViolations && Boolean(props.policy.requiresTag) ? translate('common.required') : ''}
+ interactive={!isReadOnly}
+ rightLabel={canUseViolations && !!policy?.requiresTag ? translate('common.required') : ''}
/>
))}
{shouldShowTax && (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
)
}
disabled={didConfirm}
- interactive={!props.isReadOnly}
+ interactive={!isReadOnly}
/>
)}
{shouldShowTax && (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
)
}
disabled={didConfirm}
- interactive={!props.isReadOnly}
+ interactive={!isReadOnly}
/>
)}
{shouldShowBillable && (
- {translate('common.billable')}
+ {translate('common.billable')}
onToggleBillable?.(isOn)}
/>
)}
@@ -863,34 +862,26 @@ function MoneyRequestConfirmationList(props) {
);
}
-MoneyRequestConfirmationList.propTypes = propTypes;
-MoneyRequestConfirmationList.defaultProps = defaultProps;
MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList';
-export default compose(
- withCurrentUserPersonalDetails,
- 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,
- },
- splitTransactionDraft: {
- key: ({transactionID}) => `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`,
- },
- policy: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- },
- iou: {
- key: ONYXKEYS.IOU,
- },
- }),
-)(MoneyRequestConfirmationList);
+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}`,
+ },
+ iou: {
+ key: ONYXKEYS.IOU,
+ },
+})(MoneyRequestConfirmationList);
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index b5c84354e466..a9304b9c3138 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -163,7 +163,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
{
// We don't want to activate pinch gesture when we are swiping in the pager
if (!shouldDisableTransformationGestures.value) {
diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts
index a28333725d6e..f550e93d6be2 100644
--- a/src/components/MultiGestureCanvas/useTapGestures.ts
+++ b/src/components/MultiGestureCanvas/useTapGestures.ts
@@ -121,7 +121,6 @@ const useTapGestures = ({
const doubleTapGesture = Gesture.Tap()
// The first argument is not used, but must be defined
- // eslint-disable-next-line @typescript-eslint/naming-convention
.onTouchesDown((_evt, state) => {
if (!shouldDisableTransformationGestures.value) {
return;
@@ -156,7 +155,6 @@ const useTapGestures = ({
.onBegin(() => {
stopAnimation();
})
- // eslint-disable-next-line @typescript-eslint/naming-convention
.onFinalize((_evt, success) => {
if (!success || onTap === undefined) {
return;
diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx
index 575df128894a..3844080c6f5d 100644
--- a/src/components/OptionsList/BaseOptionsList.tsx
+++ b/src/components/OptionsList/BaseOptionsList.tsx
@@ -133,7 +133,6 @@ function BaseOptionsList(
*
* [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}]
*/
- // eslint-disable-next-line @typescript-eslint/naming-convention
const getItemLayout = (_data: OptionsListData[] | null, flatDataArrayIndex: number) => {
if (!flattenedData.current[flatDataArrayIndex]) {
flattenedData.current = buildFlatSectionArray();
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index a391ff061baa..83da817da858 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -18,7 +18,7 @@ import Text from './Text';
type PopoverMenuItem = {
/** An icon element displayed on the left side */
- icon: IconAsset;
+ icon?: IconAsset;
/** Text label */
text: string;
diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx
index cfcd6acba41f..9b93d7900772 100644
--- a/src/components/RadioButtonWithLabel.tsx
+++ b/src/components/RadioButtonWithLabel.tsx
@@ -72,3 +72,5 @@ function RadioButtonWithLabel({LabelComponent, style, label = '', hasError = fal
RadioButtonWithLabel.displayName = 'RadioButtonWithLabel';
export default RadioButtonWithLabel;
+
+export type {RadioButtonWithLabelProps};
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index 8577c9fa5f97..311ed75be3e0 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -24,7 +24,6 @@ import ControlSelection from '@libs/ControlSelection';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as IOUUtils from '@libs/IOUUtils';
-import * as Localize from '@libs/Localize';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -152,12 +151,25 @@ function MoneyRequestPreviewContent({
}
let message = translate('iou.cash');
- if (hasViolations && transaction) {
+ if (shouldShowRBR && transaction) {
const violations = TransactionUtils.getTransactionViolations(transaction.transactionID, transactionViolations);
if (violations?.[0]) {
const violationMessage = ViolationsUtils.getViolationTranslation(violations[0], translate);
- const isTooLong = violations.filter((v) => v.type === 'violation').length > 1 || violationMessage.length > 15;
- message += ` • ${isTooLong ? translate('violations.reviewRequired') : violationMessage}`;
+ const violationsCount = violations.filter((v) => v.type === 'violation').length;
+ const isTooLong = violationsCount > 1 || violationMessage.length > 15;
+ const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors;
+
+ return `${message} • ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`;
+ }
+
+ const isMerchantMissing = TransactionUtils.isMerchantMissing(transaction);
+ const isAmountMissing = TransactionUtils.isAmountMissing(transaction);
+ if (isAmountMissing && isMerchantMissing) {
+ message += ` • ${translate('violations.reviewRequired')}`;
+ } else if (isAmountMissing) {
+ message += ` • ${translate('iou.missingAmount')}`;
+ } else {
+ message += ` • ${translate('iou.missingMerchant')}`;
}
} else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) {
message += ` • ${translate('iou.approved')}`;
@@ -180,10 +192,6 @@ function MoneyRequestPreviewContent({
return translate('iou.routePending');
}
- if (!isSettled && TransactionUtils.hasMissingSmartscanFields(transaction)) {
- return Localize.translateLocal('iou.receiptMissingDetails');
- }
-
return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency);
};
diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx
index 74ecc1a2adbd..ffc12957dcb4 100644
--- a/src/components/ReportActionItem/ReportActionItemImages.tsx
+++ b/src/components/ReportActionItem/ReportActionItemImages.tsx
@@ -109,3 +109,4 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report
ReportActionItemImages.displayName = 'ReportActionItemImages';
export default ReportActionItemImages;
+export type {ReportActionItemImagesProps};
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 743bfd8fff88..f1aa1751dd84 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -1,8 +1,8 @@
import React, {useMemo} from 'react';
-import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
+import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -29,7 +29,7 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {Policy, Report, ReportAction, Transaction, TransactionViolations} from '@src/types/onyx';
+import type {Policy, Report, ReportAction, Transaction, TransactionViolations, UserWallet} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import ReportActionItemImages from './ReportActionItemImages';
@@ -48,6 +48,9 @@ type ReportPreviewOnyxProps = {
/** All of the transaction violations */
transactionViolations: OnyxCollection;
+
+ /** The user's wallet account */
+ userWallet: OnyxEntry;
};
type ReportPreviewProps = ReportPreviewOnyxProps & {
@@ -94,6 +97,7 @@ function ReportPreview({
isHovered = false,
isWhisper = false,
checkIfContextMenuActive = () => {},
+ userWallet,
}: ReportPreviewProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -206,6 +210,9 @@ function ReportPreview({
const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton;
+ const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID);
+ 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;
@@ -251,12 +258,19 @@ function ReportPreview({
{getPreviewMessage()}
- {!iouSettled && hasErrors && (
+ {shouldShowRBR && (
)}
+
+ {!shouldShowRBR && shouldPromptUserToAddBankAccount && (
+
+ )}
@@ -338,4 +352,7 @@ export default withOnyx({
transactionViolations: {
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
},
+ userWallet: {
+ key: ONYXKEYS.USER_WALLET,
+ },
})(ReportPreview);
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index 306846ad7d99..827eec8088a6 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -148,7 +148,6 @@ function ScreenWrapper(
const panResponder = useRef(
PanResponder.create({
- // eslint-disable-next-line @typescript-eslint/naming-convention
onStartShouldSetPanResponderCapture: (_e, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS,
onPanResponderRelease: toggleTestToolsModal,
}),
@@ -156,7 +155,6 @@ function ScreenWrapper(
const keyboardDissmissPanResponder = useRef(
PanResponder.create({
- // eslint-disable-next-line @typescript-eslint/naming-convention
onMoveShouldSetPanResponderCapture: (_e, gestureState) => {
const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && isKeyboardShown && Browser.isMobile();
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 8c352a4d084a..db6204c8c1ef 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -1,4 +1,5 @@
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';
@@ -10,6 +11,7 @@ import FixedFooter from '@components/FixedFooter';
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import SectionList from '@components/SectionList';
+import ShowMoreButton from '@components/ShowMoreButton';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useActiveElementRole from '@hooks/useActiveElementRole';
@@ -81,6 +83,9 @@ function BaseSelectionList(
const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true);
const [itemsToHighlight, setItemsToHighlight] = useState | null>(null);
const itemFocusTimeoutRef = useRef(null);
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const incrementPage = () => setCurrentPage((prev) => prev + 1);
/**
* Iterates through the sections and items inside each section, and builds 3 arrays along the way:
@@ -156,6 +161,33 @@ function BaseSelectionList(
// If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member
const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey));
+ const [slicedSections, ShowMoreButtonInstance] = useMemo(
+ () => [
+ sections.map((section) => {
+ if (isEmpty(section.data)) {
+ return section;
+ }
+
+ return {
+ ...section,
+ data: section.data.slice(0, CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * currentPage),
+ };
+ }),
+ flattenedSections.allOptions.length > CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * currentPage ? (
+
+ ) : null,
+ ],
+ // we don't need to add styles here as they change
+ // we don't need to add flattendedSections here as they will change along with sections
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [sections, currentPage],
+ );
+
// Disable `Enter` shortcut if the active element is a button or checkbox
const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles);
@@ -364,6 +396,8 @@ function BaseSelectionList(
}
// Remove the focus if the search input is empty else focus on the first non disabled item
const newSelectedIndex = textInputValue === '' ? -1 : 0;
+ // reseting the currrent page to 1 when the user types something
+ setCurrentPage(1);
updateAndScrollToFocusedIndex(newSelectedIndex);
}, [canSelectMultiple, flattenedSections.allOptions.length, prevTextInputValue, textInputValue, updateAndScrollToFocusedIndex]);
@@ -492,7 +526,7 @@ function BaseSelectionList(
{!headerMessage && !canSelectMultiple && customListHeader}
(
testID="selection-list"
onLayout={onSectionListLayout}
style={(!maxToRenderPerBatch || isInitialSectionListRender) && styles.opacity0}
+ ListFooterComponent={ShowMoreButtonInstance}
/>
{children}
>
diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx
index 882ca56076bd..2a3a8dd04a79 100644
--- a/src/components/SelectionList/UserListItem.tsx
+++ b/src/components/SelectionList/UserListItem.tsx
@@ -1,3 +1,4 @@
+import Str from 'expensify-common/lib/str';
import React from 'react';
import {View} from 'react-native';
import MultipleAvatars from '@components/MultipleAvatars';
@@ -81,7 +82,7 @@ function UserListItem({
)}
diff --git a/src/components/SwipeableView/index.native.tsx b/src/components/SwipeableView/index.native.tsx
index 67c6695c1a7f..e5b6d371e606 100644
--- a/src/components/SwipeableView/index.native.tsx
+++ b/src/components/SwipeableView/index.native.tsx
@@ -9,7 +9,6 @@ function SwipeableView({children, onSwipeDown}: SwipeableViewProps) {
const panResponder = useRef(
PanResponder.create({
// The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance & swipe direction is downwards
- // eslint-disable-next-line @typescript-eslint/naming-convention
onMoveShouldSetPanResponderCapture: (_event, gestureState) => {
if (gestureState.dy - oldYRef.current > 0 && gestureState.dy > minimumPixelDistance) {
return true;
diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx
index 963782bb50d5..a94f54682c85 100644
--- a/src/components/WorkspaceSwitcherButton.tsx
+++ b/src/components/WorkspaceSwitcherButton.tsx
@@ -1,13 +1,11 @@
import React, {useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import Navigation from '@libs/Navigation/Navigation';
import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy} from '@src/types/onyx';
import * as Expensicons from './Icon/Expensicons';
@@ -19,14 +17,14 @@ type WorkspaceSwitcherButtonOnyxProps = {
policy: OnyxEntry;
};
-type WorkspaceSwitcherButtonProps = {activeWorkspaceID?: string} & WorkspaceSwitcherButtonOnyxProps;
+type WorkspaceSwitcherButtonProps = WorkspaceSwitcherButtonOnyxProps;
-function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherButtonProps) {
+function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) {
const {translate} = useLocalize();
const theme = useTheme();
const {source, name, type} = useMemo(() => {
- if (!activeWorkspaceID) {
+ if (!policy) {
return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR};
}
@@ -36,7 +34,7 @@ function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherB
name: policy?.name ?? '',
type: CONST.ICON_TYPE_WORKSPACE,
};
- }, [policy, activeWorkspaceID]);
+ }, [policy]);
return (
@@ -71,8 +69,4 @@ function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherB
WorkspaceSwitcherButton.displayName = 'WorkspaceSwitcherButton';
-export default withOnyx({
- policy: {
- key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`,
- },
-})(WorkspaceSwitcherButton);
+export default WorkspaceSwitcherButton;
diff --git a/src/hooks/useAppState/index.native.ts b/src/hooks/useAppState/index.native.ts
new file mode 100644
index 000000000000..39c1dff65e7c
--- /dev/null
+++ b/src/hooks/useAppState/index.native.ts
@@ -0,0 +1,28 @@
+import React from 'react';
+import type {AppStateStatus} from 'react-native';
+import {AppState} from 'react-native';
+import type AppStateType from './types';
+
+function useAppState() {
+ const [appState, setAppState] = React.useState({
+ isForeground: AppState.currentState === 'active',
+ isInactive: AppState.currentState === 'inactive',
+ isBackground: AppState.currentState === 'background',
+ });
+
+ React.useEffect(() => {
+ function handleAppStateChange(nextAppState: AppStateStatus) {
+ setAppState({
+ isForeground: nextAppState === 'active',
+ isInactive: nextAppState === 'inactive',
+ isBackground: nextAppState === 'background',
+ });
+ }
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
+ return () => subscription.remove();
+ }, []);
+
+ return appState;
+}
+
+export default useAppState;
diff --git a/src/hooks/useAppState/index.ts b/src/hooks/useAppState/index.ts
new file mode 100644
index 000000000000..74d68535d1d1
--- /dev/null
+++ b/src/hooks/useAppState/index.ts
@@ -0,0 +1,8 @@
+import type AppStateType from './types';
+
+function useAppState(): AppStateType {
+ // Since there's no AppState in web, we'll always return isForeground as true
+ return {isForeground: true, isInactive: false, isBackground: false};
+}
+
+export default useAppState;
diff --git a/src/hooks/useAppState/types.ts b/src/hooks/useAppState/types.ts
new file mode 100644
index 000000000000..9093b89fc4dc
--- /dev/null
+++ b/src/hooks/useAppState/types.ts
@@ -0,0 +1,7 @@
+type AppStateType = {
+ isForeground: boolean;
+ isInactive: boolean;
+ isBackground: boolean;
+};
+
+export default AppStateType;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 5023708c53b7..7e442eee2236 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -605,6 +605,8 @@ export default {
routePending: 'Route pending...',
receiptScanning: 'Scan in progress…',
receiptMissingDetails: 'Receipt missing details',
+ missingAmount: 'Missing amount',
+ missingMerchant: 'Missing merchant',
receiptStatusTitle: 'Scanning…',
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.',
@@ -1070,6 +1072,11 @@ export default {
},
},
},
+ workflowsPayerPage: {
+ title: 'Authorized payer',
+ genericErrorMessage: 'The authorized payer could not be changed. Please try again.',
+ admins: 'Admins',
+ },
reportFraudPage: {
title: 'Report virtual card fraud',
description: 'If your virtual card details have been stolen or compromised, we’ll permanently deactivate your existing card and provide you with a new virtual card and number.',
@@ -1827,11 +1834,14 @@ export default {
requiresTag: 'Members must tag all spend',
customTagName: 'Custom tag name',
enableTag: 'Enable tag',
+ addTag: 'Add tag',
subtitle: 'Tags add more detailed ways to classify costs.',
emptyTags: {
title: "You haven't created any tags",
subtitle: 'Add a tag to track projects, locations, departments, and more.',
},
+ tagRequiredError: 'Tag name is required.',
+ existingTagError: 'A tag with this name already exists.',
genericFailureMessage: 'An error occurred while updating the tag, please try again.',
},
emptyWorkspace: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 0a5ff9d539d9..267581f043ee 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -598,6 +598,8 @@ export default {
routePending: 'Ruta pendiente...',
receiptScanning: 'Escaneo en curso…',
receiptMissingDetails: 'Recibo con campos vacĂos',
+ missingAmount: 'Falta importe',
+ missingMerchant: 'Falta comerciante',
receiptStatusTitle: 'Escaneando…',
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.',
@@ -1066,6 +1068,11 @@ export default {
},
},
},
+ workflowsPayerPage: {
+ title: 'Pagador autorizado',
+ genericErrorMessage: 'El pagador autorizado no se pudo cambiar. Por favor, inténtalo mas tarde.',
+ admins: 'Administradores',
+ },
reportFraudPage: {
title: 'Reportar fraude con la tarjeta virtual',
description:
@@ -1851,11 +1858,14 @@ export default {
requiresTag: 'Los miembros deben etiquetar todos los gastos',
customTagName: 'Nombre de etiqueta personalizada',
enableTag: 'Habilitar etiqueta',
+ addTag: 'Añadir etiqueta',
subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.',
emptyTags: {
title: 'No has creado ninguna etiqueta',
subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.',
},
+ tagRequiredError: 'Lo nombre de la etiqueta es obligatorio.',
+ existingTagError: 'Ya existe una etiqueta con este nombre.',
genericFailureMessage: 'Se produjo un error al actualizar la etiqueta, inténtelo nuevamente.',
},
emptyWorkspace: {
diff --git a/src/languages/types.ts b/src/languages/types.ts
index 6b4298b70d3c..2d04bdc156c9 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -111,7 +111,7 @@ type RequestAmountParams = {amount: string};
type RequestedAmountMessageParams = {formattedAmount: string; comment?: string};
-type SplitAmountParams = {amount: number};
+type SplitAmountParams = {amount: string};
type DidSplitAmountMessageParams = {formattedAmount: string; comment: string};
diff --git a/src/libs/API/parameters/CreatePolicyTagsParams.ts b/src/libs/API/parameters/CreatePolicyTagsParams.ts
new file mode 100644
index 000000000000..6fd16d9ca87b
--- /dev/null
+++ b/src/libs/API/parameters/CreatePolicyTagsParams.ts
@@ -0,0 +1,10 @@
+type CreatePolicyTagsParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array<{name: string;}>
+ */
+ tags: string;
+};
+
+export default CreatePolicyTagsParams;
diff --git a/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts b/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts
new file mode 100644
index 000000000000..eea0788b3927
--- /dev/null
+++ b/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts
@@ -0,0 +1,5 @@
+type OpenPolicyWorkflowsPageParams = {
+ policyID: string;
+};
+
+export default OpenPolicyWorkflowsPageParams;
diff --git a/src/libs/API/parameters/ReconnectAppParams.ts b/src/libs/API/parameters/ReconnectAppParams.ts
index 8c5b7d6c0da9..d8c1da4f0887 100644
--- a/src/libs/API/parameters/ReconnectAppParams.ts
+++ b/src/libs/API/parameters/ReconnectAppParams.ts
@@ -2,6 +2,7 @@ type ReconnectAppParams = {
mostRecentReportActionLastModified?: string;
updateIDFrom?: number;
policyIDList: string[];
+ idempotencyKey?: string;
};
export default ReconnectAppParams;
diff --git a/src/libs/API/parameters/SetWorkspacePayerParams.ts b/src/libs/API/parameters/SetWorkspacePayerParams.ts
new file mode 100644
index 000000000000..d1c976c31dd3
--- /dev/null
+++ b/src/libs/API/parameters/SetWorkspacePayerParams.ts
@@ -0,0 +1,6 @@
+type SetWorkspacePayerParams = {
+ policyID: string;
+ reimburserEmail: string;
+};
+
+export default SetWorkspacePayerParams;
diff --git a/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts b/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts
new file mode 100644
index 000000000000..f96f6385f541
--- /dev/null
+++ b/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts
@@ -0,0 +1,9 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type SetWorkspaceReimbursementParams = {
+ policyID: string;
+ reimbursementChoice: ValueOf;
+};
+
+export default SetWorkspaceReimbursementParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 7e0e9b6e4a96..687f32f5b6de 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -156,6 +156,8 @@ export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAut
export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams';
export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams';
export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams';
+export type {default as SetWorkspacePayerParams} from './SetWorkspacePayerParams';
+export type {default as SetWorkspaceReimbursementParams} from './SetWorkspaceReimbursementParams';
export type {default as SetPolicyRequiresTag} from './SetPolicyRequiresTag';
export type {default as RenamePolicyTaglist} from './RenamePolicyTaglist';
export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams';
@@ -169,4 +171,6 @@ export type {default as EnablePolicyReportFieldsParams} from './EnablePolicyRepo
export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest';
export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest';
export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink';
+export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflowsPageParams';
export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams';
+export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index bf215e09e37d..17c38b252e0b 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -12,6 +12,8 @@ const WRITE_COMMANDS = {
SET_WORKSPACE_AUTO_REPORTING_FREQUENCY: 'SetWorkspaceAutoReportingFrequency',
SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET: 'UpdatePolicy',
SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode',
+ SET_WORKSPACE_PAYER: 'SetWorkspacePayer',
+ SET_WORKSPACE_REIMBURSEMENT: 'SetWorkspaceReimbursement',
DISMISS_REFERRAL_BANNER: 'DismissReferralBanner',
UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale',
OPEN_APP: 'OpenApp',
@@ -116,6 +118,7 @@ const WRITE_COMMANDS = {
CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment',
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories',
+ CREATE_POLICY_TAG: 'CreatePolicyTag',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag',
RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist',
@@ -281,6 +284,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
[WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglist;
+ [WRITE_COMMANDS.CREATE_POLICY_TAG]: Parameters.CreatePolicyTagsParams;
[WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams;
[WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams;
[WRITE_COMMANDS.EDIT_TASK_ASSIGNEE]: Parameters.EditTaskAssigneeParams;
@@ -325,6 +329,8 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_FREQUENCY]: Parameters.SetWorkspaceAutoReportingFrequencyParams;
[WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams;
[WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams;
+ [WRITE_COMMANDS.SET_WORKSPACE_PAYER]: Parameters.SetWorkspacePayerParams;
+ [WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT]: Parameters.SetWorkspaceReimbursementParams;
[WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams;
[WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES]: Parameters.EnablePolicyCategoriesParams;
[WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS]: Parameters.EnablePolicyConnectionsParams;
@@ -370,6 +376,7 @@ const READ_COMMANDS = {
OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage',
OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage',
OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest',
+ OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage',
OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage',
} as const;
@@ -407,6 +414,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams;
[READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams;
[READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams;
+ [READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams;
[READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams;
};
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index aef615018b4c..985499078c9a 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -146,3 +146,5 @@ function getDistanceRequestAmount(distance: number, unit: Unit, rate: number): n
}
export default {getDefaultMileageRate, getDistanceMerchant, getDistanceRequestAmount};
+
+export type {DefaultMileageRate};
diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts
index f76bdf2ed9a5..4c0e572cc9b2 100644
--- a/src/libs/E2E/client.ts
+++ b/src/libs/E2E/client.ts
@@ -1,23 +1,6 @@
import Config from '../../../tests/e2e/config';
import Routes from '../../../tests/e2e/server/routes';
-import type {NetworkCacheMap, TestConfig} from './types';
-
-type TestResult = {
- /** Name of the test */
- name: string;
-
- /** The branch where test were running */
- branch?: string;
-
- /** Duration in milliseconds */
- duration?: number;
-
- /** Optional, if set indicates that the test run failed and has no valid results. */
- error?: string;
-
- /** Render count */
- renderCount?: number;
-};
+import type {NetworkCacheMap, TestConfig, TestResult} from './types';
type NativeCommandPayload = {
text: string;
diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts
index 0964938392fb..5185a75625a3 100644
--- a/src/libs/E2E/types.ts
+++ b/src/libs/E2E/types.ts
@@ -26,4 +26,21 @@ type TestConfig = {
[key: string]: string | {autoFocus: boolean};
};
-export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig};
+type TestResult = {
+ /** Name of the test */
+ name: string;
+
+ /** The branch where test were running */
+ branch?: string;
+
+ /** Duration in milliseconds */
+ duration?: number;
+
+ /** Optional, if set indicates that the test run failed and has no valid results. */
+ error?: string;
+
+ /** Render count */
+ renderCount?: number;
+};
+
+export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig, TestResult};
diff --git a/src/libs/Growl.ts b/src/libs/Growl.ts
index 55bcf88206e9..3812a155ba1f 100644
--- a/src/libs/Growl.ts
+++ b/src/libs/Growl.ts
@@ -50,4 +50,6 @@ export default {
success,
};
+export type {GrowlRef};
+
export {growlRef, setIsReady};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index a986e2995156..585713243d3f 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -6,6 +6,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import KeyboardShortcut from '@libs/KeyboardShortcut';
+import Log from '@libs/Log';
import getCurrentUrl from '@libs/Navigation/currentUrl';
import Navigation from '@libs/Navigation/Navigation';
import type {AuthScreensParamList} from '@libs/Navigation/types';
@@ -126,6 +127,7 @@ function handleNetworkReconnect() {
if (isLoadingApp) {
App.openApp();
} else {
+ Log.info('[handleNetworkReconnect] Sending ReconnectApp');
App.reconnectApp(lastUpdateIDAppliedToClient);
}
}
@@ -189,6 +191,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
if (SessionUtils.didUserLogInDuringSession()) {
App.openApp();
} else {
+ Log.info('[AuthScreens] Sending ReconnectApp');
App.reconnectApp(initialLastUpdateIDAppliedToClient);
}
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index d56e38564149..7df1da23d068 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -186,13 +186,20 @@ const NewTeachersUniteNavigator = createModalStackNavigator require('../../../pages/TeachersUnite/ImTeacherPage').default as React.ComponentType,
});
-const AccountSettingsModalStackNavigator = createModalStackNavigator(
+const WorkspaceSettingsModalStackNavigator = createModalStackNavigator(
{
- [SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require('../../../pages/settings/Preferences/PreferencesPage').default as React.ComponentType,
- [SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default as React.ComponentType,
- [SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../pages/settings/Profile/ProfilePage').default as React.ComponentType,
- [SCREENS.SETTINGS.WALLET.ROOT]: () => require('../../../pages/settings/Wallet/WalletPage').default as React.ComponentType,
- [SCREENS.SETTINGS.ABOUT]: () => require('../../../pages/settings/AboutPage/AboutPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.PROFILE]: () => require('../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CARD]: () => require('../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.BILLS]: () => require('../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.INVOICES]: () => require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.MORE_FEATURES]: () => require('../../../pages/workspace/WorkspaceMoreFeaturesPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TAGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default as React.ComponentType,
},
(styles) => ({cardStyle: styles.navigationScreenCardStyle, headerShown: false}),
);
@@ -245,6 +252,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceInvitePage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_APPROVER]: () => require('../../../pages/workspace/workflows/WorkspaceWorkflowsApproverPage').default as React.ComponentType,
[SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: () => require('../../../pages/workspace/workflows/WorkspaceWorkflowsPayerPage').default as React.ComponentType,
[SCREENS.WORKSPACE.NAME]: () => require('../../../pages/workspace/WorkspaceNamePage').default as React.ComponentType,
[SCREENS.WORKSPACE.DESCRIPTION]: () => require('../../../pages/workspace/WorkspaceProfileDescriptionPage').default as React.ComponentType,
[SCREENS.WORKSPACE.SHARE]: () => require('../../../pages/workspace/WorkspaceProfileSharePage').default as React.ComponentType,
@@ -256,6 +264,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../pages/workspace/tags/WorkspaceCreateTagPage').default as React.ComponentType,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
[SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType,
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType,
@@ -312,7 +321,6 @@ const ProcessMoneyRequestHoldStackNavigator = createModalStackNavigator({
});
export {
- AccountSettingsModalStackNavigator,
AddPersonalBankAccountModalStackNavigator,
DetailsModalStackNavigator,
OnboardEngagementModalStackNavigator,
@@ -341,4 +349,5 @@ export {
TaskModalStackNavigator,
WalletStatementStackNavigator,
ProcessMoneyRequestHoldStackNavigator,
+ WorkspaceSettingsModalStackNavigator,
};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
index ce03a8d5bcba..87a441f16ddb 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
@@ -4,12 +4,11 @@ import React from 'react';
import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import type {BottomTabNavigatorParamList} from '@libs/Navigation/types';
-import AllSettingsScreen from '@pages/home/sidebar/AllSettingsScreen';
import SidebarScreen from '@pages/home/sidebar/SidebarScreen';
import SCREENS from '@src/SCREENS';
import ActiveRouteContext from './ActiveRouteContext';
-const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType;
+const loadInitialSettingsPage = () => require('../../../../pages/settings/InitialSettingsPage').default as React.ComponentType;
const Tab = createCustomBottomTabNavigator();
@@ -20,6 +19,7 @@ const screenOptions: StackNavigationOptions = {
function BottomTabNavigator() {
const activeRoute = useNavigationState(getTopmostCentralPaneRoute);
+
return (
@@ -28,12 +28,8 @@ function BottomTabNavigator() {
component={SidebarScreen}
/>
-
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
index 5a3af07a3d5a..16f403342a58 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
@@ -13,20 +13,13 @@ const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') :
type Screens = Partial React.ComponentType>>;
-const workspaceSettingsScreens = {
+const settingsScreens = {
[SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../../pages/workspace/WorkspacesListPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType,
- [SCREENS.WORKSPACE.CARD]: () => require('../../../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType,
- [SCREENS.WORKSPACE.BILLS]: () => require('../../../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.INVOICES]: () => require('../../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../../pages/workspace/categories/WorkspaceCategoriesPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.MORE_FEATURES]: () => require('../../../../../pages/workspace/WorkspaceMoreFeaturesPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.TAGS]: () => require('../../../../../pages/workspace/tags/WorkspaceTagsPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require('../../../../../pages/settings/Preferences/PreferencesPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.SECURITY]: () => require('../../../../../pages/settings/Security/SecuritySettingsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../../../pages/settings/Profile/ProfilePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET.ROOT]: () => require('../../../../../pages/settings/Wallet/WalletPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.ABOUT]: () => require('../../../../../pages/settings/AboutPage/AboutPage').default as React.ComponentType,
} satisfies Screens;
function BaseCentralPaneNavigator() {
@@ -46,8 +39,7 @@ function BaseCentralPaneNavigator() {
initialParams={{openOnAdminRoom: openOnAdminRoom === 'true' || undefined}}
component={ReportScreenWrapper}
/>
-
- {Object.entries(workspaceSettingsScreens).map(([screenName, componentGetter]) => (
+ {Object.entries(settingsScreens).map(([screenName, componentGetter]) => (
require('../../../../pages/settings/InitialSettingsPage').default as React.ComponentType;
+const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType;
const RootStack = createCustomFullScreenNavigator();
@@ -22,14 +22,14 @@ function FullScreenNavigator() {
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
index 58d9efb43df5..3a59e42bcca1 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
@@ -12,11 +12,11 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Session from '@libs/actions/Session';
-import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import Navigation from '@libs/Navigation/Navigation';
import type {RootStackParamList, State} from '@libs/Navigation/types';
-import {checkIfWorkspaceSettingsTabHasRBR, getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
+import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
+import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar';
import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton';
import variables from '@styles/variables';
import * as Welcome from '@userActions/Welcome';
@@ -43,14 +43,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
const navigationState = navigation.getState() as State | undefined;
const routes = navigationState?.routes;
const currentRoute = routes?.[navigationState?.index ?? 0];
- const bottomTabRoute = getTopmostBottomTabRoute(navigationState);
- if (
- // When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method.
- // To prevent this, the value of the bottomTabRoute?.name is checked here
- bottomTabRoute?.name === SCREENS.WORKSPACE.INITIAL ||
- Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) ||
- Session.isAnonymousUser()
- ) {
+ if (Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) || Session.isAnonymousUser()) {
return;
}
@@ -64,22 +57,20 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
return topmostBottomTabRoute?.name ?? SCREENS.HOME;
});
- const shouldShowWorkspaceRedBrickRoad = checkIfWorkspaceSettingsTabHasRBR(activeWorkspaceID) && currentTabName === SCREENS.HOME;
-
- const chatTabBrickRoad = currentTabName !== SCREENS.HOME ? getChatTabBrickRoad(activeWorkspaceID) : undefined;
+ const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID);
return (
-
- {
- Navigation.navigate(ROUTES.HOME);
- }}
- role={CONST.ROLE.BUTTON}
- accessibilityLabel={translate('common.chats')}
- wrapperStyle={styles.flexGrow1}
- style={styles.bottomTabBarItem}
- >
+ {
+ Navigation.navigate(ROUTES.HOME);
+ }}
+ role={CONST.ROLE.BUTTON}
+ accessibilityLabel={translate('common.chats')}
+ wrapperStyle={styles.flex1}
+ style={styles.bottomTabBarItem}
+ >
+
)}
-
-
+
+
+
-
-
- interceptAnonymousUser(() =>
- activeWorkspaceID ? Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(activeWorkspaceID)) : Navigation.navigate(ROUTES.ALL_SETTINGS),
- )
- }
- role={CONST.ROLE.BUTTON}
- accessibilityLabel={translate('common.settings')}
- wrapperStyle={styles.flexGrow1}
- style={styles.bottomTabBarItem}
- >
-
-
- {shouldShowWorkspaceRedBrickRoad && }
-
-
-
+
+
+
);
}
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
index 4ed8869c1eaa..38bfe4af9ab6 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
@@ -1,36 +1,87 @@
import React from 'react';
import {View} from 'react-native';
-import Search from '@components/Search';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import Breadcrumbs from '@components/Breadcrumbs';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import {PressableWithoutFeedback} from '@components/Pressable';
+import Tooltip from '@components/Tooltip';
import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton';
-import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import SignInOrAvatarWithOptionalStatus from '@pages/home/sidebar/SignInOrAvatarWithOptionalStatus';
+import SignInButton from '@pages/home/sidebar/SignInButton';
import * as Session from '@userActions/Session';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {Policy} from '@src/types/onyx';
-function TopBar() {
+type TopBarOnyxProps = {
+ policy: OnyxEntry;
+};
+
+// eslint-disable-next-line react/no-unused-prop-types
+type TopBarProps = {activeWorkspaceID?: string} & TopBarOnyxProps;
+
+function TopBar({policy}: TopBarProps) {
const styles = useThemeStyles();
+ const theme = useTheme();
const {translate} = useLocalize();
- const {activeWorkspaceID} = useActiveWorkspace();
+
+ const headerBreadcrumb = policy?.name
+ ? {type: CONST.BREADCRUMB_TYPE.STRONG, text: policy.name}
+ : {
+ type: CONST.BREADCRUMB_TYPE.ROOT,
+ };
return (
-
-
- Navigation.navigate(ROUTES.SEARCH))}
- containerStyle={[styles.flex1]}
- />
-
+
+
+
+
+
+
+
+
+
+ {Session.isAnonymousUser() ? (
+
+ ) : (
+
+ Navigation.navigate(ROUTES.SEARCH))}
+ >
+
+
+
+ )}
+
);
}
TopBar.displayName = 'TopBar';
-export default TopBar;
+export default withOnyx({
+ policy: {
+ key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`,
+ },
+})(TopBar);
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx
index bd32c6cab73c..8c53027cf713 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx
@@ -9,7 +9,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import type {NavigationStateRoute} from '@libs/Navigation/types';
import SCREENS from '@src/SCREENS';
import BottomTabBar from './BottomTabBar';
-import TopBar from './TopBar';
type CustomNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & {
initialRouteName: string;
@@ -52,7 +51,6 @@ function CustomBottomTabNavigator({initialRouteName, children, screenOptions, ..
shouldShowOfflineIndicator={false}
>
-
| PartialState>;
-const isAtLeastOneInState = (state: StackState, screenName: string): boolean => !!state.routes.find((route) => route.name === screenName);
+const isAtLeastOneInState = (state: StackState, screenName: string): boolean => state.routes.some((route) => route.name === screenName);
function adaptStateIfNecessary(state: StackState) {
const isNarrowLayout = getIsNarrowLayout();
+ const workspaceCentralPane = state.routes.at(-1);
+ const topmostWorkspaceCentralPaneRoute = workspaceCentralPane?.state?.routes[0];
- // There should always be SETTINGS.ROOT screen in the state to make sure go back works properly if we deeplinkg to a subpage of settings.
- if (!isAtLeastOneInState(state, SCREENS.SETTINGS.ROOT)) {
+ // When a screen from the FullScreenNavigator is opened from the deeplink then params should be passed to SCREENS.WORKSPACE.INITIAL from the variable defined below.
+ const workspacesCentralPaneParams =
+ workspaceCentralPane?.params && 'params' in workspaceCentralPane.params ? (workspaceCentralPane.params.params as Record) : undefined;
+
+ // There should always be WORKSPACE.INITIAL screen in the state to make sure go back works properly if we deeplinkg to a subpage of settings.
+ if (!isAtLeastOneInState(state, SCREENS.WORKSPACE.INITIAL)) {
// @ts-expect-error Updating read only property
// noinspection JSConstantReassignment
state.stale = true; // eslint-disable-line
@@ -20,27 +26,30 @@ function adaptStateIfNecessary(state: StackState) {
// This is necessary for ts to narrow type down to PartialState.
if (state.stale === true) {
// Unshift the root screen to fill left pane.
- state.routes.unshift({name: SCREENS.SETTINGS.ROOT});
+ state.routes.unshift({
+ name: SCREENS.WORKSPACE.INITIAL,
+ params: topmostWorkspaceCentralPaneRoute?.params ?? workspacesCentralPaneParams,
+ });
}
}
// If the screen is wide, there should be at least two screens inside:
- // - SETINGS.ROOT to cover left pane.
- // - SETTINGS_CENTRAL_PANE to cover central pane.
+ // - WORKSPACE.INITIAL to cover left pane.
+ // - WORKSPACES_CENTRAL_PANE to cover central pane.
if (!isNarrowLayout) {
- if (!isAtLeastOneInState(state, SCREENS.SETTINGS_CENTRAL_PANE)) {
+ if (!isAtLeastOneInState(state, SCREENS.WORKSPACES_CENTRAL_PANE)) {
// @ts-expect-error Updating read only property
// noinspection JSConstantReassignment
state.stale = true; // eslint-disable-line
-
// Push the default settings central pane screen.
if (state.stale === true) {
state.routes.push({
- name: SCREENS.SETTINGS_CENTRAL_PANE,
+ name: SCREENS.WORKSPACES_CENTRAL_PANE,
state: {
routes: [
{
- name: SCREENS.SETTINGS.PROFILE.ROOT,
+ name: SCREENS.WORKSPACE.PROFILE,
+ params: state.routes[0]?.params,
},
],
},
diff --git a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx
index fb7ae24947c2..f35c609402b0 100644
--- a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx
@@ -2,34 +2,12 @@ import type {ParamListBase, StackActionHelpers, StackNavigationState} from '@rea
import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/native';
import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack';
import {StackView} from '@react-navigation/stack';
-import React, {useEffect, useMemo} from 'react';
+import React, {useEffect} from 'react';
import useWindowDimensions from '@hooks/useWindowDimensions';
import navigationRef from '@libs/Navigation/navigationRef';
-import SCREENS from '@src/SCREENS';
import CustomFullScreenRouter from './CustomFullScreenRouter';
import type {FullScreenNavigatorProps, FullScreenNavigatorRouterOptions} from './types';
-type Routes = StackNavigationState['routes'];
-function reduceReportRoutes(routes: Routes): Routes {
- const result: Routes = [];
- let count = 0;
- const reverseRoutes = [...routes].reverse();
-
- reverseRoutes.forEach((route) => {
- if (route.name === SCREENS.SETTINGS_CENTRAL_PANE) {
- // Remove all report routes except the last 3. This will improve performance.
- if (count < 3) {
- result.push(route);
- count++;
- }
- } else {
- result.push(route);
- }
- });
-
- return result.reverse();
-}
-
function CustomFullScreenNavigator(props: FullScreenNavigatorProps) {
const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder<
StackNavigationState,
@@ -45,16 +23,6 @@ function CustomFullScreenNavigator(props: FullScreenNavigatorProps) {
const {isSmallScreenWidth} = useWindowDimensions();
- const stateToRender = useMemo(() => {
- const result = reduceReportRoutes(state.routes);
-
- return {
- ...state,
- index: result.length - 1,
- routes: [...result],
- };
- }, [state]);
-
useEffect(() => {
if (!navigationRef.isReady()) {
return;
@@ -69,7 +37,7 @@ function CustomFullScreenNavigator(props: FullScreenNavigatorProps) {
diff --git a/src/libs/Navigation/FreezeWrapper.tsx b/src/libs/Navigation/FreezeWrapper.tsx
index 9bb72a34588b..bb6ab107373b 100644
--- a/src/libs/Navigation/FreezeWrapper.tsx
+++ b/src/libs/Navigation/FreezeWrapper.tsx
@@ -1,7 +1,6 @@
import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native';
import React, {useEffect, useRef, useState} from 'react';
import {Freeze} from 'react-freeze';
-import {InteractionManager} from 'react-native';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
type FreezeWrapperProps = ChildrenProps & {
@@ -29,7 +28,7 @@ function FreezeWrapper({keepVisible = false, children}: FreezeWrapperProps) {
// we don't want to freeze the screen if it's the previous screen because the freeze placeholder
// would be visible at the beginning of the back animation then
if ((navigation.getState()?.index ?? 0) - (screenIndexRef.current ?? 0) > 1) {
- InteractionManager.runAfterInteractions(() => setIsScreenBlurred(true));
+ setIsScreenBlurred(true);
} else {
setIsScreenBlurred(false);
}
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index 4cd6a141bd3b..c55145a5d580 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -19,7 +19,7 @@ import linkingConfig from './linkingConfig';
import linkTo from './linkTo';
import navigationRef from './navigationRef';
import switchPolicyID from './switchPolicyID';
-import type {State, StateOrRoute, SwitchPolicyIDParams} from './types';
+import type {NavigationStateRoute, State, StateOrRoute, SwitchPolicyIDParams} from './types';
let resolveNavigationIsReadyPromise: () => void;
const navigationIsReadyPromise = new Promise((resolve) => {
@@ -234,6 +234,18 @@ function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopT
navigationRef.current.goBack();
}
+/**
+ * Reset the navigation state to Home page
+ */
+function resetToHome() {
+ const rootState = navigationRef.getRootState();
+ const bottomTabKey = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state?.key;
+ if (bottomTabKey) {
+ navigationRef.dispatch({...StackActions.popToTop(), target: bottomTabKey});
+ }
+ navigationRef.dispatch({...StackActions.popToTop(), target: rootState.key});
+}
+
/**
* Close the full screen modal.
*/
@@ -366,6 +378,7 @@ export default {
parseHybridAppUrl,
closeFullScreen,
navigateWithSwitchPolicyID,
+ resetToHome,
};
export {navigationRef};
diff --git a/src/libs/Navigation/getTopmostSettingsCentralPaneName.ts b/src/libs/Navigation/getTopmostWorkspacesCentralPaneName.ts
similarity index 76%
rename from src/libs/Navigation/getTopmostSettingsCentralPaneName.ts
rename to src/libs/Navigation/getTopmostWorkspacesCentralPaneName.ts
index 0ddea6588ef6..db11368c1345 100644
--- a/src/libs/Navigation/getTopmostSettingsCentralPaneName.ts
+++ b/src/libs/Navigation/getTopmostWorkspacesCentralPaneName.ts
@@ -2,12 +2,12 @@ import type {NavigationState, PartialState} from '@react-navigation/native';
import SCREENS from '@src/SCREENS';
// Get the name of topmost report in the navigation stack.
-function getTopmostSettingsCentralPaneName(state: NavigationState | PartialState): string | undefined {
+function getTopmostWorkspacesCentralPaneName(state: NavigationState | PartialState): string | undefined {
if (!state) {
return;
}
- const topmostCentralPane = state.routes.filter((route) => typeof route !== 'number' && 'name' in route && route.name === SCREENS.SETTINGS_CENTRAL_PANE).at(-1);
+ const topmostCentralPane = state.routes.filter((route) => typeof route !== 'number' && 'name' in route && route.name === SCREENS.WORKSPACES_CENTRAL_PANE).at(-1);
if (!topmostCentralPane) {
return;
@@ -24,4 +24,4 @@ function getTopmostSettingsCentralPaneName(state: NavigationState | PartialState
return topmostCentralPane.state?.routes.at(-1)?.name;
}
-export default getTopmostSettingsCentralPaneName;
+export default getTopmostWorkspacesCentralPaneName;
diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts
index 371ea89df2e2..2a00895f0492 100644
--- a/src/libs/Navigation/linkTo.ts
+++ b/src/libs/Navigation/linkTo.ts
@@ -6,7 +6,6 @@ import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import type {Route} from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
import getActionsFromPartialDiff from './AppNavigator/getActionsFromPartialDiff';
import getPartialStateDiff from './AppNavigator/getPartialStateDiff';
import dismissModal from './dismissModal';
@@ -119,7 +118,6 @@ export default function linkTo(navigation: NavigationContainerRef)?.name === SCREENS.WORKSPACE.INITIAL && path.includes('workspace');
+ const isFullScreenOnTop = rootState.routes?.at(-1)?.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR;
- if (policyID && !isWorkspaceSettingsOpened) {
+ if (policyID && !isFullScreenOnTop) {
// The stateFromPath doesn't include proper path if there is a policy passed with /w/id.
// We need to replace the path in the state with the proper one.
// To avoid this hacky solution we may want to create custom getActionFromState function in the future.
diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
index 743bf2e0cff1..95233bfed079 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -2,12 +2,40 @@ import type {CentralPaneName} from '@libs/Navigation/types';
import SCREENS from '@src/SCREENS';
const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = {
- [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE],
- [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT],
- [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION],
- [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET],
- [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT],
- [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
+ [SCREENS.SETTINGS.PROFILE.ROOT]: [
+ SCREENS.SETTINGS.PROFILE.DISPLAY_NAME,
+ SCREENS.SETTINGS.PROFILE.CONTACT_METHODS,
+ SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS,
+ SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD,
+ SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER,
+ SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE,
+ SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME,
+ SCREENS.SETTINGS.PROFILE.STATUS,
+ SCREENS.SETTINGS.PROFILE.PRONOUNS,
+ SCREENS.SETTINGS.PROFILE.TIMEZONE,
+ SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT,
+ SCREENS.SETTINGS.PROFILE.LEGAL_NAME,
+ SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH,
+ SCREENS.SETTINGS.PROFILE.ADDRESS,
+ SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY,
+ SCREENS.SETTINGS.SHARE_CODE,
+ ],
+ [SCREENS.SETTINGS.PREFERENCES.ROOT]: [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE, SCREENS.SETTINGS.PREFERENCES.LANGUAGE, SCREENS.SETTINGS.PREFERENCES.THEME],
+ [SCREENS.SETTINGS.WALLET.ROOT]: [
+ SCREENS.SETTINGS.WALLET.DOMAIN_CARD,
+ SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME,
+ SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE,
+ SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS,
+ SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM,
+ SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE,
+ SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT,
+ SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS,
+ SCREENS.SETTINGS.WALLET.CARD_ACTIVATE,
+ SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD,
+ SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS,
+ ],
+ [SCREENS.SETTINGS.SECURITY]: [SCREENS.SETTINGS.TWO_FACTOR_AUTH, SCREENS.SETTINGS.CLOSE],
+ [SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS, SCREENS.SETTINGS.TROUBLESHOOT],
};
export default CENTRAL_PANE_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index f79e275007d7..fd108f2c95f3 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -2,39 +2,17 @@ import type {FullScreenName} from '@libs/Navigation/types';
import SCREENS from '@src/SCREENS';
const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
- [SCREENS.SETTINGS.PROFILE.ROOT]: [
- SCREENS.SETTINGS.PROFILE.DISPLAY_NAME,
- SCREENS.SETTINGS.PROFILE.CONTACT_METHODS,
- SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS,
- SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD,
- SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER,
- SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE,
- SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME,
- SCREENS.SETTINGS.PROFILE.STATUS,
- SCREENS.SETTINGS.PROFILE.PRONOUNS,
- SCREENS.SETTINGS.PROFILE.TIMEZONE,
- SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT,
- SCREENS.SETTINGS.PROFILE.LEGAL_NAME,
- SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH,
- SCREENS.SETTINGS.PROFILE.ADDRESS,
- SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY,
+ [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE],
+ [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT],
+ [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION],
+ [SCREENS.WORKSPACE.WORKFLOWS]: [
+ SCREENS.WORKSPACE.WORKFLOWS_APPROVER,
+ SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY,
+ SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET,
+ SCREENS.WORKSPACE.WORKFLOWS_PAYER,
],
- [SCREENS.SETTINGS.PREFERENCES.ROOT]: [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE, SCREENS.SETTINGS.PREFERENCES.LANGUAGE, SCREENS.SETTINGS.PREFERENCES.THEME],
- [SCREENS.SETTINGS.WALLET.ROOT]: [
- SCREENS.SETTINGS.WALLET.DOMAIN_CARD,
- SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME,
- SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE,
- SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS,
- SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM,
- SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE,
- SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT,
- SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS,
- SCREENS.SETTINGS.WALLET.CARD_ACTIVATE,
- SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD,
- SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS,
- ],
- [SCREENS.SETTINGS.SECURITY]: [SCREENS.SETTINGS.TWO_FACTOR_AUTH, SCREENS.SETTINGS.CLOSE],
- [SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS, SCREENS.SETTINGS.TROUBLESHOOT],
+ [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE],
+ [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
};
export default FULL_SCREEN_TO_RHP_MAPPING;
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 b2939cf38d9f..78a644ab4aee 100755
--- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
@@ -3,20 +3,13 @@ import SCREENS from '@src/SCREENS';
const TAB_TO_CENTRAL_PANE_MAPPING: Record = {
[SCREENS.HOME]: [SCREENS.REPORT],
- [SCREENS.ALL_SETTINGS]: [SCREENS.SETTINGS.WORKSPACES],
- [SCREENS.WORKSPACE.INITIAL]: [
- SCREENS.WORKSPACE.PROFILE,
- SCREENS.WORKSPACE.CARD,
- SCREENS.WORKSPACE.WORKFLOWS,
- SCREENS.WORKSPACE.REIMBURSE,
- SCREENS.WORKSPACE.BILLS,
- SCREENS.WORKSPACE.INVOICES,
- SCREENS.WORKSPACE.TRAVEL,
- SCREENS.WORKSPACE.MEMBERS,
- SCREENS.WORKSPACE.CATEGORIES,
- SCREENS.WORKSPACE.TAGS,
- SCREENS.WORKSPACE.MORE_FEATURES,
- SCREENS.WORKSPACE.DISTANCE_RATES,
+ [SCREENS.SETTINGS.ROOT]: [
+ SCREENS.SETTINGS.PROFILE.ROOT,
+ SCREENS.SETTINGS.PREFERENCES.ROOT,
+ SCREENS.SETTINGS.SECURITY,
+ SCREENS.SETTINGS.WALLET.ROOT,
+ SCREENS.SETTINGS.ABOUT,
+ SCREENS.SETTINGS.WORKSPACES,
],
};
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 97d7650a9043..1461c27e03e0 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -31,10 +31,8 @@ const config: LinkingOptions['config'] = {
initialRouteName: SCREENS.HOME,
screens: {
[SCREENS.HOME]: ROUTES.HOME,
- [SCREENS.ALL_SETTINGS]: ROUTES.ALL_SETTINGS,
- [SCREENS.WORKSPACE.INITIAL]: {
- path: ROUTES.WORKSPACE_INITIAL.route,
- exact: true,
+ [SCREENS.SETTINGS.ROOT]: {
+ path: ROUTES.SETTINGS,
},
},
},
@@ -42,42 +40,27 @@ const config: LinkingOptions['config'] = {
[NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: {
screens: {
[SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route,
-
- [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES,
- [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route,
- [SCREENS.WORKSPACE.CARD]: {
- path: ROUTES.WORKSPACE_CARD.route,
- },
- [SCREENS.WORKSPACE.WORKFLOWS]: {
- path: ROUTES.WORKSPACE_WORKFLOWS.route,
- },
- [SCREENS.WORKSPACE.REIMBURSE]: {
- path: ROUTES.WORKSPACE_REIMBURSE.route,
- },
- [SCREENS.WORKSPACE.BILLS]: {
- path: ROUTES.WORKSPACE_BILLS.route,
- },
- [SCREENS.WORKSPACE.INVOICES]: {
- path: ROUTES.WORKSPACE_INVOICES.route,
- },
- [SCREENS.WORKSPACE.TRAVEL]: {
- path: ROUTES.WORKSPACE_TRAVEL.route,
- },
- [SCREENS.WORKSPACE.MEMBERS]: {
- path: ROUTES.WORKSPACE_MEMBERS.route,
+ [SCREENS.SETTINGS.PROFILE.ROOT]: {
+ path: ROUTES.SETTINGS_PROFILE,
+ exact: true,
},
- [SCREENS.WORKSPACE.CATEGORIES]: {
- path: ROUTES.WORKSPACE_CATEGORIES.route,
+ [SCREENS.SETTINGS.PREFERENCES.ROOT]: {
+ path: ROUTES.SETTINGS_PREFERENCES,
+ exact: true,
},
- [SCREENS.WORKSPACE.MORE_FEATURES]: {
- path: ROUTES.WORKSPACE_MORE_FEATURES.route,
+ [SCREENS.SETTINGS.SECURITY]: {
+ path: ROUTES.SETTINGS_SECURITY,
+ exact: true,
},
- [SCREENS.WORKSPACE.TAGS]: {
- path: ROUTES.WORKSPACE_TAGS.route,
+ [SCREENS.SETTINGS.WALLET.ROOT]: {
+ path: ROUTES.SETTINGS_WALLET,
+ exact: true,
},
- [SCREENS.WORKSPACE.DISTANCE_RATES]: {
- path: ROUTES.WORKSPACE_DISTANCE_RATES.route,
+ [SCREENS.SETTINGS.ABOUT]: {
+ path: ROUTES.SETTINGS_ABOUT,
+ exact: true,
},
+ [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES,
},
},
[SCREENS.NOT_FOUND]: '*',
@@ -288,6 +271,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route,
},
+ [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: {
+ path: ROUTES.WORKSPACE_WORKFLOWS_PAYER.route,
+ },
[SCREENS.WORKSPACE.MEMBER_DETAILS]: {
path: ROUTES.WORKSPACE_MEMBER_DETAILS.route,
},
@@ -303,6 +289,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.TAGS_EDIT]: {
path: ROUTES.WORKSPACE_EDIT_TAGS.route,
},
+ [SCREENS.WORKSPACE.TAG_CREATE]: {
+ path: ROUTES.WORKSPACE_TAG_CREATE.route,
+ },
[SCREENS.REIMBURSEMENT_ACCOUNT]: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
@@ -557,30 +546,44 @@ const config: LinkingOptions['config'] = {
[NAVIGATORS.FULL_SCREEN_NAVIGATOR]: {
screens: {
- [SCREENS.SETTINGS.ROOT]: {
- path: ROUTES.SETTINGS,
+ [SCREENS.WORKSPACE.INITIAL]: {
+ path: ROUTES.WORKSPACE_INITIAL.route,
},
- [SCREENS.SETTINGS_CENTRAL_PANE]: {
+ [SCREENS.WORKSPACES_CENTRAL_PANE]: {
screens: {
- [SCREENS.SETTINGS.PROFILE.ROOT]: {
- path: ROUTES.SETTINGS_PROFILE,
- exact: true,
+ [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route,
+ [SCREENS.WORKSPACE.CARD]: {
+ path: ROUTES.WORKSPACE_CARD.route,
},
- [SCREENS.SETTINGS.PREFERENCES.ROOT]: {
- path: ROUTES.SETTINGS_PREFERENCES,
- exact: true,
+ [SCREENS.WORKSPACE.WORKFLOWS]: {
+ path: ROUTES.WORKSPACE_WORKFLOWS.route,
},
- [SCREENS.SETTINGS.SECURITY]: {
- path: ROUTES.SETTINGS_SECURITY,
- exact: true,
+ [SCREENS.WORKSPACE.REIMBURSE]: {
+ path: ROUTES.WORKSPACE_REIMBURSE.route,
},
- [SCREENS.SETTINGS.WALLET.ROOT]: {
- path: ROUTES.SETTINGS_WALLET,
- exact: true,
+ [SCREENS.WORKSPACE.BILLS]: {
+ path: ROUTES.WORKSPACE_BILLS.route,
},
- [SCREENS.SETTINGS.ABOUT]: {
- path: ROUTES.SETTINGS_ABOUT,
- exact: true,
+ [SCREENS.WORKSPACE.INVOICES]: {
+ path: ROUTES.WORKSPACE_INVOICES.route,
+ },
+ [SCREENS.WORKSPACE.TRAVEL]: {
+ path: ROUTES.WORKSPACE_TRAVEL.route,
+ },
+ [SCREENS.WORKSPACE.MEMBERS]: {
+ path: ROUTES.WORKSPACE_MEMBERS.route,
+ },
+ [SCREENS.WORKSPACE.CATEGORIES]: {
+ path: ROUTES.WORKSPACE_CATEGORIES.route,
+ },
+ [SCREENS.WORKSPACE.MORE_FEATURES]: {
+ path: ROUTES.WORKSPACE_MORE_FEATURES.route,
+ },
+ [SCREENS.WORKSPACE.TAGS]: {
+ path: ROUTES.WORKSPACE_TAGS.route,
+ },
+ [SCREENS.WORKSPACE.DISTANCE_RATES]: {
+ path: ROUTES.WORKSPACE_DISTANCE_RATES.route,
},
},
},
diff --git a/src/libs/Navigation/linkingConfig/customGetPathFromState.ts b/src/libs/Navigation/linkingConfig/customGetPathFromState.ts
index 4f7023d14db4..4017b1b2b17c 100644
--- a/src/libs/Navigation/linkingConfig/customGetPathFromState.ts
+++ b/src/libs/Navigation/linkingConfig/customGetPathFromState.ts
@@ -8,20 +8,19 @@ import SCREENS from '@src/SCREENS';
const removePolicyIDParamFromState = (state: State) => {
const stateCopy = _.cloneDeep(state);
const bottomTabRoute = getTopmostBottomTabRoute(stateCopy);
- if (bottomTabRoute?.name === SCREENS.HOME && bottomTabRoute?.params && 'policyID' in bottomTabRoute.params) {
+ if (bottomTabRoute?.params && 'policyID' in bottomTabRoute.params) {
delete bottomTabRoute.params.policyID;
}
return stateCopy;
};
const customGetPathFromState: typeof getPathFromState = (state, options) => {
+ // For the Home and Settings pages we should remove policyID from the params, because on small screens it's displayed twice in the URL
const stateWithoutPolicyID = removePolicyIDParamFromState(state as State);
-
- // For the Home page we should remove policyID from the params,
const path = getPathFromState(stateWithoutPolicyID, options);
const policyIDFromState = getPolicyIDFromState(state as State);
- const isWorkspaceSettingsOpened = getTopmostBottomTabRoute(state as State)?.name === SCREENS.WORKSPACE.INITIAL && path.includes('workspace');
- return `${policyIDFromState && !isWorkspaceSettingsOpened ? `/w/${policyIDFromState}` : ''}${path}`;
+ const isHomeOpened = getTopmostBottomTabRoute(state as State)?.name === SCREENS.HOME;
+ return `${policyIDFromState && isHomeOpened ? `/w/${policyIDFromState}` : ''}${path}`;
};
export default customGetPathFromState;
diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index 5f89b2cd4630..e4a8464d7ddd 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -14,6 +14,10 @@ import getMatchingBottomTabRouteForState from './getMatchingBottomTabRouteForSta
import getMatchingCentralPaneRouteForState from './getMatchingCentralPaneRouteForState';
import replacePathInNestedState from './replacePathInNestedState';
+const RHP_SCREENS_OPENED_FROM_LHN = [SCREENS.SETTINGS.SHARE_CODE, SCREENS.SETTINGS.PROFILE.STATUS] as const;
+
+type RHPScreenOpenedFromLHN = (typeof RHP_SCREENS_OPENED_FROM_LHN)[number];
+
type Metainfo = {
// Sometimes modal screens don't have information about what should be visible under the overlay.
// That means such screen can have different screens under the overlay depending on what was already in the state.
@@ -73,14 +77,21 @@ function createCentralPaneNavigator(route: NavigationPartialRoute): NavigationPartialRoute {
const routes = [];
- routes.push({name: SCREENS.SETTINGS.ROOT});
+ const policyID = route?.params && 'policyID' in route.params ? route.params.policyID : undefined;
+
+ // Both routes in FullScreenNavigator should store a policyID in params, so here this param is also passed to the screen displayed in LHN in FullScreenNavigator
+ routes.push({
+ name: SCREENS.WORKSPACE.INITIAL,
+ params: {
+ policyID,
+ },
+ });
if (route) {
routes.push({
- name: SCREENS.SETTINGS_CENTRAL_PANE,
+ name: SCREENS.WORKSPACES_CENTRAL_PANE,
state: getRoutesWithIndex([route]),
});
}
-
return {
name: NAVIGATORS.FULL_SCREEN_NAVIGATOR,
state: getRoutesWithIndex(routes),
@@ -131,11 +142,6 @@ function getMatchingRootRouteForRHPRoute(
return createFullScreenNavigator({name: fullScreenName as FullScreenName, params: route.params});
}
}
-
- // This screen is opened from the LHN of the FullStackNavigator, so in this case we shouldn't push any CentralPane screen
- if (route.name === SCREENS.SETTINGS.SHARE_CODE) {
- return createFullScreenNavigator();
- }
}
function getAdaptedState(state: PartialState>, policyID?: string): GetAdaptedStateReturnType {
@@ -165,18 +171,25 @@ function getAdaptedState(state: PartialState
if (topmostNestedRHPRoute) {
let matchingRootRoute = getMatchingRootRouteForRHPRoute(topmostNestedRHPRoute);
-
+ const isRHPScreenOpenedFromLHN = topmostNestedRHPRoute?.name && RHP_SCREENS_OPENED_FROM_LHN.includes(topmostNestedRHPRoute?.name as RHPScreenOpenedFromLHN);
// This may happen if this RHP doens't have a route that should be under the overlay defined.
- if (!matchingRootRoute) {
+ if (!matchingRootRoute || isRHPScreenOpenedFromLHN) {
metainfo.isCentralPaneAndBottomTabMandatory = false;
metainfo.isFullScreenNavigatorMandatory = false;
- matchingRootRoute = createCentralPaneNavigator({name: SCREENS.REPORT});
+ matchingRootRoute = matchingRootRoute ?? createCentralPaneNavigator({name: SCREENS.REPORT});
}
// If the root route is type of FullScreenNavigator, the default bottom tab will be added.
const matchingBottomTabRoute = getMatchingBottomTabRouteForState({routes: [matchingRootRoute]});
routes.push(createBottomTabNavigator(matchingBottomTabRoute, policyID));
- routes.push(matchingRootRoute);
+ // When we open a screen in RHP from FullScreenNavigator, we need to add the appropriate screen in CentralPane.
+ // Then, when we close FullScreenNavigator, we will be redirected to the correct page in CentralPane.
+ if (matchingRootRoute.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) {
+ routes.push(createCentralPaneNavigator({name: SCREENS.SETTINGS.WORKSPACES}));
+ }
+ if (!isNarrowLayout || !isRHPScreenOpenedFromLHN) {
+ routes.push(matchingRootRoute);
+ }
}
routes.push(rhpNavigator);
@@ -230,14 +243,18 @@ function getAdaptedState(state: PartialState
routes.push(
createBottomTabNavigator(
{
- name: SCREENS.HOME,
+ name: SCREENS.SETTINGS.ROOT,
},
policyID,
),
);
- if (!isNarrowLayout) {
- routes.push(createCentralPaneNavigator({name: SCREENS.REPORT}));
- }
+
+ routes.push(
+ createCentralPaneNavigator({
+ name: SCREENS.SETTINGS.WORKSPACES,
+ }),
+ );
+
routes.push(fullScreenNavigator);
return {
@@ -318,7 +335,6 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => {
const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>;
replacePathInNestedState(state, path);
-
if (state === undefined) {
throw new Error('Unable to parse path');
}
diff --git a/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts b/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts
index ef4cd65942b0..fd45685acf23 100644
--- a/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts
+++ b/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts
@@ -1,5 +1,6 @@
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import type {BottomTabName, NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
+import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import {CENTRAL_PANE_TO_TAB_MAPPING} from './TAB_TO_CENTRAL_PANE_MAPPING';
@@ -7,6 +8,12 @@ import {CENTRAL_PANE_TO_TAB_MAPPING} from './TAB_TO_CENTRAL_PANE_MAPPING';
function getMatchingBottomTabRouteForState(state: State, policyID?: string): NavigationPartialRoute {
const paramsWithPolicyID = policyID ? {policyID} : undefined;
const defaultRoute = {name: SCREENS.HOME, params: paramsWithPolicyID};
+ const isFullScreenNavigatorOpened = state.routes.some((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR);
+
+ if (isFullScreenNavigatorOpened) {
+ return {name: SCREENS.SETTINGS.ROOT, params: paramsWithPolicyID};
+ }
+
const topmostCentralPaneRoute = getTopmostCentralPaneRoute(state);
if (topmostCentralPaneRoute === undefined) {
@@ -14,9 +21,6 @@ function getMatchingBottomTabRouteForState(state: State, pol
}
const tabName = CENTRAL_PANE_TO_TAB_MAPPING[topmostCentralPaneRoute.name];
- if (tabName === SCREENS.WORKSPACE.INITIAL) {
- return {name: tabName, params: topmostCentralPaneRoute.params};
- }
return {name: tabName, params: paramsWithPolicyID};
}
diff --git a/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts b/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts
index d4d558708b63..51cb9e3aa9a5 100644
--- a/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts
+++ b/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts
@@ -4,8 +4,6 @@ import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import TAB_TO_CENTRAL_PANE_MAPPING from './TAB_TO_CENTRAL_PANE_MAPPING';
-const WORKSPACES_SCREENS = TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.WORKSPACE.INITIAL].concat(TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.ALL_SETTINGS]);
-
/**
* @param state - react-navigation state
*/
@@ -55,11 +53,11 @@ function getAlreadyOpenedSettingsScreen(rootState?: State, policyID?: string): k
return undefined;
}
- // If one of the screen from WORKSPACES_SCREENS is now in the navigation state, we can decide which screen we should display.
+ // If one of the screen from TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.SETTINGS.ROOT] is now in the navigation state, we can decide which screen we should display.
// A screen from the navigation state can be pushed to the navigation state again only if it has a matching policyID with the currently selected workspace.
// Otherwise, when we switch the workspace, we want to display the initial screen in the settings tab.
const alreadyOpenedSettingsTab = rootState.routes
- .filter((item) => item.params && 'screen' in item.params && WORKSPACES_SCREENS.includes(item.params.screen as keyof CentralPaneNavigatorParamList))
+ .filter((item) => item.params && 'screen' in item.params && TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.SETTINGS.ROOT].includes(item.params.screen as keyof CentralPaneNavigatorParamList))
.at(-1);
if (!hasRouteMatchingPolicyID(alreadyOpenedSettingsTab as NavigationPartialRoute, policyID)) {
@@ -82,7 +80,7 @@ function getMatchingCentralPaneRouteForState(state: State, r
const centralPaneName = TAB_TO_CENTRAL_PANE_MAPPING[topmostBottomTabRoute.name][0];
- if (topmostBottomTabRoute.name === SCREENS.WORKSPACE.INITIAL) {
+ if (topmostBottomTabRoute.name === SCREENS.SETTINGS.ROOT) {
// When we go back to the settings tab without switching the workspace id, we want to return to the previously opened screen
const policyID = topmostBottomTabRoute?.params && 'policyID' in topmostBottomTabRoute.params ? (topmostBottomTabRoute.params.policyID as string) : undefined;
const screen = getAlreadyOpenedSettingsScreen(rootState, policyID) ?? centralPaneName;
diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts
index c425beca73fd..685c21d88e79 100644
--- a/src/libs/Navigation/switchPolicyID.ts
+++ b/src/libs/Navigation/switchPolicyID.ts
@@ -10,7 +10,6 @@ import SCREENS from '@src/SCREENS';
import getStateFromPath from './getStateFromPath';
import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute';
import linkingConfig from './linkingConfig';
-import TAB_TO_CENTRAL_PANE_MAPPING from './linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING';
import type {NavigationRoot, RootStackParamList, StackNavigationAction, State, SwitchPolicyIDParams} from './types';
type ActionPayloadParams = {
@@ -62,7 +61,7 @@ function getActionForBottomTabNavigator(action: StackNavigationAction, state: Na
};
}
-export default function switchPolicyID(navigation: NavigationContainerRef | null, {policyID, route, isPolicyAdmin = false}: SwitchPolicyIDParams) {
+export default function switchPolicyID(navigation: NavigationContainerRef | null, {policyID, route}: SwitchPolicyIDParams) {
if (!navigation) {
throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?");
}
@@ -110,7 +109,7 @@ export default function switchPolicyID(navigation: NavigationContainerRef);
@@ -122,23 +121,12 @@ export default function switchPolicyID(navigation: NavigationContainerRef;
};
-type SettingsCentralPaneNavigatorParamList = {
- [SCREENS.SETTINGS.PROFILE.ROOT]: undefined;
- [SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined;
- [SCREENS.SETTINGS.SECURITY]: undefined;
- [SCREENS.SETTINGS.WALLET.ROOT]: undefined;
- [SCREENS.SETTINGS.ABOUT]: undefined;
+type WorkspacesCentralPaneNavigatorParamList = {
+ [SCREENS.WORKSPACE.PROFILE]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.CARD]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.WORKFLOWS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.WORKFLOWS_APPROVER]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.REIMBURSE]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.BILLS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.INVOICES]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.TRAVEL]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.MEMBERS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.CATEGORIES]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.MORE_FEATURES]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.TAGS]: {
+ policyID: string;
+ categoryName: string;
+ };
+ [SCREENS.WORKSPACE.DISTANCE_RATES]: {
+ policyID: string;
+ };
};
type FullScreenNavigatorParamList = {
- [SCREENS.SETTINGS.ROOT]: undefined;
- [SCREENS.SETTINGS_CENTRAL_PANE]: NavigatorScreenParams;
+ [SCREENS.WORKSPACE.INITIAL]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACES_CENTRAL_PANE]: NavigatorScreenParams;
};
type BottomTabNavigatorParamList = {
[SCREENS.HOME]: undefined;
- [SCREENS.ALL_SETTINGS]: undefined;
- [SCREENS.WORKSPACE.INITIAL]: undefined;
+ [SCREENS.SETTINGS.ROOT]: undefined;
};
type SharedScreensParamList = {
@@ -618,7 +624,7 @@ type BottomTabName = keyof BottomTabNavigatorParamList;
type CentralPaneName = keyof CentralPaneNavigatorParamList;
-type FullScreenName = keyof SettingsCentralPaneNavigatorParamList;
+type FullScreenName = keyof WorkspacesCentralPaneNavigatorParamList;
type SwitchPolicyIDParams = {
policyID?: string;
@@ -672,5 +678,7 @@ export type {
WorkspaceSwitcherNavigatorParamList,
OnboardEngagementNavigatorParamList,
SwitchPolicyIDParams,
+ FullScreenNavigatorParamList,
+ WorkspacesCentralPaneNavigatorParamList,
BackToParams,
};
diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts
index ff63f5aefa2b..15c26b8648fe 100644
--- a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts
+++ b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts
@@ -39,6 +39,7 @@ const backgroundRefresh: BackgroundRefresh = () => {
* See more here: https://reactnative.dev/docs/headless-js-android
*/
App.confirmReadyToOpenApp();
+ Log.info('[PushNotification] Sending ReconnectApp');
App.reconnectApp(lastUpdateIDAppliedToClient ?? undefined);
})
.catch((error) => {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 1aa73e752eae..3de3d1622405 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -31,6 +31,7 @@ import type {
import type {Participant} from '@src/types/onyx/IOU';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
+import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import times from '@src/utils/times';
import Timing from './actions/Timing';
@@ -480,7 +481,7 @@ function getSearchText(
/**
* Get an object of error messages keyed by microtime by combining all error objects related to the report.
*/
-function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry, transactions: OnyxCollection = allTransactions): OnyxCommon.Errors {
+function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors {
const reportErrors = report?.errors ?? {};
const reportErrorFields = report?.errorFields ?? {};
const reportActionErrors: OnyxCommon.ErrorFields = Object.values(reportActions ?? {}).reduce(
@@ -492,7 +493,7 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry<
if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) {
const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null;
- const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) {
reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage');
}
@@ -976,7 +977,17 @@ function getCategoryListSections(
}
if (searchInputValue) {
- const searchCategories = enabledCategories.filter((category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase()));
+ const searchCategories: Category[] = [];
+
+ enabledCategories.forEach((category) => {
+ if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) {
+ return;
+ }
+ searchCategories.push({
+ ...category,
+ isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name),
+ });
+ });
categorySections.push({
// "Search" section
@@ -1764,7 +1775,7 @@ function getShareLogOptions(reports: OnyxCollection, personalDetails: On
/**
* Build the IOUConfirmation options for showing the payee personalDetail
*/
-function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string): PayeePersonalDetails {
+function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails | EmptyObject, amountText?: string): PayeePersonalDetails {
const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? '');
return {
text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin),
@@ -1777,7 +1788,7 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person
id: personalDetail.accountID,
},
],
- descriptiveText: amountText,
+ descriptiveText: amountText ?? '',
login: personalDetail.login ?? '',
accountID: personalDetail.accountID,
keyForList: String(personalDetail.accountID),
@@ -2065,4 +2076,4 @@ export {
getShareLogOptions,
};
-export type {MemberForList, CategorySection, GetOptions};
+export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails};
diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts
index 9dd60eeebcef..65aadd440010 100644
--- a/src/libs/PersonalDetailsUtils.ts
+++ b/src/libs/PersonalDetailsUtils.ts
@@ -26,6 +26,13 @@ Onyx.connect({
function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string {
let displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : '';
+
+ // If the displayName is not set by the user, the backend sets the diplayName same as the login so
+ // we need to remove the sms domain from the displayName if it is an sms login.
+ if (displayName === passedPersonalDetails?.login && Str.isSMSLogin(passedPersonalDetails?.login)) {
+ displayName = Str.removeSMSDomain(displayName);
+ }
+
if (shouldAddCurrentUserPostfix && !!displayName) {
displayName = `${displayName} (${Localize.translateLocal('common.you').toLowerCase()})`;
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index ec31889146f8..8dc1c9967f13 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -31,6 +31,7 @@ import type {
Task,
Transaction,
TransactionViolation,
+ UserWallet,
} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
@@ -51,6 +52,7 @@ 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 store from './actions/ReimbursementAccount/store';
import * as CollectionUtils from './CollectionUtils';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
@@ -424,6 +426,8 @@ type AncestorIDs = {
reportActionsIDs: string[];
};
+type MissingPaymentMethod = 'bankAccount' | 'wallet';
+
let currentUserEmail: string | undefined;
let currentUserPrivateDomain: string | undefined;
let currentUserAccountID: number | undefined;
@@ -917,7 +921,7 @@ function isChatThread(report: OnyxEntry): boolean {
}
function isDM(report: OnyxEntry): boolean {
- return isChatReport(report) && !getChatType(report);
+ return isChatReport(report) && !getChatType(report) && !isThread(report);
}
function isSelfDM(report: OnyxEntry): boolean {
@@ -1177,7 +1181,7 @@ function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined):
* If the report is a thread and has a chat type set, it is a workspace chat.
*/
function isWorkspaceThread(report: OnyxEntry): boolean {
- return isThread(report) && isChatReport(report) && !isDM(report);
+ return isThread(report) && isChatReport(report) && !!getChatType(report);
}
/**
@@ -1243,7 +1247,6 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | EmptyObject | stri
function isOneOnOneChat(report: OnyxEntry): boolean {
const participantAccountIDs = report?.participantAccountIDs ?? [];
return (
- !isThread(report) &&
!isChatRoom(report) &&
!isExpenseRequest(report) &&
!isMoneyRequestReport(report) &&
@@ -5248,6 +5251,30 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry, reportId: string, reportAction: ReportAction): MissingPaymentMethod | undefined {
+ const isSubmitterOfUnsettledReport = isCurrentUserSubmitter(reportId) && !isSettled(reportId);
+ if (!isSubmitterOfUnsettledReport || reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) {
+ return undefined;
+ }
+ const paymentType = reportAction.originalMessage?.paymentType;
+ if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
+ return isEmpty(userWallet) || userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER ? 'wallet' : undefined;
+ }
+
+ return !store.hasCreditBankAccount() ? 'bankAccount' : undefined;
+}
+
+/**
+ * Checks if report chat contains missing payment method
+ */
+function hasMissingPaymentMethod(userWallet: OnyxEntry, iouReportID: string): boolean {
+ const reportActions = ReportActionsUtils.getAllReportActions(iouReportID);
+ return Object.values(reportActions).some((action) => getIndicatedMissingPaymentMethod(userWallet, iouReportID, action) !== undefined);
+}
+
/**
* Used from money request actions to decide if we need to build an optimistic money request report.
Create a new report if:
@@ -5455,6 +5482,7 @@ export {
isValidReport,
getReportDescriptionText,
isReportFieldOfTypeTitle,
+ hasMissingPaymentMethod,
isIOUReportUsingReport,
hasUpdatedTotal,
isReportFieldDisabled,
@@ -5465,6 +5493,7 @@ export {
canEditRoomVisibility,
canEditPolicyDescription,
getPolicyDescriptionText,
+ getIndicatedMissingPaymentMethod,
isJoinRequestInAdminRoom,
canAddOrDeleteTransactions,
shouldCreateNewMoneyRequestReport,
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 40aa4c7247c6..7bf163416054 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -7,7 +7,6 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx';
import type Beta from '@src/types/onyx/Beta';
-import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type Policy from '@src/types/onyx/Policy';
import type Report from '@src/types/onyx/Report';
import type {ReportActions} from '@src/types/onyx/ReportAction';
@@ -58,13 +57,6 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 {
return 0;
}
-function filterDisplayName(displayName: string): string {
- if (CONST.REGEX.INVALID_DISPLAY_NAME_ONLY_LHN.test(displayName)) {
- return displayName;
- }
- return displayName.replace(CONST.REGEX.INVALID_DISPLAY_NAME_LHN, '').trim();
-}
-
/**
* @returns An array of reportIDs sorted in the proper order
*/
@@ -74,27 +66,22 @@ function getOrderedReportIDs(
betas: Beta[],
policies: Record,
priorityMode: ValueOf,
- allReportActions: OnyxCollection,
+ allReportActions: OnyxCollection,
transactionViolations: OnyxCollection,
currentPolicyID = '',
policyMemberAccountIDs: number[] = [],
- reportIDsWithErrors: Record = {},
- canUseViolations = false,
): string[] {
const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD;
const isInDefaultMode = !isInGSDMode;
const allReportsDictValues = Object.values(allReports);
- const reportIDsWithViolations = new Set();
-
// Filter out all the reports that shouldn't be displayed
let reportsToDisplay = allReportsDictValues.filter((report) => {
const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`;
- const parentReportAction = allReportActions?.[parentReportActionsKey]?.[report.parentReportActionID ?? ''];
- const doesReportHaveViolations = canUseViolations && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
- if (doesReportHaveViolations) {
- reportIDsWithViolations.add(report.reportID);
- }
+ const parentReportActions = allReportActions?.[parentReportActionsKey];
+ const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID);
+ const doesReportHaveViolations =
+ betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
return ReportUtils.shouldReportBeInOptionList({
report,
currentReportId: currentReportId ?? '',
@@ -116,7 +103,7 @@ function getOrderedReportIDs(
}
// The LHN is split into four distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order:
- // 1. Pinned/GBR/RBR - Always sorted by reportDisplayName
+ // 1. Pinned/GBR - Always sorted by reportDisplayName
// 2. Drafts - Always sorted by reportDisplayName
// 3. Non-archived reports and settled IOUs
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
@@ -124,7 +111,7 @@ function getOrderedReportIDs(
// 4. Archived reports
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
// - Sorted by reportDisplayName in GSD (focus) view mode
- const pinnedAndBrickRoadReports: Report[] = [];
+ const pinnedAndGBRReports: Report[] = [];
const draftReports: Report[] = [];
const nonArchivedReports: Report[] = [];
const archivedReports: Report[] = [];
@@ -140,14 +127,12 @@ function getOrderedReportIDs(
// However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add
// the reportDisplayName property to the report object directly.
// eslint-disable-next-line no-param-reassign
- report.displayName = filterDisplayName(ReportUtils.getReportName(report));
-
- const hasRBR = report.reportID in reportIDsWithErrors || reportIDsWithViolations.has(report.reportID);
+ report.displayName = ReportUtils.getReportName(report);
const isPinned = report.isPinned ?? false;
const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? '');
- if (isPinned || hasRBR || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) {
- pinnedAndBrickRoadReports.push(report);
+ if (isPinned || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) {
+ pinnedAndGBRReports.push(report);
} else if (report.hasDraft) {
draftReports.push(report);
} else if (ReportUtils.isArchivedRoom(report)) {
@@ -158,7 +143,7 @@ function getOrderedReportIDs(
});
// Sort each group of reports accordingly
- pinnedAndBrickRoadReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
+ pinnedAndGBRReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
draftReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
if (isInDefaultMode) {
@@ -179,7 +164,7 @@ function getOrderedReportIDs(
// Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID.
// The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar.
- const LHNReports = [...pinnedAndBrickRoadReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID);
+ const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID);
return LHNReports;
}
@@ -188,19 +173,19 @@ function getOrderedReportIDs(
*/
function getOptionData({
report,
+ reportActions,
personalDetails,
preferredLocale,
policy,
parentReportAction,
- reportErrors,
hasViolations,
}: {
report: OnyxEntry;
+ reportActions: OnyxEntry;
personalDetails: OnyxEntry;
preferredLocale: DeepValueOf;
policy: OnyxEntry | undefined;
parentReportAction: OnyxEntry | undefined;
- reportErrors: OnyxCommon.Errors | undefined;
hasViolations: boolean;
}): ReportUtils.OptionData | undefined {
// When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for
@@ -213,7 +198,7 @@ function getOptionData({
const result: ReportUtils.OptionData = {
text: '',
alternateText: null,
- allReportErrors: reportErrors,
+ allReportErrors: OptionsListUtils.getAllReportErrors(report, reportActions),
brickRoadIndicator: null,
tooltipText: null,
subtitle: null,
@@ -319,7 +304,8 @@ function getOptionData({
const lastActorDisplayName = OptionsListUtils.getLastActorDisplayName(lastActorDetails, hasMultipleParticipants);
const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report, lastActorDetails, policy);
- let lastMessageText = lastMessageTextFromReport;
+ // We need to remove sms domain in case the last message text has a phone number mention with sms domain.
+ let lastMessageText = Str.removeSMSDomain(lastMessageTextFromReport);
const lastAction = visibleReportActionItems[report.reportID];
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 8a98fe0f2cdc..f8f9ed0e0d47 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -140,11 +140,11 @@ function hasReceipt(transaction: Transaction | undefined | null): boolean {
return !!transaction?.receipt?.state || hasEReceipt(transaction);
}
-function isMerchantMissing(transaction: Transaction) {
- if (transaction.modifiedMerchant && transaction.modifiedMerchant !== '') {
+function isMerchantMissing(transaction: OnyxEntry) {
+ if (transaction?.modifiedMerchant && transaction.modifiedMerchant !== '') {
return transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
}
- const isMerchantEmpty = transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.merchant === '';
+ const isMerchantEmpty = transaction?.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction?.merchant === '';
return isMerchantEmpty;
}
@@ -156,18 +156,18 @@ function isPartialMerchant(merchant: string): boolean {
return merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
}
-function isAmountMissing(transaction: Transaction) {
- return transaction.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0);
+function isAmountMissing(transaction: OnyxEntry) {
+ return transaction?.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0);
}
-function isCreatedMissing(transaction: Transaction) {
- return transaction.created === '' && (!transaction.created || transaction.modifiedCreated === '');
+function isCreatedMissing(transaction: OnyxEntry) {
+ return transaction?.created === '' && (!transaction.created || transaction.modifiedCreated === '');
}
-function areRequiredFieldsEmpty(transaction: Transaction): boolean {
+function areRequiredFieldsEmpty(transaction: OnyxEntry): boolean {
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`] ?? null;
const isFromExpenseReport = parentReport?.type === CONST.REPORT.TYPE.EXPENSE;
- const isSplitPolicyExpenseChat = !!transaction.comment?.splits?.some((participant) => allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`]?.isOwnPolicyExpenseChat);
+ const isSplitPolicyExpenseChat = !!transaction?.comment?.splits?.some((participant) => allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`]?.isOwnPolicyExpenseChat);
const isMerchantRequired = isFromExpenseReport || isSplitPolicyExpenseChat;
return (isMerchantRequired && isMerchantMissing(transaction)) || isAmountMissing(transaction) || isCreatedMissing(transaction);
}
@@ -489,7 +489,7 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean
/**
* Check if the transaction has a defined route
*/
-function hasRoute(transaction: Transaction): boolean {
+function hasRoute(transaction: OnyxEntry): boolean {
return !!transaction?.routes?.route0?.geometry?.coordinates;
}
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 6442f2ec0eef..302e4048d0e8 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -222,7 +222,10 @@ function openApp() {
function reconnectApp(updateIDFrom: OnyxEntry = 0) {
console.debug(`[OnyxUpdates] App reconnecting with updateIDFrom: ${updateIDFrom}`);
getPolicyParamsForOpenOrReconnect().then((policyParams) => {
- const params: ReconnectAppParams = {...policyParams};
+ const params: ReconnectAppParams = {
+ ...policyParams,
+ idempotencyKey: `${WRITE_COMMANDS.RECONNECT_APP}`,
+ };
// When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID.
// we have locally. And then only update the user about chats with messages that have occurred after that reportActionID.
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 85572ebe04a7..48b9948fc1ff 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -26,6 +26,7 @@ import type {
OpenPolicyCategoriesPageParams,
OpenPolicyDistanceRatesPageParams,
OpenPolicyTagsPageParams,
+ OpenPolicyWorkflowsPageParams,
OpenWorkspaceInvitePageParams,
OpenWorkspaceMembersPageParams,
OpenWorkspaceParams,
@@ -34,6 +35,8 @@ import type {
SetWorkspaceAutoReportingFrequencyParams,
SetWorkspaceAutoReportingMonthlyOffsetParams,
SetWorkspaceAutoReportingParams,
+ SetWorkspacePayerParams,
+ SetWorkspaceReimbursementParams,
UpdateWorkspaceAvatarParams,
UpdateWorkspaceCustomUnitAndRateParams,
UpdateWorkspaceDescriptionParams,
@@ -589,6 +592,106 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo
API.write(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, params, {optimisticData, failureData, successData});
}
+function setWorkspacePayer(policyID: string, reimburserEmail: string, reimburserAccountID: number) {
+ const policy = ReportUtils.getPolicy(policyID);
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ reimburserEmail,
+ reimburserAccountID,
+ errorFields: {reimburserEmail: null},
+ pendingFields: {reimburserEmail: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ errorFields: {reimburserEmail: null},
+ pendingFields: {reimburserEmail: null},
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ reimburserEmail: policy.reimburserEmail,
+ reimburserAccountID: policy.reimburserAccountID,
+ errorFields: {reimburserEmail: ErrorUtils.getMicroSecondOnyxError('workflowsPayerPage.genericErrorMessage')},
+ pendingFields: {reimburserEmail: null},
+ },
+ },
+ ];
+
+ const params: SetWorkspacePayerParams = {policyID, reimburserEmail};
+
+ API.write(WRITE_COMMANDS.SET_WORKSPACE_PAYER, params, {optimisticData, failureData, successData});
+}
+
+function clearWorkspacePayerError(policyID: string) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {reimburserEmail: null}});
+}
+
+function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserAccountID: number, reimburserEmail: string) {
+ const policy = ReportUtils.getPolicy(policyID);
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ reimbursementChoice,
+ reimburserAccountID,
+ reimburserEmail,
+ errorFields: {reimbursementChoice: null},
+ pendingFields: {reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ errorFields: {reimbursementChoice: null},
+ pendingFields: {reimbursementChoice: null},
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ reimbursementChoice: policy.reimbursementChoice,
+ reimburserAccountID: policy.reimburserAccountID,
+ reimburserEmail: policy.reimburserEmail,
+ errorFields: {reimbursementChoice: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')},
+ pendingFields: {reimbursementChoice: null},
+ },
+ },
+ ];
+
+ const params: SetWorkspaceReimbursementParams = {policyID, reimbursementChoice};
+
+ API.write(WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT, params, {optimisticData, failureData, successData});
+}
+
+function clearWorkspaceReimbursementErrors(policyID: string) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {reimbursementChoice: null}});
+}
+
/**
* Build optimistic data for removing users from the announcement room
*/
@@ -1887,6 +1990,50 @@ function openWorkspaceReimburseView(policyID: string) {
API.read(READ_COMMANDS.OPEN_WORKSPACE_REIMBURSE_VIEW, params, {successData, failureData});
}
+function openPolicyWorkflowsPage(policyID: string) {
+ if (!policyID) {
+ Log.warn('openPolicyWorkflowsPage invalid params', {policyID});
+ return;
+ }
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
+ key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`,
+ value: {
+ isLoading: true,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
+ key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
+ key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ };
+
+ const params: OpenPolicyWorkflowsPageParams = {policyID};
+
+ API.read(READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE, params, onyxData);
+}
+
function setPolicyIDForReimburseView(policyID: string) {
Onyx.merge(ONYXKEYS.WORKSPACE_RATE_AND_UNIT, {policyID, rate: null, unit: null});
}
@@ -2599,6 +2746,70 @@ function createPolicyCategory(policyID: string, categoryName: string) {
API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData);
}
+function createPolicyTag(policyID: string, tagName: string) {
+ const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[0];
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ [tagListName]: {
+ tags: {
+ [tagName]: {
+ name: tagName,
+ enabled: false,
+ errors: null,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ },
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ [tagListName]: {
+ tags: {
+ [tagName]: {
+ errors: null,
+ pendingAction: null,
+ },
+ },
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ [tagListName]: {
+ tags: {
+ [tagName]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'),
+ pendingAction: null,
+ },
+ },
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ tags: JSON.stringify([{name: tagName}]),
+ };
+
+ API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData);
+}
+
function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) {
const onyxData: OnyxData = {
optimisticData: [
@@ -3292,6 +3503,10 @@ export {
declineJoinRequest,
createPolicyCategory,
clearCategoryErrors,
+ setWorkspacePayer,
+ clearWorkspacePayerError,
+ setWorkspaceReimbursement,
+ openPolicyWorkflowsPage,
setPolicyRequiresTag,
renamePolicyTaglist,
enablePolicyCategories,
@@ -3302,4 +3517,6 @@ export {
enablePolicyTaxes,
enablePolicyWorkflows,
openPolicyDistanceRatesPage,
+ createPolicyTag,
+ clearWorkspaceReimbursementErrors,
};
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index b677b5369b68..5c20536e5234 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -2229,10 +2229,10 @@ function toggleEmojiReaction(
addEmojiReaction(originalReportID, reportAction.reportActionID, emoji, skinTone);
}
-function openReportFromDeepLink(url: string, isAuthenticated: boolean) {
+function openReportFromDeepLink(url: string) {
const reportID = ReportUtils.getReportIDFromLink(url);
- if (reportID && !isAuthenticated) {
+ if (reportID && !Session.hasAuthToken()) {
// Call the OpenReport command to check in the server if it's a public room. If so, we'll open it as an anonymous user
openReport(reportID, [], {}, '0', true);
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 619281ac7ecf..2c2baee9b96e 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -184,7 +184,14 @@ function hasStashedSession(): boolean {
return Boolean(stashedSession.authToken && stashedCredentials.autoGeneratedLogin && stashedCredentials.autoGeneratedLogin !== '');
}
-function signOutAndRedirectToSignIn(shouldReplaceCurrentScreen?: boolean, shouldStashSession?: boolean) {
+/**
+ * Checks if the user has authToken
+ */
+function hasAuthToken(): boolean {
+ return !!session.authToken;
+}
+
+function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean) {
Log.info('Redirecting to Sign In because signOut() was called');
hideContextMenu(false);
if (!isAnonymousUser()) {
@@ -233,11 +240,10 @@ function signOutAndRedirectToSignIn(shouldReplaceCurrentScreen?: boolean, should
if (Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) {
return;
}
- if (shouldReplaceCurrentScreen) {
- Navigation.navigate(ROUTES.SIGN_IN_MODAL, CONST.NAVIGATION.TYPE.UP);
- } else {
- Navigation.navigate(ROUTES.SIGN_IN_MODAL);
+ if (shouldResetToHome) {
+ Navigation.resetToHome();
}
+ Navigation.navigate(ROUTES.SIGN_IN_MODAL);
Linking.getInitialURL().then((url) => {
const reportID = ReportUtils.getReportIDFromLink(url);
if (reportID) {
@@ -987,6 +993,7 @@ export {
toggleTwoFactorAuth,
validateTwoFactorAuth,
waitForUserSignIn,
+ hasAuthToken,
canAnonymousUserAccessRoute,
signInWithSupportAuthToken,
isSupportAuthToken,
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index 3e00d8084825..48ab7cce9186 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -689,8 +689,8 @@ function clearOutTaskInfoAndNavigate(reportID: string) {
/**
* Get the assignee data
*/
-function getAssignee(assigneeAccountID: number, personalDetails: OnyxTypes.PersonalDetailsList): Assignee {
- const details = personalDetails[assigneeAccountID];
+function getAssignee(assigneeAccountID: number, personalDetails: OnyxEntry): Assignee {
+ const details = personalDetails?.[assigneeAccountID];
if (!details) {
return {
@@ -710,7 +710,7 @@ function getAssignee(assigneeAccountID: number, personalDetails: OnyxTypes.Perso
/**
* Get the share destination data
* */
-function getShareDestination(reportID: string, reports: OnyxCollection, personalDetails: OnyxTypes.PersonalDetailsList): ShareDestination {
+function getShareDestination(reportID: string, reports: OnyxCollection, personalDetails: OnyxEntry): ShareDestination {
const report = reports?.[`report_${reportID}`] ?? null;
const participantAccountIDs = report?.participantAccountIDs ?? [];
@@ -721,8 +721,8 @@ function getShareDestination(reportID: string, reports: OnyxCollection PolicyUtils.isPolicyMember(report?.policyID ?? '', policies), [report?.policyID, policies]);
const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(report), [report]);
const isChatRoom = useMemo(() => ReportUtils.isChatRoom(report), [report]);
- const isThread = useMemo(() => ReportUtils.isChatThread(report), [report]);
const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(report), [report]);
+ const isDefaultRoom = useMemo(() => ReportUtils.isDefaultRoom(report), [report]);
+ const isChatThread = useMemo(() => ReportUtils.isChatThread(report), [report]);
const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(report), [report]);
const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(report), [report]);
const canEditReportDescription = useMemo(() => ReportUtils.canEditReportDescription(report, policy), [report, policy]);
@@ -117,9 +118,15 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
}
// The Members page is only shown when:
+ // - The report is a thread in a chat report
// - The report is not a user created room with participants to show i.e. DM, Group Chat, etc
// - The report is a user created room and the room and the current user is a workspace member i.e. non-workspace members should not see this option.
- if ((!isUserCreatedPolicyRoom && participants.length) || (isUserCreatedPolicyRoom && isPolicyMember)) {
+ if (
+ ((isDefaultRoom && isChatThread && isPolicyMember) ||
+ (!isUserCreatedPolicyRoom && participants.length) ||
+ (isUserCreatedPolicyRoom && (isPolicyMember || (isChatThread && !ReportUtils.isPublicRoom(report))))) &&
+ !ReportUtils.isConciergeChatReport(report)
+ ) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.MEMBERS,
translationKey: 'common.members',
@@ -127,14 +134,17 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
subtitle: participants.length,
isAnonymousAction: false,
action: () => {
- if (isUserCreatedPolicyRoom && !report?.parentReportID) {
+ if (isUserCreatedPolicyRoom || isChatThread) {
Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? ''));
} else {
Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? ''));
}
},
});
- } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !report?.parentReportID) {
+ } else if (
+ (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember)) ||
+ ((isDefaultRoom || ReportUtils.isPolicyExpenseChat(report)) && isChatThread && !isPolicyMember)
+ ) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE,
translationKey: 'common.invite',
@@ -157,7 +167,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
});
// Prevent displaying private notes option for threads and task reports
- if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(report)) {
+ if (!isChatThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(report)) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.PRIVATE_NOTES,
translationKey: 'privateNotes.title',
@@ -169,7 +179,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
}
return items;
- }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, session, isSelfDM]);
+ }, [isArchivedRoom, participants.length, isChatThread, isMoneyRequestReport, report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, session, isSelfDM, isDefaultRoom]);
const displayNamesWithTooltips = useMemo(() => {
const hasMultipleParticipants = participants.length > 1;
@@ -217,8 +227,8 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
fullTitle={ReportUtils.getReportName(report)}
displayNamesWithTooltips={displayNamesWithTooltips}
tooltipEnabled
- numberOfLines={isChatRoom && !isThread ? 0 : 1}
- textStyles={[styles.textHeadline, styles.textAlignCenter, isChatRoom && !isThread ? undefined : styles.pre]}
+ numberOfLines={isChatRoom && !isChatThread ? 0 : 1}
+ textStyles={[styles.textHeadline, styles.textAlignCenter, isChatRoom && !isChatThread ? undefined : styles.pre]}
shouldUseFullTitle={shouldUseFullTitle}
/>
diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx
index 82402d62a074..89d20287f66c 100644
--- a/src/pages/RoomMembersPage.tsx
+++ b/src/pages/RoomMembersPage.tsx
@@ -224,7 +224,9 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
testID={RoomMembersPage.displayName}
>
{
Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx
index 3d46ede8b31e..2eb5ecaf373f 100644
--- a/src/pages/WorkspaceSwitcherPage.tsx
+++ b/src/pages/WorkspaceSwitcherPage.tsx
@@ -18,6 +18,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -102,7 +103,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) {
return;
}
- const {policyID, isPolicyAdmin} = option;
+ const {policyID} = option;
if (policyID) {
setSelectedOption(option);
@@ -112,7 +113,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) {
setActiveWorkspaceID(policyID);
Navigation.goBack();
if (policyID !== activeWorkspaceID) {
- Navigation.navigateWithSwitchPolicyID({policyID, isPolicyAdmin});
+ Navigation.navigateWithSwitchPolicyID({policyID});
}
},
[activeWorkspaceID, setActiveWorkspaceID],
@@ -219,7 +220,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) {
role={CONST.ROLE.BUTTON}
onPress={() => {
Navigation.goBack();
- App.createWorkspaceWithPolicyDraftAndNavigateToIt();
+ interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt());
}}
>
{({hovered}) => (
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index d4221c324ade..ab09047ede68 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -430,7 +430,7 @@ function ReportScreen({
}, [report, didSubscribeToReportLeavingEvents, reportID]);
const onListLayout = useCallback((event: LayoutChangeEvent) => {
- setListHeight((prev) => event.nativeEvent.layout.height ?? prev);
+ setListHeight((prev) => event.nativeEvent?.layout?.height ?? prev);
if (!markReadyForHydration) {
return;
}
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
index 931b87704ce5..ea3828c762c0 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
@@ -30,7 +30,6 @@ function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEv
return event;
}
-// eslint-disable-next-line @typescript-eslint/naming-convention
function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef) {
const {translate} = useLocalize();
const reportIDRef = useRef('0');
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index a9aba936f807..7216f4238621 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -53,7 +53,6 @@ import {ReactionListContext} from '@pages/home/ReportScreenContext';
import * as BankAccounts from '@userActions/BankAccounts';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
import * as Policy from '@userActions/Policy';
-import * as store from '@userActions/ReimbursementAccount/store';
import * as Report from '@userActions/Report';
import * as ReportActions from '@userActions/ReportActions';
import * as Session from '@userActions/Session';
@@ -470,17 +469,13 @@ function ReportActionItem({
const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails[report.ownerAccountID ?? -1]);
const paymentType = action.originalMessage.paymentType ?? '';
- const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(report.reportID) && !ReportUtils.isSettled(report.reportID);
- const shouldShowAddCreditBankAccountButton = isSubmitterOfUnsettledReport && !store.hasCreditBankAccount() && paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
- const shouldShowEnableWalletButton =
- isSubmitterOfUnsettledReport && (isEmptyObject(userWallet) || userWallet?.tierName === CONST.WALLET.TIER_NAME.SILVER) && paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
-
+ const missingPaymentMethod = ReportUtils.getIndicatedMissingPaymentMethod(userWallet, report.reportID, action);
children = (
<>
- {shouldShowAddCreditBankAccountButton && (
+ {missingPaymentMethod === 'bankAccount' && (
)}
-
- {shouldShowEnableWalletButton && (
+ {missingPaymentMethod === 'wallet' && (
Navigation.navigate(ROUTES.ENABLE_PAYMENTS)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.js b/src/pages/home/sidebar/AvatarWithOptionalStatus.js
index e1ff3982a0cc..942d5c1da1f2 100644
--- a/src/pages/home/sidebar/AvatarWithOptionalStatus.js
+++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.js
@@ -1,61 +1,53 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
import PropTypes from 'prop-types';
-import React, {useCallback} from 'react';
+import React from 'react';
import {View} from 'react-native';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Text from '@components/Text';
-import Tooltip from '@components/Tooltip';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
-import ROUTES from '@src/ROUTES';
import PressableAvatarWithIndicator from './PressableAvatarWithIndicator';
const propTypes = {
- /** Whether the create menu is open or not */
- isCreateMenuOpen: PropTypes.bool,
-
/** Emoji status */
emojiStatus: PropTypes.string,
+
+ /** Whether the avatar is selected */
+ isSelected: PropTypes.bool,
+
+ /** Callback called when the avatar or status icon is pressed */
+ onPress: PropTypes.func,
};
const defaultProps = {
- isCreateMenuOpen: false,
emojiStatus: '',
+ isSelected: false,
+ onPress: () => {},
};
-function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) {
+function AvatarWithOptionalStatus({emojiStatus, isSelected, onPress}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const showStatusPage = useCallback(() => {
- if (isCreateMenuOpen) {
- // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
- return;
- }
-
- Navigation.setShouldPopAllStateOnUP();
- Navigation.navigate(ROUTES.SETTINGS_STATUS);
- }, [isCreateMenuOpen]);
-
return (
-
+
-
-
- {emojiStatus}
-
-
+
+ {emojiStatus}
+
);
diff --git a/src/pages/home/sidebar/BottomTabAvatar.tsx b/src/pages/home/sidebar/BottomTabAvatar.tsx
new file mode 100644
index 000000000000..15134b762161
--- /dev/null
+++ b/src/pages/home/sidebar/BottomTabAvatar.tsx
@@ -0,0 +1,49 @@
+/* eslint-disable rulesdir/onyx-props-must-have-default */
+import React, {useCallback} from 'react';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
+import Navigation from '@libs/Navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+import AvatarWithOptionalStatus from './AvatarWithOptionalStatus';
+import PressableAvatarWithIndicator from './PressableAvatarWithIndicator';
+
+type BottomTabAvatarProps = {
+ /** Whether the create menu is open or not */
+ isCreateMenuOpen?: boolean;
+
+ /** Whether the avatar is selected */
+ isSelected?: boolean;
+};
+
+function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomTabAvatarProps) {
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const emojiStatus = currentUserPersonalDetails?.status?.emojiCode ?? '';
+
+ const showSettingsPage = useCallback(() => {
+ if (isCreateMenuOpen) {
+ // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
+ return;
+ }
+
+ interceptAnonymousUser(() => Navigation.navigate(ROUTES.SETTINGS));
+ }, [isCreateMenuOpen]);
+
+ if (emojiStatus) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+}
+
+BottomTabAvatar.displayName = 'BottomTabAvatar';
+export default BottomTabAvatar;
diff --git a/src/pages/home/sidebar/PressableAvatarWithIndicator.js b/src/pages/home/sidebar/PressableAvatarWithIndicator.js
index 63c5936e957b..a7345ff6c14a 100644
--- a/src/pages/home/sidebar/PressableAvatarWithIndicator.js
+++ b/src/pages/home/sidebar/PressableAvatarWithIndicator.js
@@ -1,67 +1,65 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback} from 'react';
+import React from 'react';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import AvatarWithIndicator from '@components/AvatarWithIndicator';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
-import Navigation from '@libs/Navigation/Navigation';
import * as UserUtils from '@libs/UserUtils';
import personalDetailsPropType from '@pages/personalDetailsPropType';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
const propTypes = {
- /** Whether the create menu is open or not */
- isCreateMenuOpen: PropTypes.bool,
-
/** The personal details of the person who is logged in */
currentUserPersonalDetails: personalDetailsPropType,
/** Indicates whether the app is loading initial data */
isLoading: PropTypes.bool,
+
+ /** Whether the avatar is selected */
+ isSelected: PropTypes.bool,
+
+ /** Callback called when the avatar is pressed */
+ onPress: PropTypes.func,
};
const defaultProps = {
- isCreateMenuOpen: false,
currentUserPersonalDetails: {
pendingFields: {avatar: ''},
accountID: '',
avatar: '',
},
isLoading: true,
+ isSelected: false,
+ onPress: () => {},
};
-function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDetails, isLoading}) {
+function PressableAvatarWithIndicator({currentUserPersonalDetails, isLoading, isSelected, onPress}) {
const {translate} = useLocalize();
-
- const showSettingsPage = useCallback(() => {
- if (isCreateMenuOpen) {
- // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
- return;
- }
-
- Navigation.navigate(ROUTES.SETTINGS);
- }, [isCreateMenuOpen]);
+ const styles = useThemeStyles();
return (
-
+
+
+
);
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 0a97f00c5002..4d1585cd424a 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -1,11 +1,8 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
-import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import {InteractionManager, StyleSheet, View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
-import Breadcrumbs from '@components/Breadcrumbs';
import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList';
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
import useLocalize from '@hooks/useLocalize';
@@ -43,11 +40,11 @@ const propTypes = {
isActiveReport: PropTypes.func.isRequired,
};
-function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen, activePolicy, reportIDsWithErrors}) {
+function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen}) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const modal = useRef({});
- const {translate, updateLocale} = useLocalize();
+ const {updateLocale} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
useEffect(() => {
@@ -129,22 +126,6 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
return (
-
{isLoading && optionListItems.length === 0 && (
@@ -169,9 +149,5 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
SidebarLinks.propTypes = propTypes;
SidebarLinks.displayName = 'SidebarLinks';
-export default withOnyx({
- activePolicy: {
- key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`,
- },
-})(SidebarLinks);
+export default SidebarLinks;
export {basePropTypes};
diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js
index 3a1b17aa7fbd..c4cc0713c596 100644
--- a/src/pages/home/sidebar/SidebarLinksData.js
+++ b/src/pages/home/sidebar/SidebarLinksData.js
@@ -1,6 +1,6 @@
import {deepEqual} from 'fast-equals';
import lodashGet from 'lodash/get';
-import lodashMapValues from 'lodash/mapValues';
+import lodashMap from 'lodash/map';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
@@ -13,11 +13,9 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro
import withNavigationFocus from '@components/withNavigationFocus';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import SidebarUtils from '@libs/SidebarUtils';
@@ -37,11 +35,20 @@ const propTypes = {
/** All report actions for all reports */
/** Object of report actions for this report */
- // eslint-disable-next-line react/forbid-prop-types
- allReportActions: PropTypes.object,
-
- // eslint-disable-next-line react/forbid-prop-types
- allTransactions: PropTypes.object,
+ allReportActions: PropTypes.objectOf(
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ error: PropTypes.string,
+ message: PropTypes.arrayOf(
+ PropTypes.shape({
+ moderationDecision: PropTypes.shape({
+ decision: PropTypes.string,
+ }),
+ }),
+ ),
+ }),
+ ),
+ ),
/** Whether the reports are loading. When false it means they are ready to be used. */
isLoadingApp: PropTypes.bool,
@@ -98,14 +105,12 @@ const defaultProps = {
policyMembers: {},
transactionViolations: {},
allReportActions: {},
- allTransactions: {},
...withCurrentUserPersonalDetailsDefaultProps,
};
function SidebarLinksData({
isFocused,
allReportActions,
- allTransactions,
betas,
chatReports,
currentReportID,
@@ -123,30 +128,12 @@ function SidebarLinksData({
const {activeWorkspaceID} = useActiveWorkspace();
const {translate} = useLocalize();
const prevPriorityMode = usePrevious(priorityMode);
- const {canUseViolations} = usePermissions();
const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, currentUserPersonalDetails.accountID);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => Policy.openWorkspace(activeWorkspaceID, policyMemberAccountIDs), [activeWorkspaceID]);
- const reportIDsWithErrors = useMemo(() => {
- const reportKeys = _.keys(chatReports);
- return _.reduce(
- reportKeys,
- (errorsMap, reportKey) => {
- const report = chatReports[reportKey];
- const allReportsActions = allReportActions[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS)];
- const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions, allTransactions) || {};
- if (_.size(errors) === 0) {
- return errorsMap;
- }
- return {...errorsMap, [reportKey.replace(ONYXKEYS.COLLECTION.REPORT, '')]: errors};
- },
- {},
- );
- }, [allReportActions, allTransactions, chatReports]);
-
const reportIDsRef = useRef(null);
const isLoading = isLoadingApp;
const optionListItems = useMemo(() => {
@@ -160,8 +147,6 @@ function SidebarLinksData({
transactionViolations,
activeWorkspaceID,
policyMemberAccountIDs,
- reportIDsWithErrors,
- canUseViolations,
);
if (deepEqual(reportIDsRef.current, reportIDs)) {
@@ -175,21 +160,7 @@ function SidebarLinksData({
reportIDsRef.current = reportIDs;
}
return reportIDsRef.current || [];
- }, [
- chatReports,
- betas,
- policies,
- priorityMode,
- allReportActions,
- transactionViolations,
- activeWorkspaceID,
- policyMemberAccountIDs,
- reportIDsWithErrors,
- canUseViolations,
- isLoading,
- network.isOffline,
- prevPriorityMode,
- ]);
+ }, [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, isLoading, network.isOffline, prevPriorityMode]);
// We need to make sure the current report is in the list of reports, but we do not want
// to have to re-generate the list every time the currentReportID changes. To do that
@@ -208,25 +179,10 @@ function SidebarLinksData({
transactionViolations,
activeWorkspaceID,
policyMemberAccountIDs,
- reportIDsWithErrors,
- canUseViolations,
);
}
return optionListItems;
- }, [
- currentReportID,
- optionListItems,
- chatReports,
- betas,
- policies,
- priorityMode,
- allReportActions,
- transactionViolations,
- activeWorkspaceID,
- policyMemberAccountIDs,
- reportIDsWithErrors,
- canUseViolations,
- ]);
+ }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs]);
const currentReportIDRef = useRef(currentReportID);
currentReportIDRef.current = currentReportID;
@@ -248,7 +204,6 @@ function SidebarLinksData({
isLoading={isLoading}
optionListItems={optionListItemsWithCurrentReport}
activeWorkspaceID={activeWorkspaceID}
- reportIDsWithErrors={reportIDsWithErrors}
/>
);
@@ -272,7 +227,6 @@ const chatReportSelector = (report) =>
isPinned: report.isPinned,
isHidden: report.isHidden,
notificationPreference: report.notificationPreference,
- errors: report.errors,
errorFields: {
addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom,
},
@@ -294,9 +248,6 @@ const chatReportSelector = (report) =>
reportName: report.reportName,
policyName: report.policyName,
oldPolicyName: report.oldPolicyName,
- isPolicyExpenseChat: report.isPolicyExpenseChat,
- isOwnPolicyExpenseChat: report.isOwnPolicyExpenseChat,
- isCancelledIOU: report.isCancelledIOU,
// Other less obvious properites considered for sorting:
ownerAccountID: report.ownerAccountID,
currency: report.currency,
@@ -314,7 +265,7 @@ const chatReportSelector = (report) =>
*/
const reportActionsSelector = (reportActions) =>
reportActions &&
- lodashMapValues(reportActions, (reportAction) => {
+ lodashMap(reportActions, (reportAction) => {
const {reportActionID, parentReportActionID, actionName, errors = [], originalMessage} = reportAction;
const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision');
@@ -343,24 +294,6 @@ const policySelector = (policy) =>
avatar: policy.avatar,
};
-/**
- * @param {Object} [transaction]
- * @returns {Object|undefined}
- */
-const transactionSelector = (transaction) =>
- transaction && {
- reportID: transaction.reportID,
- iouRequestType: transaction.iouRequestType,
- comment: transaction.comment,
- receipt: transaction.receipt,
- merchant: transaction.merchant,
- modifiedMerchant: transaction.modifiedMerchant,
- amount: transaction.amount,
- modifiedAmount: transaction.modifiedAmount,
- created: transaction.created,
- modifiedCreated: transaction.modifiedCreated,
- };
-
export default compose(
withCurrentReportID,
withCurrentUserPersonalDetails,
@@ -388,11 +321,6 @@ export default compose(
selector: reportActionsSelector,
initialValue: {},
},
- allTransactions: {
- key: ONYXKEYS.COLLECTION.TRANSACTION,
- selector: transactionSelector,
- initialValue: {},
- },
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
selector: policySelector,
diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
index 9188a859d175..2c2d28a0edbc 100644
--- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
+++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
@@ -1,8 +1,10 @@
import React, {useEffect} from 'react';
import {View} from 'react-native';
import ScreenWrapper from '@components/ScreenWrapper';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Browser from '@libs/Browser';
+import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar';
import Performance from '@libs/Performance';
import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData';
import Timing from '@userActions/Timing';
@@ -19,6 +21,7 @@ const startTimer = () => {
function BaseSidebarScreen(props) {
const styles = useThemeStyles();
+ const {activeWorkspaceID} = useActiveWorkspace();
useEffect(() => {
Performance.markStart(CONST.TIMING.SIDEBAR_LOADED);
Timing.start(CONST.TIMING.SIDEBAR_LOADED, true);
@@ -33,13 +36,16 @@ function BaseSidebarScreen(props) {
includePaddingTop={false}
>
{({insets}) => (
-
-
-
+ <>
+
+
+
+
+ >
)}
);
diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js
index 7086e8a8561a..dfb5db7c15d3 100755
--- a/src/pages/home/sidebar/SidebarScreen/index.js
+++ b/src/pages/home/sidebar/SidebarScreen/index.js
@@ -1,14 +1,11 @@
import React from 'react';
-import useWindowDimensions from '@hooks/useWindowDimensions';
import FreezeWrapper from '@libs/Navigation/FreezeWrapper';
import BaseSidebarScreen from './BaseSidebarScreen';
import sidebarPropTypes from './sidebarPropTypes';
function SidebarScreen(props) {
- const {isSmallScreenWidth} = useWindowDimensions();
-
return (
-
+
;
- }
- if (emojiStatus) {
- return (
-
- );
- }
- return ;
-}
-
-SignInOrAvatarWithOptionalStatus.propTypes = propTypes;
-SignInOrAvatarWithOptionalStatus.defaultProps = defaultProps;
-SignInOrAvatarWithOptionalStatus.displayName = 'SignInOrAvatarWithOptionalStatus';
-export default SignInOrAvatarWithOptionalStatus;
diff --git a/src/pages/settings/AppDownloadLinks.tsx b/src/pages/settings/AppDownloadLinks.tsx
index e4165178ff2f..00f6aeec04db 100644
--- a/src/pages/settings/AppDownloadLinks.tsx
+++ b/src/pages/settings/AppDownloadLinks.tsx
@@ -47,7 +47,7 @@ function AppDownloadLinksPage() {
{
translationKey: 'initialSettingsPage.appDownloadLinks.desktop.label',
action: () => {
- Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.DESKTOP);
+ Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.DESKTOP, true);
},
link: CONST.APP_DOWNLOAD_LINKS.DESKTOP,
icon: Expensicons.Monitor,
diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx
index 2f2343027cf0..a57f308b5623 100755
--- a/src/pages/settings/InitialSettingsPage.tsx
+++ b/src/pages/settings/InitialSettingsPage.tsx
@@ -1,23 +1,24 @@
-import {useNavigationState} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import ConfirmModal from '@components/ConfirmModal';
import CurrentUserPersonalDetailsSkeletonView from '@components/CurrentUserPersonalDetailsSkeletonView';
-import HeaderPageLayout from '@components/HeaderPageLayout';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {PressableWithFeedback} from '@components/Pressable';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
+import useActiveRoute from '@hooks/useActiveRoute';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useSingleExecution from '@hooks/useSingleExecution';
@@ -25,9 +26,10 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import getTopmostSettingsCentralPaneName from '@libs/Navigation/getTopmostSettingsCentralPaneName';
import Navigation from '@libs/Navigation/Navigation';
+import shouldShowSubscriptionsMenu from '@libs/shouldShowSubscriptionsMenu';
import * as UserUtils from '@libs/UserUtils';
+import {hasGlobalWorkspaceSettingsRBR} from '@libs/WorkspacesSettingsUtils';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import variables from '@styles/variables';
import * as Link from '@userActions/Link';
@@ -40,7 +42,6 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import type {Icon as TIcon} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -64,6 +65,12 @@ type InitialSettingsPageOnyxProps = {
/** Login list for the user that is signed in */
loginList: OnyxEntry;
+
+ /** The policies which the user has access to */
+ policies: OnyxCollection;
+
+ /** Members of all the workspaces the user is member of */
+ policyMembers: OnyxCollection;
};
type InitialSettingsPageProps = InitialSettingsPageOnyxProps & WithCurrentUserPersonalDetailsProps;
@@ -88,7 +95,7 @@ type MenuData = {
type Menu = {sectionStyle: StyleProp; sectionTranslationKey: TranslationPaths; items: MenuData[]};
-function InitialSettingsPage({session, userWallet, bankAccountList, fundList, walletTerms, loginList, currentUserPersonalDetails}: InitialSettingsPageProps) {
+function InitialSettingsPage({session, userWallet, bankAccountList, fundList, walletTerms, loginList, currentUserPersonalDetails, policies, policyMembers}: InitialSettingsPageProps) {
const network = useNetwork();
const theme = useTheme();
const styles = useThemeStyles();
@@ -96,7 +103,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
const waitForNavigate = useWaitForNavigation();
const popoverAnchor = useRef(null);
const {translate, formatPhoneNumber} = useLocalize();
- const activeRoute = useNavigationState(getTopmostSettingsCentralPaneName);
+ const activeRoute = useActiveRoute();
const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? '';
const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false);
@@ -129,7 +136,6 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
const accountMenuItemsData: Menu = useMemo(() => {
const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(loginList);
const paymentCardList = fundList;
- const signOutTranslationKey = Session.isSupportAuthToken() && Session.hasStashedSession() ? 'initialSettingsPage.restoreStashed' : 'initialSettingsPage.signOut';
const defaultMenu: Menu = {
sectionStyle: styles.accountSettingsSectionContainer,
sectionTranslationKey: 'initialSettingsPage.account',
@@ -164,32 +170,70 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
icon: Expensicons.Lock,
routeName: ROUTES.SETTINGS_SECURITY,
},
- {
- translationKey: signOutTranslationKey,
- icon: Expensicons.Exit,
- action: () => {
- signOut(false);
- },
- },
],
};
return defaultMenu;
- }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors, signOut]);
+ }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors]);
+
+ /**
+ * Retuns a list of menu items data for workspace section
+ * @returns object with translationKey, style and items for the workspace section
+ */
+ const workspaceMenuItemsData: Menu = useMemo(() => {
+ const items: MenuData[] = [
+ {
+ translationKey: 'common.workspaces',
+ icon: Expensicons.Building,
+ routeName: ROUTES.SETTINGS_WORKSPACES,
+ brickRoadIndicator: hasGlobalWorkspaceSettingsRBR(policies, policyMembers) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
+ },
+ {
+ translationKey: 'allSettingsScreen.cardsAndDomains',
+ icon: Expensicons.CardsAndDomains,
+ action: () => {
+ Link.openOldDotLink(CONST.OLDDOT_URLS.ADMIN_DOMAINS_URL);
+ },
+ shouldShowRightIcon: true,
+ iconRight: Expensicons.NewWindow,
+ link: () => Link.buildOldDotURL(CONST.OLDDOT_URLS.ADMIN_DOMAINS_URL),
+ },
+ ];
+
+ if (shouldShowSubscriptionsMenu) {
+ items.splice(1, 0, {
+ translationKey: 'allSettingsScreen.subscriptions',
+ icon: Expensicons.MoneyBag,
+ action: () => {
+ Link.openOldDotLink(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL);
+ },
+ shouldShowRightIcon: true,
+ iconRight: Expensicons.NewWindow,
+ link: () => Link.buildOldDotURL(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL),
+ });
+ }
+
+ return {
+ sectionStyle: styles.workspaceSettingsSectionContainer,
+ sectionTranslationKey: 'common.workspaces',
+ items,
+ };
+ }, [policies, policyMembers, styles.workspaceSettingsSectionContainer]);
/**
* Retuns a list of menu items data for general section
* @returns object with translationKey, style and items for the general section
*/
- const generalMenuItemsData: Menu = useMemo(
- () => ({
+ const generalMenuItemsData: Menu = useMemo(() => {
+ const signOutTranslationKey = Session.isSupportAuthToken() && Session.hasStashedSession() ? 'initialSettingsPage.restoreStashed' : 'initialSettingsPage.signOut';
+ const defaultMenu: Menu = {
sectionStyle: {
...styles.pt4,
},
- sectionTranslationKey: 'initialSettingsPage.general' as const,
+ sectionTranslationKey: 'initialSettingsPage.general',
items: [
{
- translationKey: 'initialSettingsPage.help' as const,
+ translationKey: 'initialSettingsPage.help',
icon: Expensicons.QuestionMark,
action: () => {
Link.openExternalLink(CONST.NEWHELP_URL);
@@ -199,14 +243,22 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
link: CONST.NEWHELP_URL,
},
{
- translationKey: 'initialSettingsPage.about' as const,
+ translationKey: 'initialSettingsPage.about',
icon: Expensicons.Info,
routeName: ROUTES.SETTINGS_ABOUT,
},
+ {
+ translationKey: signOutTranslationKey,
+ icon: Expensicons.Exit,
+ action: () => {
+ signOut(false);
+ },
+ },
],
- }),
- [styles.pt4],
- );
+ };
+
+ return defaultMenu;
+ }, [styles.pt4, signOut]);
/**
* Retuns JSX.Element with menu items
@@ -291,13 +343,14 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
const accountMenuItems = useMemo(() => getMenuItemsSection(accountMenuItemsData), [accountMenuItemsData, getMenuItemsSection]);
const generalMenuItems = useMemo(() => getMenuItemsSection(generalMenuItemsData), [generalMenuItemsData, getMenuItemsSection]);
+ const workspaceMenuItems = useMemo(() => getMenuItemsSection(workspaceMenuItemsData), [workspaceMenuItemsData, getMenuItemsSection]);
const currentUserDetails = currentUserPersonalDetails;
const avatarURL = currentUserDetails?.avatar ?? '';
const accountID = currentUserDetails?.accountID ?? '';
const headerContent = (
-
+
{isEmptyObject(currentUserPersonalDetails) || currentUserPersonalDetails.displayName === undefined ? (
) : (
@@ -384,17 +437,15 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
);
return (
- Navigation.closeFullScreen()}
- backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.ROOT].backgroundColor}
- childrenContainerStyles={[styles.m0, styles.p0]}
+
-
+
+ {headerContent}
{accountMenuItems}
+ {workspaceMenuItems}
{generalMenuItems}
signOut(true)}
onCancel={() => toggleSignoutConfirmModal(false)}
/>
-
-
+
+
);
}
@@ -433,5 +484,11 @@ export default withCurrentUserPersonalDetails(
session: {
key: ONYXKEYS.SESSION,
},
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+ policyMembers: {
+ key: ONYXKEYS.COLLECTION.POLICY_MEMBERS,
+ },
})(InitialSettingsPage),
);
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
index 88236e06f9a9..c28a0f6649e4 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
@@ -396,7 +396,6 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi
)}
navigateToWalletOrTransferBalancePage(source)}
onSelectPaymentMethod={(selectedPaymentMethod: string) => {
if (hasActivatedWallet || selectedPaymentMethod !== CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.tsx
similarity index 60%
rename from src/pages/tasks/NewTaskDescriptionPage.js
rename to src/pages/tasks/NewTaskDescriptionPage.tsx
index dbcb10d47f39..a5eb79497707 100644
--- a/src/pages/tasks/NewTaskDescriptionPage.js
+++ b/src/pages/tasks/NewTaskDescriptionPage.tsx
@@ -1,58 +1,50 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapperWithRef from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
+import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
-import * as Task from '@userActions/Task';
+import * as TaskActions from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/NewTaskForm';
+import type {Task} from '@src/types/onyx';
-const propTypes = {
- /** Grab the Share description of the Task */
- task: PropTypes.shape({
- /** Description of the Task */
- description: PropTypes.string,
- }),
-
- ...withLocalizePropTypes,
+type NewTaskDescriptionPageOnyxProps = {
+ /** Task Creation Data */
+ task: OnyxEntry;
};
-const defaultProps = {
- task: {
- description: '',
- },
-};
+type NewTaskDescriptionPageProps = NewTaskDescriptionPageOnyxProps & StackScreenProps;
const parser = new ExpensiMark();
-function NewTaskDescriptionPage(props) {
+function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
- const onSubmit = (values) => {
- Task.setDescriptionValue(values.taskDescription);
+ const onSubmit = (values: FormOnyxValues) => {
+ TaskActions.setDescriptionValue(values.taskDescription);
Navigation.goBack(ROUTES.NEW_TASK);
};
- /**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
- */
- function validate(values) {
+ const validate = (values: FormOnyxValues): FormInputErrors => {
const errors = {};
if (values.taskDescription.length > CONST.DESCRIPTION_LIMIT) {
@@ -60,7 +52,7 @@ function NewTaskDescriptionPage(props) {
}
return errors;
- }
+ };
return (
<>
Task.dismissModalAndClearOutTaskInfo()}
+ title={translate('task.description')}
+ onCloseButtonPress={() => TaskActions.dismissModalAndClearOutTaskInfo()}
onBackButtonPress={() => Navigation.goBack(ROUTES.NEW_TASK)}
/>
validate(values)}
- onSubmit={(values) => onSubmit(values)}
+ validate={validate}
+ onSubmit={onSubmit}
enabledWhenOffline
>
{
inputCallbackRef(el);
@@ -96,7 +88,7 @@ function NewTaskDescriptionPage(props) {
}}
autoGrowHeight
shouldSubmitForm
- containerStyles={[styles.autoGrowHeightMultilineInput]}
+ containerStyles={styles.autoGrowHeightMultilineInput}
/>
@@ -106,14 +98,9 @@ function NewTaskDescriptionPage(props) {
}
NewTaskDescriptionPage.displayName = 'NewTaskDescriptionPage';
-NewTaskDescriptionPage.propTypes = propTypes;
-NewTaskDescriptionPage.defaultProps = defaultProps;
-export default compose(
- withOnyx({
- task: {
- key: ONYXKEYS.TASK,
- },
- }),
- withLocalize,
-)(NewTaskDescriptionPage);
+export default withOnyx({
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+})(NewTaskDescriptionPage);
diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.tsx
similarity index 58%
rename from src/pages/tasks/NewTaskDetailsPage.js
rename to src/pages/tasks/NewTaskDetailsPage.tsx
index e4533b637dee..15612e20afd7 100644
--- a/src/pages/tasks/NewTaskDetailsPage.js
+++ b/src/pages/tasks/NewTaskDetailsPage.tsx
@@ -1,58 +1,52 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import PropTypes from 'prop-types';
import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as Task from '@userActions/Task';
+import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
+import * as TaskActions from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/NewTaskForm';
+import type {Task} from '@src/types/onyx';
-const propTypes = {
- /** Task title and description data */
- task: PropTypes.shape({
- title: PropTypes.string,
- description: PropTypes.string,
- }),
-
- ...withLocalizePropTypes,
+type NewTaskDetailsPageOnyxProps = {
+ /** Task Creation Data */
+ task: OnyxEntry;
};
-const defaultProps = {
- task: {},
-};
+type NewTaskDetailsPageProps = NewTaskDetailsPageOnyxProps & StackScreenProps;
const parser = new ExpensiMark();
-function NewTaskDetailsPage(props) {
+function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) {
const styles = useThemeStyles();
- const [taskTitle, setTaskTitle] = useState(props.task.title);
- const [taskDescription, setTaskDescription] = useState(props.task.description || '');
+ const {translate} = useLocalize();
+ const [taskTitle, setTaskTitle] = useState(task?.title ?? '');
+ const [taskDescription, setTaskDescription] = useState(task?.description ?? '');
const {inputCallbackRef} = useAutoFocusInput();
useEffect(() => {
- setTaskTitle(props.task.title);
- setTaskDescription(parser.htmlToMarkdown(parser.replace(props.task.description || '')));
- }, [props.task]);
+ setTaskTitle(task?.title ?? '');
+ setTaskDescription(parser.htmlToMarkdown(parser.replace(task?.description ?? '')));
+ }, [task]);
- /**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
- */
- function validate(values) {
+ const validate = (values: FormOnyxValues): FormInputErrors => {
const errors = {};
if (!values.taskTitle) {
@@ -66,14 +60,14 @@ function NewTaskDetailsPage(props) {
}
return errors;
- }
+ };
// On submit, we want to call the assignTask function and wait to validate
// the response
- function onSubmit(values) {
- Task.setDetailsValue(values.taskTitle, values.taskDescription);
+ const onSubmit = (values: FormOnyxValues) => {
+ TaskActions.setDetailsValue(values.taskTitle, values.taskDescription);
Navigation.navigate(ROUTES.NEW_TASK);
- }
+ };
return (
Task.dismissModalAndClearOutTaskInfo()}
+ title={translate('newTaskPage.assignTask')}
+ onCloseButtonPress={() => TaskActions.dismissModalAndClearOutTaskInfo()}
shouldShowBackButton
- onBackButtonPress={() => Task.dismissModalAndClearOutTaskInfo()}
+ onBackButtonPress={() => TaskActions.dismissModalAndClearOutTaskInfo()}
/>
validate(values)}
- onSubmit={(values) => onSubmit(values)}
+ validate={validate}
+ onSubmit={onSubmit}
enabledWhenOffline
>
setTaskTitle(value)}
+ onValueChange={setTaskTitle}
autoCorrect={false}
/>
setTaskDescription(value)}
+ onValueChange={setTaskDescription}
/>
@@ -129,14 +125,9 @@ function NewTaskDetailsPage(props) {
}
NewTaskDetailsPage.displayName = 'NewTaskDetailsPage';
-NewTaskDetailsPage.propTypes = propTypes;
-NewTaskDetailsPage.defaultProps = defaultProps;
-export default compose(
- withOnyx({
- task: {
- key: ONYXKEYS.TASK,
- },
- }),
- withLocalize,
-)(NewTaskDetailsPage);
+export default withOnyx({
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+})(NewTaskDetailsPage);
diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js
deleted file mode 100644
index 352c08115114..000000000000
--- a/src/pages/tasks/NewTaskPage.js
+++ /dev/null
@@ -1,231 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useEffect, useMemo, useState} from 'react';
-import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import MenuItem from '@components/MenuItem';
-import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import ScreenWrapper from '@components/ScreenWrapper';
-import ScrollView from '@components/ScrollView';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
-import Navigation from '@libs/Navigation/Navigation';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import playSound, {SOUNDS} from '@libs/Sound';
-import reportPropTypes from '@pages/reportPropTypes';
-import * as Task from '@userActions/Task';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-
-const propTypes = {
- /** Task Creation Data */
- task: PropTypes.shape({
- assignee: PropTypes.string,
- shareDestination: PropTypes.string,
- title: PropTypes.string,
- description: PropTypes.string,
- parentReportID: PropTypes.string,
- }),
-
- /** All of the personal details for everyone */
- personalDetails: PropTypes.objectOf(
- PropTypes.shape({
- /** Display name of the person */
- displayName: PropTypes.string,
-
- /** Avatar URL of the person */
- avatar: PropTypes.string,
-
- /** Login of the person */
- login: PropTypes.string,
- }),
- ),
-
- /** All reports shared with the user */
- reports: PropTypes.objectOf(reportPropTypes),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- task: {},
- personalDetails: {},
- reports: {},
-};
-
-function NewTaskPage(props) {
- const styles = useThemeStyles();
- const [assignee, setAssignee] = useState({});
- const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs([props.task.assigneeAccountID], props.personalDetails), false);
- const [shareDestination, setShareDestination] = useState({});
- const [title, setTitle] = useState('');
- const [description, setDescription] = useState('');
- const [errorMessage, setErrorMessage] = useState('');
- const [parentReport, setParentReport] = useState({});
-
- const isAllowedToCreateTask = useMemo(() => _.isEmpty(parentReport) || ReportUtils.isAllowedToComment(parentReport), [parentReport]);
-
- useEffect(() => {
- setErrorMessage('');
-
- // If we have an assignee, we want to set the assignee data
- // If there's an issue with the assignee chosen, we want to notify the user
- if (props.task.assignee) {
- const displayDetails = Task.getAssignee(props.task.assigneeAccountID, props.personalDetails);
- setAssignee(displayDetails);
- }
-
- // We only set the parentReportID if we are creating a task from a report
- // this allows us to go ahead and set that report as the share destination
- // and disable the share destination selector
- if (props.task.parentReportID) {
- Task.setShareDestinationValue(props.task.parentReportID);
- }
-
- // If we have a share destination, we want to set the parent report and
- // the share destination data
- if (props.task.shareDestination) {
- setParentReport(lodashGet(props.reports, `report_${props.task.shareDestination}`, {}));
- const displayDetails = Task.getShareDestination(props.task.shareDestination, props.reports, props.personalDetails);
- setShareDestination(displayDetails);
- }
-
- // If we have a title, we want to set the title
- if (!_.isUndefined(props.task.title)) {
- setTitle(props.task.title);
- }
-
- // If we have a description, we want to set the description
- if (!_.isUndefined(props.task.description)) {
- setDescription(props.task.description);
- }
- }, [props]);
-
- // On submit, we want to call the createTask function and wait to validate
- // the response
- function onSubmit() {
- if (!props.task.title && !props.task.shareDestination) {
- setErrorMessage('newTaskPage.confirmError');
- return;
- }
-
- if (!props.task.title) {
- setErrorMessage('newTaskPage.pleaseEnterTaskName');
- return;
- }
-
- if (!props.task.shareDestination) {
- setErrorMessage('newTaskPage.pleaseEnterTaskDestination');
- return;
- }
-
- playSound(SOUNDS.DONE);
- Task.createTaskAndNavigate(
- parentReport.reportID,
- props.task.title,
- props.task.description,
- props.task.assignee,
- props.task.assigneeAccountID,
- props.task.assigneeChatReport,
- parentReport.policyID,
- );
- }
-
- return (
-
- Task.dismissModalAndClearOutTaskInfo()}
- shouldShowLink={false}
- >
- Task.dismissModalAndClearOutTaskInfo()}
- shouldShowBackButton
- onBackButtonPress={() => {
- Navigation.goBack(ROUTES.NEW_TASK_DETAILS);
- }}
- />
-
-
-
- Navigation.navigate(ROUTES.NEW_TASK_TITLE)}
- shouldShowRightIcon
- />
- Navigation.navigate(ROUTES.NEW_TASK_DESCRIPTION)}
- shouldShowRightIcon
- shouldParseTitle
- numberOfLinesTitle={2}
- titleStyle={styles.flex1}
- />
-
-
-
- onSubmit()}
- enabledWhenOffline
- buttonText={props.translate('newTaskPage.confirmTask')}
- containerStyles={[styles.mh0, styles.mt5, styles.flex1, styles.ph5]}
- />
-
-
-
-
- );
-}
-
-NewTaskPage.displayName = 'NewTaskPage';
-NewTaskPage.propTypes = propTypes;
-NewTaskPage.defaultProps = defaultProps;
-
-export default compose(
- withOnyx({
- task: {
- key: ONYXKEYS.TASK,
- },
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- }),
- withLocalize,
-)(NewTaskPage);
diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx
new file mode 100644
index 000000000000..64c46e75c91d
--- /dev/null
+++ b/src/pages/tasks/NewTaskPage.tsx
@@ -0,0 +1,215 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useEffect, useMemo, useState} from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import MenuItem from '@components/MenuItem';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
+import Navigation from '@libs/Navigation/Navigation';
+import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import playSound, {SOUNDS} from '@libs/Sound';
+import * as TaskActions from '@userActions/Task';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {PersonalDetailsList, Report, Task} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type NewTaskPageOnyxProps = {
+ /** Task Creation Data */
+ task: OnyxEntry;
+
+ /** All of the personal details for everyone */
+ personalDetails: OnyxEntry;
+
+ /** All reports shared with the user */
+ reports: OnyxCollection;
+};
+
+type NewTaskPageProps = NewTaskPageOnyxProps & StackScreenProps;
+
+function NewTaskPage({task, reports, personalDetails}: NewTaskPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [assignee, setAssignee] = useState();
+ const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(
+ OptionsListUtils.getPersonalDetailsForAccountIDs(task?.assigneeAccountID ? [task.assigneeAccountID] : [], personalDetails),
+ false,
+ );
+ const [shareDestination, setShareDestination] = useState();
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [errorMessage, setErrorMessage] = useState('');
+ const [parentReport, setParentReport] = useState>(null);
+
+ const isAllowedToCreateTask = useMemo(() => isEmptyObject(parentReport) || ReportUtils.isAllowedToComment(parentReport), [parentReport]);
+
+ useEffect(() => {
+ setErrorMessage('');
+
+ // If we have an assignee, we want to set the assignee data
+ // If there's an issue with the assignee chosen, we want to notify the user
+ if (task?.assignee) {
+ const displayDetails = TaskActions.getAssignee(task?.assigneeAccountID ?? -1, personalDetails);
+ setAssignee(displayDetails);
+ }
+
+ // We only set the parentReportID if we are creating a task from a report
+ // this allows us to go ahead and set that report as the share destination
+ // and disable the share destination selector
+ if (task?.parentReportID) {
+ TaskActions.setShareDestinationValue(task.parentReportID);
+ }
+
+ // If we have a share destination, we want to set the parent report and
+ // the share destination data
+ if (task?.shareDestination) {
+ setParentReport(reports?.[`report_${task.shareDestination}`] ?? null);
+ const displayDetails = TaskActions.getShareDestination(task.shareDestination, reports, personalDetails);
+ setShareDestination(displayDetails);
+ }
+
+ // If we have a title, we want to set the title
+ if (task?.title !== undefined) {
+ setTitle(task.title);
+ }
+
+ // If we have a description, we want to set the description
+ if (task?.description !== undefined) {
+ setDescription(task.description);
+ }
+ }, [personalDetails, reports, task?.assignee, task?.assigneeAccountID, task?.description, task?.parentReportID, task?.shareDestination, task?.title]);
+
+ // On submit, we want to call the createTask function and wait to validate
+ // the response
+ const onSubmit = () => {
+ if (!task?.title && !task?.shareDestination) {
+ setErrorMessage('newTaskPage.confirmError');
+ return;
+ }
+
+ if (!task.title) {
+ setErrorMessage('newTaskPage.pleaseEnterTaskName');
+ return;
+ }
+
+ if (!task.shareDestination) {
+ setErrorMessage('newTaskPage.pleaseEnterTaskDestination');
+ return;
+ }
+
+ playSound(SOUNDS.DONE);
+ TaskActions.createTaskAndNavigate(
+ parentReport?.reportID ?? '',
+ task.title,
+ task?.description ?? '',
+ task?.assignee ?? '',
+ task.assigneeAccountID,
+ task.assigneeChatReport,
+ parentReport?.policyID,
+ );
+ };
+
+ return (
+
+ TaskActions.dismissModalAndClearOutTaskInfo()}
+ shouldShowLink={false}
+ >
+ TaskActions.dismissModalAndClearOutTaskInfo()}
+ shouldShowBackButton
+ onBackButtonPress={() => {
+ Navigation.goBack(ROUTES.NEW_TASK_DETAILS);
+ }}
+ />
+
+
+
+ Navigation.navigate(ROUTES.NEW_TASK_TITLE)}
+ shouldShowRightIcon
+ />
+ Navigation.navigate(ROUTES.NEW_TASK_DESCRIPTION)}
+ shouldShowRightIcon
+ shouldParseTitle
+ numberOfLinesTitle={2}
+ titleStyle={styles.flex1}
+ />
+ Navigation.navigate(ROUTES.NEW_TASK_ASSIGNEE)}
+ shouldShowRightIcon
+ titleWithTooltips={assigneeTooltipDetails}
+ />
+ Navigation.navigate(ROUTES.NEW_TASK_SHARE_DESTINATION)}
+ interactive={!task?.parentReportID}
+ shouldShowRightIcon={!task?.parentReportID}
+ titleWithTooltips={shareDestination?.shouldUseFullTitleToDisplay ? shareDestination?.displayNamesWithTooltips : []}
+ />
+
+
+
+
+
+
+
+
+ );
+}
+
+NewTaskPage.displayName = 'NewTaskPage';
+
+export default withOnyx({
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+})(NewTaskPage);
diff --git a/src/pages/tasks/NewTaskTitlePage.js b/src/pages/tasks/NewTaskTitlePage.tsx
similarity index 60%
rename from src/pages/tasks/NewTaskTitlePage.js
rename to src/pages/tasks/NewTaskTitlePage.tsx
index 5b1f180c97d6..582d2a5c6500 100644
--- a/src/pages/tasks/NewTaskTitlePage.js
+++ b/src/pages/tasks/NewTaskTitlePage.tsx
@@ -1,49 +1,41 @@
-import PropTypes from 'prop-types';
+import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapperWithRef from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as Task from '@userActions/Task';
+import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
+import * as TaskActions from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/NewTaskForm';
+import type {Task} from '@src/types/onyx';
-const propTypes = {
- /** Grab the Share title of the Task */
- task: PropTypes.shape({
- /** Title of the Task */
- title: PropTypes.string,
- }),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- task: {
- title: '',
- },
+type NewTaskTitlePageOnyxProps = {
+ /** Task Creation Data */
+ task: OnyxEntry;
};
+type NewTaskTitlePageProps = NewTaskTitlePageOnyxProps & StackScreenProps;
-function NewTaskTitlePage(props) {
+function NewTaskTitlePage({task}: NewTaskTitlePageProps) {
const styles = useThemeStyles();
const {inputCallbackRef} = useAutoFocusInput();
- /**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
- */
- function validate(values) {
+ const {translate} = useLocalize();
+
+ const validate = (values: FormOnyxValues): FormInputErrors => {
const errors = {};
if (!values.taskTitle) {
@@ -54,14 +46,14 @@ function NewTaskTitlePage(props) {
}
return errors;
- }
+ };
// On submit, we want to call the assignTask function and wait to validate
// the response
- function onSubmit(values) {
- Task.setTitleValue(values.taskTitle);
+ const onSubmit = (values: FormOnyxValues) => {
+ TaskActions.setTitleValue(values.taskTitle);
Navigation.goBack(ROUTES.NEW_TASK);
- }
+ };
return (
Task.dismissModalAndClearOutTaskInfo()}
+ title={translate('task.title')}
+ onCloseButtonPress={() => TaskActions.dismissModalAndClearOutTaskInfo()}
shouldShowBackButton
onBackButtonPress={() => Navigation.goBack(ROUTES.NEW_TASK)}
/>
validate(values)}
- onSubmit={(values) => onSubmit(values)}
+ validate={validate}
+ onSubmit={onSubmit}
enabledWhenOffline
>
@@ -100,14 +92,9 @@ function NewTaskTitlePage(props) {
}
NewTaskTitlePage.displayName = 'NewTaskTitlePage';
-NewTaskTitlePage.propTypes = propTypes;
-NewTaskTitlePage.defaultProps = defaultProps;
-export default compose(
- withOnyx({
- task: {
- key: ONYXKEYS.TASK,
- },
- }),
- withLocalize,
-)(NewTaskTitlePage);
+export default withOnyx({
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+})(NewTaskTitlePage);
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 240a148110f7..38828a0406ef 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -1,26 +1,28 @@
+import {useNavigationState} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import Breadcrumbs from '@components/Breadcrumbs';
import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
-import useActiveRoute from '@hooks/useActiveRoute';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
+import getTopmostWorkspacesCentralPaneName from '@libs/Navigation/getTopmostWorkspacesCentralPaneName';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
-import type {BottomTabNavigatorParamList} from '@navigation/types';
+import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
+import type {FullScreenNavigatorParamList} from '@navigation/types';
import * as App from '@userActions/App';
import * as Policy from '@userActions/Policy';
import * as ReimbursementAccount from '@userActions/ReimbursementAccount';
@@ -48,7 +50,7 @@ type WorkspaceInitialPageOnyxProps = {
reimbursementAccount: OnyxEntry;
};
-type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInitialPageOnyxProps & StackScreenProps;
+type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInitialPageOnyxProps & StackScreenProps;
function dismissError(policyID: string) {
PolicyUtils.goBackFromInvalidPolicy();
@@ -62,8 +64,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors);
const waitForNavigate = useWaitForNavigation();
const {singleExecution, isExecuting} = useSingleExecution();
- const activeRoute = useActiveRoute();
-
+ const activeRoute = useNavigationState(getTopmostWorkspacesCentralPaneName);
const {translate} = useLocalize();
const policyID = policy?.id ?? '';
@@ -160,6 +161,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
icon: Expensicons.Workflows,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)))),
routeName: SCREENS.WORKSPACE.WORKFLOWS,
+ brickRoadIndicator: !isEmptyObject(policy?.errorFields?.reimburserEmail ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
});
}
@@ -215,31 +217,38 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
// We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace
(PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy));
+ const policyAvatar = useMemo(() => {
+ if (!policy) {
+ return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR};
+ }
+
+ const avatar = policy?.avatar ? policy.avatar : getDefaultWorkspaceAvatar(policy?.name);
+ return {
+ source: avatar,
+ name: policy?.name ?? '',
+ type: CONST.ICON_TYPE_WORKSPACE,
+ };
+ }, [policy]);
+
return (
-
+
-
+
{/*
Ideally we should use MenuList component for MenuItems with singleExecution/Navigation actions.
In this case where user can click on workspace avatar or menu items, we need to have a check for `isExecuting`. So, we are directly mapping menuItems.
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index a574f64cf774..20be7913e31e 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -32,7 +32,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
-import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
@@ -62,7 +62,7 @@ type WorkspaceMembersPageOnyxProps = {
type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps &
WithCurrentUserPersonalDetailsProps &
WorkspaceMembersPageOnyxProps &
- StackScreenProps;
+ StackScreenProps;
/**
* Inverts an object, equivalent of _.invert
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index 45a950e0fafb..000ba0db7bc7 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -9,7 +9,7 @@ import Section from '@components/Section';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import * as Policy from '@userActions/Policy';
import type {TranslationPaths} from '@src/languages/types';
import type SCREENS from '@src/SCREENS';
@@ -21,7 +21,7 @@ import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscree
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
import ToggleSettingOptionRow from './workflows/ToggleSettingsOptionRow';
-type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
type Item = {
icon: IconAsset;
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index 32b76ef46202..4904a4f35193 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -19,7 +19,6 @@ import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimburs
import * as BankAccounts from '@userActions/BankAccounts';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
-import ROUTES from '@src/ROUTES';
import type {Policy, ReimbursementAccount, User} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
@@ -81,6 +80,9 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
* taller header on desktop and different font of the title.
* */
icon?: IconAsset;
+
+ /** Whether the page is loading, example any other API call in progres */
+ isLoading?: boolean;
};
function fetchData(policyID: string, skipVBBACal?: boolean) {
@@ -110,12 +112,14 @@ function WorkspacePageWithSections({
shouldShowOfflineIndicatorInWideScreen = false,
shouldShowNonAdmin = false,
shouldShowNotFoundPage = false,
+ isLoading: isPageLoading = false,
}: WorkspacePageWithSectionsProps) {
const styles = useThemeStyles();
const policyID = route.params?.policyID ?? '';
useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)});
- const isLoading = reimbursementAccount?.isLoading ?? true;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true;
const achState = reimbursementAccount?.achData?.state ?? '';
const isUsingECard = user?.isUsingExpensifyCard ?? false;
const hasVBA = achState === BankAccount.STATE.OPEN;
@@ -152,8 +156,8 @@ function WorkspacePageWithSections({
shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow}
>
Navigation.goBack(backButtonRoute ?? ROUTES.WORKSPACE_INITIAL.getRoute(policyID))}
+ onBackButtonPress={() => Navigation.goBack(backButtonRoute)}
icon={icon ?? undefined}
+ style={styles.headerBarDesktopHeight}
/>
{(isLoading || firstRender.current) && shouldShowLoading && isFocused ? (
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index 9d90557b1d37..ddebc9d4b368 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -102,7 +102,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
>
{(hasVBA?: boolean) => (
-
+
App.createWorkspaceWithPolicyDraftAndNavigateToIt()}
+ onPress={() => interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt())}
/>
@@ -366,7 +367,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
subtitle={translate('workspace.emptyWorkspace.subtitle')}
ctaText={translate('workspace.new.newWorkspace')}
ctaAccessibilityLabel={translate('workspace.new.newWorkspace')}
- onCtaPress={() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()}
+ onCtaPress={() => interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt())}
illustration={LottieAnimations.WorkspacePlanet}
// We use this style to vertically center the illustration, as the original illustration is not centered
illustrationStyle={styles.emptyWorkspaceIllustrationStyle}
@@ -394,7 +395,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
success
medium
text={translate('workspace.new.newWorkspace')}
- onPress={() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()}
+ onPress={() => interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt())}
/>
;
+type WorkspaceBillsPageProps = StackScreenProps;
function WorkspaceBillsPage({route}: WorkspaceBillsPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/card/WorkspaceCardPage.tsx b/src/pages/workspace/card/WorkspaceCardPage.tsx
index 710ef3735026..079c715bffd6 100644
--- a/src/pages/workspace/card/WorkspaceCardPage.tsx
+++ b/src/pages/workspace/card/WorkspaceCardPage.tsx
@@ -4,7 +4,7 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
import type SCREENS from '@src/SCREENS';
@@ -12,7 +12,7 @@ import WorkspaceCardNoVBAView from './WorkspaceCardNoVBAView';
import WorkspaceCardVBANoECardView from './WorkspaceCardVBANoECardView';
import WorkspaceCardVBAWithECardView from './WorkspaceCardVBAWithECardView';
-type WorkspaceCardPageProps = StackScreenProps;
+type WorkspaceCardPageProps = StackScreenProps;
function WorkspaceCardPage({route}: WorkspaceCardPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx
index cfe28ba292b0..8de0e3a07980 100644
--- a/src/pages/workspace/categories/CreateCategoryPage.tsx
+++ b/src/pages/workspace/categories/CreateCategoryPage.tsx
@@ -9,6 +9,7 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -34,6 +35,7 @@ type CreateCategoryPageProps = WorkspaceCreateCategoryPageOnyxProps & StackScree
function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
const validate = useCallback(
(values: FormOnyxValues) => {
@@ -72,6 +74,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps)
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CreateCategoryPage.displayName}
+ shouldEnableMaxHeight
>
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index 17afefd1cea4..3f2ef8ce6aa6 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -21,7 +21,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import localeCompare from '@libs/LocaleCompare';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
@@ -47,7 +47,7 @@ type WorkspaceCategoriesOnyxProps = {
policyCategories: OnyxEntry;
};
-type WorkspaceCategoriesPageProps = WorkspaceCategoriesOnyxProps & StackScreenProps;
+type WorkspaceCategoriesPageProps = WorkspaceCategoriesOnyxProps & StackScreenProps;
function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCategoriesPageProps) {
const {isSmallScreenWidth} = useWindowDimensions();
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
index fd6466da1758..ca508791c028 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
@@ -20,7 +20,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
@@ -44,7 +44,7 @@ type PolicyDistanceRatesPageOnyxProps = {
policy: OnyxEntry;
};
-type PolicyDistanceRatesPageProps = PolicyDistanceRatesPageOnyxProps & StackScreenProps;
+type PolicyDistanceRatesPageProps = PolicyDistanceRatesPageOnyxProps & StackScreenProps;
function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) {
const {isSmallScreenWidth} = useWindowDimensions();
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
index 96aa350496b5..a00c4959cedb 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
@@ -4,14 +4,14 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
import type SCREENS from '@src/SCREENS';
import WorkspaceInvoicesNoVBAView from './WorkspaceInvoicesNoVBAView';
import WorkspaceInvoicesVBAView from './WorkspaceInvoicesVBAView';
-type WorkspaceInvoicesPageProps = StackScreenProps;
+type WorkspaceInvoicesPageProps = StackScreenProps;
function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
new file mode 100644
index 000000000000..2df7621c17d3
--- /dev/null
+++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
@@ -0,0 +1,113 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import {Keyboard} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/WorkspaceTagCreateForm';
+import type {PolicyTagList} from '@src/types/onyx';
+
+type WorkspaceCreateTagPageOnyxProps = {
+ /** All policy tags */
+ policyTags: OnyxEntry;
+};
+
+type CreateTagPageProps = WorkspaceCreateTagPageOnyxProps & StackScreenProps;
+
+function CreateTagPage({route, policyTags}: CreateTagPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+ const tagName = values.tagName.trim();
+ const {tags} = PolicyUtils.getTagList(policyTags, 0);
+
+ if (!ValidationUtils.isRequiredFulfilled(tagName)) {
+ errors.tagName = 'workspace.tags.tagRequiredError';
+ } else if (tags?.[tagName]) {
+ errors.tagName = 'workspace.tags.existingTagError';
+ } else if ([...tagName].length > CONST.TAG_NAME_LIMIT) {
+ // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units.
+ ErrorUtils.addErrorMessage(errors, 'tagName', ['common.error.characterLimitExceedCounter', {length: [...tagName].length, limit: CONST.TAG_NAME_LIMIT}]);
+ }
+
+ return errors;
+ },
+ [policyTags],
+ );
+
+ const createTag = useCallback(
+ (values: FormOnyxValues) => {
+ Policy.createPolicyTag(route.params.policyID, values.tagName.trim());
+ Keyboard.dismiss();
+ Navigation.goBack();
+ },
+ [route.params.policyID],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+CreateTagPage.displayName = 'CreateTagPage';
+
+export default withOnyx({
+ policyTags: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route?.params?.policyID}`,
+ },
+})(CreateTagPage);
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
index cc809892d45e..4ea8ba669b9b 100644
--- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -20,7 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
@@ -43,7 +43,7 @@ type WorkspaceTagsOnyxProps = {
policyTags: OnyxEntry;
};
-type WorkspaceTagsPageProps = WorkspaceTagsOnyxProps & StackScreenProps;
+type WorkspaceTagsPageProps = WorkspaceTagsOnyxProps & StackScreenProps;
function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
const {isSmallScreenWidth} = useWindowDimensions();
@@ -115,18 +115,32 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
Navigation.navigate(ROUTES.WORKSPACE_TAGS_SETTINGS.getRoute(route.params.policyID));
};
+ const navigateToCreateTagPage = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_TAG_CREATE.getRoute(route.params.policyID));
+ };
+
const isLoading = !isOffline && policyTags === undefined;
- const settingsButton = (
+ const headerButtons = (
+ {policyTags && (
+
+ )}
);
@@ -144,9 +158,9 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
title={translate('workspace.common.tags')}
shouldShowBackButton={isSmallScreenWidth}
>
- {!isSmallScreenWidth && policyTags && settingsButton}
+ {!isSmallScreenWidth && headerButtons}
- {isSmallScreenWidth && policyTags && {settingsButton}}
+ {isSmallScreenWidth && {headerButtons}}
{translate('workspace.tags.subtitle')}
diff --git a/src/pages/workspace/travel/WorkspaceTravelPage.tsx b/src/pages/workspace/travel/WorkspaceTravelPage.tsx
index 88dfe5254fcf..c03bcc7cfb9b 100644
--- a/src/pages/workspace/travel/WorkspaceTravelPage.tsx
+++ b/src/pages/workspace/travel/WorkspaceTravelPage.tsx
@@ -4,14 +4,14 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
import type SCREENS from '@src/SCREENS';
import WorkspaceTravelNoVBAView from './WorkspaceTravelNoVBAView';
import WorkspaceTravelVBAView from './WorkspaceTravelVBAView';
-type WorkspaceTravelPageProps = StackScreenProps;
+type WorkspaceTravelPageProps = StackScreenProps;
function WorkspaceTravelPage({route}: WorkspaceTravelPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx
index 76126040652b..16af069fa0a2 100644
--- a/src/pages/workspace/withPolicy.tsx
+++ b/src/pages/workspace/withPolicy.tsx
@@ -8,7 +8,7 @@ import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import taxPropTypes from '@components/taxPropTypes';
import {translatableTextPropTypes} from '@libs/Localize';
-import type {BottomTabNavigatorParamList, CentralPaneNavigatorParamList, SettingsNavigatorParamList} from '@navigation/types';
+import type {CentralPaneNavigatorParamList, FullScreenNavigatorParamList, SettingsNavigatorParamList, WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import policyMemberPropType from '@pages/policyMemberPropType';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
@@ -16,7 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
-type WorkspaceParamList = BottomTabNavigatorParamList & CentralPaneNavigatorParamList & SettingsNavigatorParamList;
+type WorkspaceParamList = WorkspacesCentralPaneNavigatorParamList & FullScreenNavigatorParamList & CentralPaneNavigatorParamList & SettingsNavigatorParamList;
type PolicyRoute = RouteProp>;
function getPolicyIDFromRoute(route: PolicyRoute): string {
@@ -148,5 +148,5 @@ export default function (WrappedComponent:
})(forwardRef(WithPolicy));
}
-export {policyPropTypes, policyDefaultProps};
-export type {WithPolicyOnyxProps, WithPolicyProps, PolicyRoute};
+export {policyDefaultProps, policyPropTypes};
+export type {PolicyRoute, WithPolicyOnyxProps, WithPolicyProps};
diff --git a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
index c5c4465937b9..da995de1d5d5 100644
--- a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
+++ b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
@@ -5,7 +5,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Switch from '@components/Switch';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
-import type {PendingAction} from '@src/types/onyx/OnyxCommon';
+import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
import type IconAsset from '@src/types/utils/IconAsset';
type ToggleSettingOptionRowProps = {
@@ -23,14 +23,23 @@ type ToggleSettingOptionRowProps = {
subMenuItems?: React.ReactNode;
/** If there is a pending action, we will grey out the option */
pendingAction?: PendingAction;
+ /** Any error message to show */
+ errors?: Errors;
+ /** Callback to close the error messages */
+ onCloseError?: () => void;
};
const ICON_SIZE = 48;
-function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction}: ToggleSettingOptionRowProps) {
+function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction, errors, onCloseError}: ToggleSettingOptionRowProps) {
const styles = useThemeStyles();
return (
-
+
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index ee3934cacc06..c6ace2b0856e 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -1,31 +1,34 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useCallback, useMemo} from 'react';
+import React, {useCallback, useEffect, useMemo} from 'react';
import {FlatList, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import BankAccount from '@libs/models/BankAccount';
import Navigation from '@libs/Navigation/Navigation';
import Permissions from '@libs/Permissions';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import type {WithPolicyProps} from '@pages/workspace/withPolicy';
import withPolicy from '@pages/workspace/withPolicy';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import * as Policy from '@userActions/Policy';
+import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-import type {Beta} from '@src/types/onyx';
+import type {Beta, ReimbursementAccount, Session} from '@src/types/onyx';
import ToggleSettingOptionRow from './ToggleSettingsOptionRow';
import type {ToggleSettingOptionRowProps} from './ToggleSettingsOptionRow';
import {getAutoReportingFrequencyDisplayNames} from './WorkspaceAutoReportingFrequencyPage';
@@ -34,25 +37,49 @@ import type {AutoReportingFrequencyKey} from './WorkspaceAutoReportingFrequencyP
type WorkspaceWorkflowsPageOnyxProps = {
/** Beta features list */
betas: OnyxEntry;
+ /** Reimbursement account details */
+ reimbursementAccount: OnyxEntry;
+ /** Policy details */
+ session: OnyxEntry;
};
-type WorkspaceWorkflowsPageProps = WithPolicyProps & WorkspaceWorkflowsPageOnyxProps & StackScreenProps;
+type WorkspaceWorkflowsPageProps = WithPolicyProps & WorkspaceWorkflowsPageOnyxProps & StackScreenProps;
-function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPageProps) {
+function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, session}: WorkspaceWorkflowsPageProps) {
const {translate, preferredLocale} = useLocalize();
const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
const {isSmallScreenWidth} = useWindowDimensions();
- const {isOffline} = useNetwork();
const policyApproverEmail = policy?.approver;
const policyApproverName = useMemo(() => PersonalDetailsUtils.getPersonalDetailByEmail(policyApproverEmail ?? '')?.displayName ?? policyApproverEmail, [policyApproverEmail]);
const containerStyle = useMemo(() => [styles.ph8, styles.mhn8, styles.ml11, styles.pv3, styles.pr0, styles.pl4, styles.mr0, styles.widthAuto, styles.mt4], [styles]);
const canUseDelayedSubmission = Permissions.canUseWorkflowsDelayedSubmission(betas);
+ const displayNameForAuthorizedPayer = useMemo(() => {
+ const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([policy?.reimburserAccountID ?? 0], session?.accountID ?? 0);
+ const displayNameFromReimburserEmail = PersonalDetailsUtils.getPersonalDetailByEmail(policy?.reimburserEmail ?? '')?.displayName ?? policy?.reimburserEmail;
+ return displayNameFromReimburserEmail ?? personalDetails?.[0]?.displayName;
+ }, [policy?.reimburserAccountID, policy?.reimburserEmail, session?.accountID]);
+
const onPressAutoReportingFrequency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY.getRoute(policy?.id ?? '')), [policy?.id]);
- const items: ToggleSettingOptionRowProps[] = useMemo(
- () => [
+ const fetchData = () => {
+ Policy.openPolicyWorkflowsPage(policy?.id ?? route.params.policyID);
+ };
+
+ useNetwork({onReconnect: fetchData});
+
+ useEffect(() => {
+ fetchData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const optionItems: ToggleSettingOptionRowProps[] = useMemo(() => {
+ const {accountNumber, state, bankName} = reimbursementAccount?.achData ?? {};
+ const hasVBA = state === BankAccount.STATE.OPEN;
+ const bankDisplayName = bankName ? `${bankName} ${accountNumber ? `${accountNumber.slice(-5)}` : ''}` : '';
+ const hasReimburserEmailError = !!policy?.errorFields?.reimburserEmail;
+
+ return [
...(canUseDelayedSubmission
? [
{
@@ -115,41 +142,71 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
title: translate('workflowsPage.makeOrTrackPaymentsTitle'),
subtitle: translate('workflowsPage.makeOrTrackPaymentsDescription'),
onToggle: () => {
- // TODO will be done in https://github.com/Expensify/Expensify/issues/368335
+ const isActive = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES;
+ const newReimbursementChoice = isActive ? CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL : CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES;
+ const newReimburserAccountID =
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ PersonalDetailsUtils.getPersonalDetailByEmail(policy?.reimburserEmail ?? '')?.accountID || policy?.reimburserAccountID || policy?.ownerAccountID;
+ const newReimburserEmail = PersonalDetailsUtils.getPersonalDetailsByIDs([newReimburserAccountID ?? 0], session?.accountID ?? 0)?.[0]?.login;
+ Policy.setWorkspaceReimbursement(policy?.id ?? '', newReimbursementChoice, newReimburserAccountID ?? 0, newReimburserEmail ?? '');
},
subMenuItems: (
- Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_CONNECT_BANK_ACCOUNT.getRoute(route.params.policyID))}
- // TODO will be done in https://github.com/Expensify/Expensify/issues/368335
- shouldShowRightIcon
- wrapperStyle={containerStyle}
- hoverAndPressStyle={[styles.mr0, styles.br2]}
- />
+ <>
+ navigateToBankAccountRoute(route.params.policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID))}
+ shouldShowRightIcon
+ wrapperStyle={containerStyle}
+ hoverAndPressStyle={[styles.mr0, styles.br2]}
+ />
+ {hasVBA && (
+ Policy.clearWorkspacePayerError(policy?.id ?? '')}
+ errorRowStyles={[styles.ml7]}
+ >
+ Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_PAYER.getRoute(route.params.policyID))}
+ shouldShowRightIcon
+ wrapperStyle={containerStyle}
+ hoverAndPressStyle={[styles.mr0, styles.br2]}
+ brickRoadIndicator={hasReimburserEmailError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ />
+
+ )}
+ >
),
isEndOptionRow: true,
- isActive: false, // TODO will be done in https://github.com/Expensify/Expensify/issues/368335
+ isActive: policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES,
+ pendingAction: policy?.pendingFields?.reimbursementChoice,
+ errors: ErrorUtils.getLatestErrorField(policy ?? {}, 'reimbursementChoice'),
+ onCloseError: () => Policy.clearWorkspaceReimbursementErrors(policy?.id ?? ''),
},
- ],
- [
- policyApproverName,
- policy,
- route.params.policyID,
- styles,
- translate,
- containerStyle,
- isOffline,
- StyleUtils,
- onPressAutoReportingFrequency,
- preferredLocale,
- canUseDelayedSubmission,
- ],
- );
+ ];
+ }, [
+ policy,
+ route.params.policyID,
+ styles,
+ translate,
+ policyApproverName,
+ containerStyle,
+ onPressAutoReportingFrequency,
+ preferredLocale,
+ canUseDelayedSubmission,
+ reimbursementAccount?.achData,
+ displayNameForAuthorizedPayer,
+ session?.accountID,
+ ]);
- const renderItem = ({item}: {item: ToggleSettingOptionRowProps}) => (
+ const renderOptionItem = ({item}: {item: ToggleSettingOptionRowProps}) => (
);
const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy);
const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy);
+ const isLoading = reimbursementAccount?.isLoading ?? true;
return (
{translate('workflowsPage.workflowDescription')}
item.title}
/>
@@ -202,5 +264,12 @@ export default withPolicy(
betas: {
key: ONYXKEYS.BETAS,
},
+ reimbursementAccount: {
+ // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
+ key: ({route}) => `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${route.params.policyID}`,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
})(WorkspaceWorkflowsPage),
);
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx
new file mode 100644
index 000000000000..9934de95878b
--- /dev/null
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx
@@ -0,0 +1,238 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useMemo, useState} from 'react';
+import type {SectionListData} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import Badge from '@components/Badge';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import type {ListItem, Section} from '@components/SelectionList/types';
+import UserListItem from '@components/SelectionList/UserListItem';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import compose from '@libs/compose';
+import {formatPhoneNumber} from '@libs/LocalePhoneNumber';
+import Log from '@libs/Log';
+import Navigation from '@libs/Navigation/Navigation';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as UserUtils from '@libs/UserUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {PersonalDetailsList, PolicyMember} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type WorkspaceWorkflowsPayerPageOnyxProps = {
+ /** All of the personal details for everyone */
+ personalDetails: OnyxEntry;
+};
+
+type WorkspaceWorkflowsPayerPageProps = WorkspaceWorkflowsPayerPageOnyxProps &
+ WithPolicyAndFullscreenLoadingProps &
+ StackScreenProps;
+type MemberOption = Omit & {accountID: number};
+type MembersSection = SectionListData>;
+
+function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDetails, isLoadingReportData = true}: WorkspaceWorkflowsPayerPageProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const policyName = policy?.name ?? '';
+ const {isOffline} = useNetwork();
+
+ const [searchTerm, setSearchTerm] = useState('');
+
+ const isDeletedPolicyMember = useCallback(
+ (policyMember: PolicyMember) => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors),
+ [isOffline],
+ );
+
+ const [formattedPolicyAdmins, formattedAuthorizedPayer] = useMemo(() => {
+ const policyAdminDetails: MemberOption[] = [];
+ const authorizedPayerDetails: MemberOption[] = [];
+
+ Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => {
+ const accountID = Number(accountIDKey);
+ const details = personalDetails?.[accountID];
+ if (!details) {
+ Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`);
+ return;
+ }
+
+ const isOwner = policy?.owner === details?.login;
+ const isAdmin = policyMember.role === CONST.POLICY.ROLE.ADMIN;
+ const shouldSkipMember = isDeletedPolicyMember(policyMember) || PolicyUtils.isExpensifyTeam(details?.login) || (!isOwner && !isAdmin);
+
+ if (shouldSkipMember) {
+ return;
+ }
+
+ const roleBadge = (
+
+ );
+
+ const isAuthorizedPayer = policy?.reimburserEmail === details?.login ?? policy?.reimburserAccountID === accountID;
+
+ const formattedMember = {
+ keyForList: accountIDKey,
+ accountID,
+ isSelected: isAuthorizedPayer,
+ isDisabled: policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyMember.errors),
+ text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)),
+ alternateText: formatPhoneNumber(details?.login ?? ''),
+ rightElement: roleBadge,
+ icons: [
+ {
+ source: UserUtils.getAvatar(details?.avatar, accountID),
+ name: formatPhoneNumber(details?.login ?? ''),
+ type: CONST.ICON_TYPE_AVATAR,
+ id: accountID,
+ },
+ ],
+ errors: policyMember.errors,
+ pendingAction: policyMember.pendingAction ?? isAuthorizedPayer ? policy?.pendingFields?.reimburserEmail : null,
+ };
+
+ if (policy?.reimburserEmail === details?.login ?? policy?.reimburserAccountID === accountID) {
+ authorizedPayerDetails.push(formattedMember);
+ } else {
+ policyAdminDetails.push(formattedMember);
+ }
+ });
+ return [policyAdminDetails, authorizedPayerDetails];
+ }, [
+ personalDetails,
+ policyMembers,
+ translate,
+ policy?.reimburserEmail,
+ isDeletedPolicyMember,
+ policy?.owner,
+ styles,
+ StyleUtils,
+ policy?.reimburserAccountID,
+ policy?.pendingFields?.reimburserEmail,
+ ]);
+
+ const sections: MembersSection[] = useMemo(() => {
+ const sectionsArray: MembersSection[] = [];
+
+ if (searchTerm !== '') {
+ const filteredOptions = [...formattedPolicyAdmins, ...formattedAuthorizedPayer].filter((option) => {
+ const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm);
+ return !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue);
+ });
+ return [
+ {
+ title: undefined,
+ data: filteredOptions,
+ shouldShow: true,
+ },
+ ];
+ }
+
+ sectionsArray.push({
+ data: formattedAuthorizedPayer,
+ shouldShow: true,
+ indexOffset: formattedPolicyAdmins.length,
+ });
+
+ sectionsArray.push({
+ title: translate('workflowsPayerPage.admins'),
+ data: formattedPolicyAdmins,
+ shouldShow: true,
+ indexOffset: 0,
+ });
+ return sectionsArray;
+ }, [formattedPolicyAdmins, formattedAuthorizedPayer, translate, searchTerm]);
+
+ const headerMessage = useMemo(
+ () => (searchTerm && !sections[0].data.length ? translate('common.noResultsFound') : ''),
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [translate, sections],
+ );
+
+ const setPolicyAuthorizedPayer = (member: MemberOption) => {
+ const authorizedPayerEmail = personalDetails?.[member.accountID]?.login ?? '';
+ if (policy?.reimburserEmail === authorizedPayerEmail) {
+ return;
+ }
+
+ if (policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) {
+ return;
+ }
+ const authorizedPayerAccountID = member.accountID;
+ Policy.setWorkspacePayer(policy?.id ?? '', authorizedPayerEmail, authorizedPayerAccountID);
+ Navigation.goBack();
+ };
+
+ // eslint-disable-next-line rulesdir/no-negated-variables
+ const shouldShowNotFoundPage = useMemo(
+ () =>
+ (isEmptyObject(policy) && !isLoadingReportData) ||
+ PolicyUtils.isPendingDeletePolicy(policy) ||
+ policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES,
+ [policy, isLoadingReportData],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+WorkspaceWorkflowsPayerPage.displayName = 'WorkspaceWorkflowsPayerPage';
+
+export default compose(
+ withOnyx({
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ }),
+ withPolicyAndFullscreenLoading,
+)(WorkspaceWorkflowsPayerPage);
diff --git a/src/stories/Checkbox.stories.js b/src/stories/Checkbox.stories.tsx
similarity index 62%
rename from src/stories/Checkbox.stories.js
rename to src/stories/Checkbox.stories.tsx
index fc5b63347c71..36bd40366a2a 100644
--- a/src/stories/Checkbox.stories.js
+++ b/src/stories/Checkbox.stories.tsx
@@ -1,28 +1,33 @@
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
import Checkbox from '@components/Checkbox';
+import type {CheckboxProps} from '@components/Checkbox';
+
+type CheckboxStory = ComponentStory;
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-export default {
+const story: ComponentMeta = {
title: 'Components/Checkbox',
component: Checkbox,
};
-function Template(args) {
+function Template(props: CheckboxProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
- return ;
+ return ;
}
// Arguments can be passed to the component by binding
// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
-const Default = Template.bind({});
+const Default: CheckboxStory = Template.bind({});
Default.args = {
onPress: () => {},
isChecked: true,
accessibilityLabel: '',
};
+export default story;
export {Default};
diff --git a/src/stories/Composer.stories.js b/src/stories/Composer.stories.tsx
similarity index 78%
rename from src/stories/Composer.stories.js
rename to src/stories/Composer.stories.tsx
index e4051a4ab72a..8cb3f297684e 100644
--- a/src/stories/Composer.stories.js
+++ b/src/stories/Composer.stories.tsx
@@ -1,16 +1,16 @@
+import type {ComponentMeta} from '@storybook/react';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import React, {useState} from 'react';
import {Image, View} from 'react-native';
import Composer from '@components/Composer';
+import type {ComposerProps} from '@components/Composer/types';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import withNavigationFallback from '@components/withNavigationFallback';
import useStyleUtils from '@hooks/useStyleUtils';
// eslint-disable-next-line no-restricted-imports
-import {defaultStyles} from '@styles/index';
-// eslint-disable-next-line no-restricted-imports
import {defaultTheme} from '@styles/theme';
-import CONST from '@src/CONST';
+import {defaultStyles} from '@src/styles';
const ComposerWithNavigation = withNavigationFallback(Composer);
@@ -19,25 +19,25 @@ const ComposerWithNavigation = withNavigationFallback(Composer);
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/Composer',
component: ComposerWithNavigation,
};
const parser = new ExpensiMark();
-function Default(args) {
+function Default(props: ComposerProps) {
const StyleUtils = useStyleUtils();
- const [pastedFile, setPastedFile] = useState(null);
- const [comment, setComment] = useState(args.defaultValue);
- const renderedHTML = parser.replace(comment);
+ const [pastedFile, setPastedFile] = useState(null);
+ const [comment, setComment] = useState(props.defaultValue);
+ const renderedHTML = parser.replace(comment ?? '');
return (
-
+
Entered Comment (Drop Enabled)
{comment}
Rendered Comment
- {Boolean(renderedHTML) && }
- {Boolean(pastedFile) && (
+ {!!renderedHTML && }
+ {!!pastedFile && (
= {
title: 'Components/DragAndDrop',
component: DragAndDropConsumer,
};
function Default() {
const [fileURL, setFileURL] = useState('');
+
return (
{
- const file = lodashGet(e, ['dataTransfer', 'files', 0]);
- if (file && file.type.includes('image')) {
+ onDrop={(event) => {
+ const file = event.dataTransfer?.files?.[0];
+ if (file?.type.includes('image')) {
const reader = new FileReader();
- reader.addEventListener('load', () => setFileURL(reader.result));
+ reader.addEventListener('load', () => setFileURL(reader.result as string));
reader.readAsDataURL(file);
}
}}
diff --git a/src/stories/EReceipt.stories.js b/src/stories/EReceipt.stories.tsx
similarity index 81%
rename from src/stories/EReceipt.stories.js
rename to src/stories/EReceipt.stories.tsx
index d4f2b58cb213..f652c08df6f6 100644
--- a/src/stories/EReceipt.stories.js
+++ b/src/stories/EReceipt.stories.tsx
@@ -1,8 +1,13 @@
-/* eslint-disable rulesdir/prefer-actions-set-data */
+/* eslint-disable @typescript-eslint/naming-convention, rulesdir/prefer-actions-set-data */
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
import Onyx from 'react-native-onyx';
+import type {EReceiptOnyxProps, EReceiptProps} from '@components/EReceipt';
import EReceipt from '@components/EReceipt';
import ONYXKEYS from '@src/ONYXKEYS';
+import type CollectionDataSet from '@src/types/utils/CollectionDataSet';
+
+type EReceiptStory = ComponentStory;
const transactionData = {
[`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_1`]: {
@@ -146,7 +151,7 @@ const transactionData = {
created: '2023-01-11 13:46:20',
hasEReceipt: true,
},
-};
+} as CollectionDataSet;
Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION, transactionData);
Onyx.merge('cardList', {
@@ -159,92 +164,92 @@ Onyx.merge('cardList', {
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/EReceipt',
component: EReceipt,
};
-function Template(args) {
+function Template(props: Omit) {
// eslint-disable-next-line react/jsx-props-no-spreading
- return ;
+ return ;
}
-const Default = Template.bind({});
+const Default: EReceiptStory = Template.bind({});
Default.args = {
transactionID: 'FAKE_1',
};
-const Airlines = Template.bind({});
+const Airlines: EReceiptStory = Template.bind({});
Airlines.args = {
transactionID: 'FAKE_2',
};
-const Commuter = Template.bind({});
+const Commuter: EReceiptStory = Template.bind({});
Commuter.args = {
transactionID: 'FAKE_3',
};
-const Gas = Template.bind({});
+const Gas: EReceiptStory = Template.bind({});
Gas.args = {
transactionID: 'FAKE_4',
};
-const Goods = Template.bind({});
+const Goods: EReceiptStory = Template.bind({});
Goods.args = {
transactionID: 'FAKE_5',
};
-const Groceries = Template.bind({});
+const Groceries: EReceiptStory = Template.bind({});
Groceries.args = {
transactionID: 'FAKE_6',
};
-const Hotel = Template.bind({});
+const Hotel: EReceiptStory = Template.bind({});
Hotel.args = {
transactionID: 'FAKE_7',
};
-const Mail = Template.bind({});
+const Mail: EReceiptStory = Template.bind({});
Mail.args = {
transactionID: 'FAKE_8',
};
-const Meals = Template.bind({});
+const Meals: EReceiptStory = Template.bind({});
Meals.args = {
transactionID: 'FAKE_9',
};
-const Rental = Template.bind({});
+const Rental: EReceiptStory = Template.bind({});
Rental.args = {
transactionID: 'FAKE_10',
};
-const Services = Template.bind({});
+const Services: EReceiptStory = Template.bind({});
Services.args = {
transactionID: 'FAKE_11',
};
-const Taxi = Template.bind({});
+const Taxi: EReceiptStory = Template.bind({});
Taxi.args = {
transactionID: 'FAKE_12',
};
-const Miscellaneous = Template.bind({});
+const Miscellaneous: EReceiptStory = Template.bind({});
Miscellaneous.args = {
transactionID: 'FAKE_13',
};
-const Utilities = Template.bind({});
+const Utilities: EReceiptStory = Template.bind({});
Utilities.args = {
transactionID: 'FAKE_14',
};
-const invalidMCC = Template.bind({});
+const invalidMCC: EReceiptStory = Template.bind({});
invalidMCC.args = {
transactionID: 'FAKE_15',
};
-const veryLong = Template.bind({});
+const veryLong: EReceiptStory = Template.bind({});
veryLong.args = {
transactionID: 'FAKE_16',
};
diff --git a/src/stories/EReceiptThumbail.stories.js b/src/stories/EReceiptThumbail.stories.tsx
similarity index 62%
rename from src/stories/EReceiptThumbail.stories.js
rename to src/stories/EReceiptThumbail.stories.tsx
index bea32fb0213c..1feb811e57c8 100644
--- a/src/stories/EReceiptThumbail.stories.js
+++ b/src/stories/EReceiptThumbail.stories.tsx
@@ -1,52 +1,56 @@
/* eslint-disable react/jsx-props-no-spreading */
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
import {View} from 'react-native';
+import type {EReceiptThumbnailOnyxProps, EReceiptThumbnailProps} from '@components/EReceiptThumbnail';
import EReceiptThumbnail from '@components/EReceiptThumbnail';
+type EReceiptThumbnailStory = ComponentStory;
+
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/EReceiptThumbnail',
component: EReceiptThumbnail,
};
-function Template(args) {
+function Template(props: Omit) {
return (
@@ -54,77 +58,77 @@ function Template(args) {
);
}
-const Default = Template.bind({});
+const Default: EReceiptThumbnailStory = Template.bind({});
Default.args = {
transactionID: 'FAKE_1',
};
-const Airlines = Template.bind({});
+const Airlines: EReceiptThumbnailStory = Template.bind({});
Airlines.args = {
transactionID: 'FAKE_2',
};
-const Commuter = Template.bind({});
+const Commuter: EReceiptThumbnailStory = Template.bind({});
Commuter.args = {
transactionID: 'FAKE_3',
};
-const Gas = Template.bind({});
+const Gas: EReceiptThumbnailStory = Template.bind({});
Gas.args = {
transactionID: 'FAKE_4',
};
-const Goods = Template.bind({});
+const Goods: EReceiptThumbnailStory = Template.bind({});
Goods.args = {
transactionID: 'FAKE_5',
};
-const Groceries = Template.bind({});
+const Groceries: EReceiptThumbnailStory = Template.bind({});
Groceries.args = {
transactionID: 'FAKE_6',
};
-const Hotel = Template.bind({});
+const Hotel: EReceiptThumbnailStory = Template.bind({});
Hotel.args = {
transactionID: 'FAKE_7',
};
-const Mail = Template.bind({});
+const Mail: EReceiptThumbnailStory = Template.bind({});
Mail.args = {
transactionID: 'FAKE_8',
};
-const Meals = Template.bind({});
+const Meals: EReceiptThumbnailStory = Template.bind({});
Meals.args = {
transactionID: 'FAKE_9',
};
-const Rental = Template.bind({});
+const Rental: EReceiptThumbnailStory = Template.bind({});
Rental.args = {
transactionID: 'FAKE_10',
};
-const Services = Template.bind({});
+const Services: EReceiptThumbnailStory = Template.bind({});
Services.args = {
transactionID: 'FAKE_11',
};
-const Taxi = Template.bind({});
+const Taxi: EReceiptThumbnailStory = Template.bind({});
Taxi.args = {
transactionID: 'FAKE_12',
};
-const Miscellaneous = Template.bind({});
+const Miscellaneous: EReceiptThumbnailStory = Template.bind({});
Miscellaneous.args = {
transactionID: 'FAKE_13',
};
-const Utilities = Template.bind({});
+const Utilities: EReceiptThumbnailStory = Template.bind({});
Utilities.args = {
transactionID: 'FAKE_14',
};
-const invalidMCC = Template.bind({});
+const invalidMCC: EReceiptThumbnailStory = Template.bind({});
invalidMCC.args = {
transactionID: 'FAKE_15',
};
diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.tsx
similarity index 76%
rename from src/stories/Form.stories.js
rename to src/stories/Form.stories.tsx
index 62f9a35d89e2..8eeab971ea88 100644
--- a/src/stories/Form.stories.js
+++ b/src/stories/Form.stories.tsx
@@ -1,27 +1,47 @@
+import type {ComponentMeta, Story} from '@storybook/react';
import React, {useState} from 'react';
import {View} from 'react-native';
import AddressSearch from '@components/AddressSearch';
import CheckboxWithLabel from '@components/CheckboxWithLabel';
import DatePicker from '@components/DatePicker';
import FormProvider from '@components/Form/FormProvider';
+import type {FormProviderProps} from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import Picker from '@components/Picker';
import StatePicker from '@components/StatePicker';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
+import type {MaybePhraseKey} from '@libs/Localize';
import NetworkConnection from '@libs/NetworkConnection';
import * as ValidationUtils from '@libs/ValidationUtils';
-// eslint-disable-next-line no-restricted-imports
-import {defaultStyles} from '@styles/index';
import * as FormActions from '@userActions/FormActions';
import CONST from '@src/CONST';
+import type {OnyxFormValuesMapping} from '@src/ONYXKEYS';
+import {defaultStyles} from '@src/styles';
+
+type FormStory = Story;
+
+type StorybookFormValues = {
+ routingNumber?: string;
+ accountNumber?: string;
+ street?: string;
+ dob?: string;
+ pickFruit?: string;
+ pickAnotherFruit?: string;
+ state?: string;
+ checkbox?: boolean;
+};
+
+type StorybookFormErrors = Partial>;
+
+const STORYBOOK_FORM_ID = 'TestForm' as keyof OnyxFormValuesMapping;
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/Form',
component: FormProvider,
subcomponents: {
@@ -35,16 +55,21 @@ const story = {
},
};
-function Template(args) {
+function Template(props: FormProviderProps) {
// Form consumes data from Onyx, so we initialize Onyx with the necessary data here
NetworkConnection.setOfflineStatus(false);
- FormActions.setIsLoading(args.formID, args.formState.isLoading);
- FormActions.setErrors(args.formID, args.formState.error);
- FormActions.setDraftValues(args.formID, args.draftValues);
+ FormActions.setIsLoading(props.formID, !!props.formState?.isLoading);
+ FormActions.setDraftValues(props.formID, props.draftValues);
+
+ if (props.formState?.error) {
+ FormActions.setErrors(props.formID, {error: props.formState.error as MaybePhraseKey});
+ } else {
+ FormActions.clearErrors(props.formID);
+ }
return (
// eslint-disable-next-line react/jsx-props-no-spreading
-
+
{}}
+ containerStyles={defaultStyles.mt4}
shouldSaveDraft
items={[
{
@@ -103,7 +129,8 @@ function Template(args) {
InputComponent={Picker}
label="Another Fruit"
inputID="pickAnotherFruit"
- containerStyles={[defaultStyles.mt4]}
+ onInputChange={() => {}}
+ containerStyles={defaultStyles.mt4}
items={[
{
label: 'Select a Fruit',
@@ -139,21 +166,24 @@ function Template(args) {
/**
* Story to exhibit the native event handlers for TextInput in the Form Component
- * @param {Object} args
- * @returns {JSX}
*/
-function WithNativeEventHandler(args) {
+function WithNativeEventHandler(props: FormProviderProps) {
const [log, setLog] = useState('');
// Form consumes data from Onyx, so we initialize Onyx with the necessary data here
NetworkConnection.setOfflineStatus(false);
- FormActions.setIsLoading(args.formID, args.formState.isLoading);
- FormActions.setErrors(args.formID, args.formState.error);
- FormActions.setDraftValues(args.formID, args.draftValues);
+ FormActions.setIsLoading(props.formID, !!props.formState?.isLoading);
+ FormActions.setDraftValues(props.formID, props.draftValues);
+
+ if (props.formState?.error) {
+ FormActions.setErrors(props.formID, {error: props.formState.error as MaybePhraseKey});
+ } else {
+ FormActions.clearErrors(props.formID);
+ }
return (
// eslint-disable-next-line react/jsx-props-no-spreading
-
+
{
- const errors = {};
+ validate: (values: StorybookFormValues) => {
+ const errors: StorybookFormErrors = {};
if (!ValidationUtils.isRequiredFulfilled(values.routingNumber)) {
errors.routingNumber = 'Please enter a routing number';
}
@@ -206,10 +236,10 @@ const defaultArgs = {
}
return errors;
},
- onSubmit: (values) => {
+ onSubmit: (values: StorybookFormValues) => {
setTimeout(() => {
alert(`Form submitted!\n\nInput values: ${JSON.stringify(values, null, 4)}`);
- FormActions.setIsLoading('TestForm', false);
+ FormActions.setIsLoading(STORYBOOK_FORM_ID, false);
}, 1000);
},
formState: {
diff --git a/src/stories/FormAlertWithSubmitButton.stories.js b/src/stories/FormAlertWithSubmitButton.stories.tsx
similarity index 60%
rename from src/stories/FormAlertWithSubmitButton.stories.js
rename to src/stories/FormAlertWithSubmitButton.stories.tsx
index 5d64deb74145..d6060b9c2ad1 100644
--- a/src/stories/FormAlertWithSubmitButton.stories.js
+++ b/src/stories/FormAlertWithSubmitButton.stories.tsx
@@ -1,33 +1,34 @@
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import type {FormAlertWithSubmitButtonProps} from '@components/FormAlertWithSubmitButton';
+
+type FormAlertWithSubmitButtonStory = ComponentStory;
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/FormAlertWithSubmitButton',
component: FormAlertWithSubmitButton,
};
-function Template(args) {
+function Template(props: FormAlertWithSubmitButtonProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
- return ;
+ return ;
}
// Arguments can be passed to the component by binding
// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
-const Default = Template.bind({});
-const HtmlError = Template.bind({});
+const Default: FormAlertWithSubmitButtonStory = Template.bind({});
+const HtmlError: FormAlertWithSubmitButtonStory = Template.bind({});
-const defaultArgs = {
+const defaultArgs: FormAlertWithSubmitButtonStory['args'] = {
isAlertVisible: true,
onSubmit: () => {},
buttonText: 'Submit',
- network: {
- isOffline: true,
- },
};
Default.args = defaultArgs;
diff --git a/src/stories/Header.stories.js b/src/stories/Header.stories.tsx
similarity index 64%
rename from src/stories/Header.stories.js
rename to src/stories/Header.stories.tsx
index 8560fba4b27f..e683a78be992 100644
--- a/src/stories/Header.stories.js
+++ b/src/stories/Header.stories.tsx
@@ -1,24 +1,28 @@
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
+import type {HeaderProps} from '@components/Header';
import Header from '@components/Header';
+type HeaderStory = ComponentStory;
+
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/Header',
component: Header,
};
-function Template(args) {
+function Template(props: HeaderProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
- return ;
+ return ;
}
// Arguments can be passed to the component by binding
// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
-const Default = Template.bind({});
+const Default: HeaderStory = Template.bind({});
Default.args = {
title: 'Chats',
shouldShowEnvironmentBadge: true,
diff --git a/src/stories/HeaderWithBackButton.stories.js b/src/stories/HeaderWithBackButton.stories.tsx
similarity index 61%
rename from src/stories/HeaderWithBackButton.stories.js
rename to src/stories/HeaderWithBackButton.stories.tsx
index eb31413de1d5..8306d8e19225 100644
--- a/src/stories/HeaderWithBackButton.stories.js
+++ b/src/stories/HeaderWithBackButton.stories.tsx
@@ -1,29 +1,33 @@
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/types';
import withNavigationFallback from '@components/withNavigationFallback';
const HeaderWithBackButtonWithNavigation = withNavigationFallback(HeaderWithBackButton);
+type HeaderWithBackButtonStory = ComponentStory;
+
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/HeaderWithBackButton',
component: HeaderWithBackButtonWithNavigation,
};
-function Template(args) {
+function Template(props: HeaderWithBackButtonProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
- return ;
+ return ;
}
// Arguments can be passed to the component by binding
// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
-const Default = Template.bind({});
-const Attachment = Template.bind({});
-const Profile = Template.bind({});
+const Default: HeaderWithBackButtonStory = Template.bind({});
+const Attachment: HeaderWithBackButtonStory = Template.bind({});
+const Profile: HeaderWithBackButtonStory = Template.bind({});
Default.args = {
title: 'Settings',
};
diff --git a/src/stories/RadioButtonWithLabel.stories.js b/src/stories/RadioButtonWithLabel.stories.tsx
similarity index 60%
rename from src/stories/RadioButtonWithLabel.stories.js
rename to src/stories/RadioButtonWithLabel.stories.tsx
index af5d6ec15a8c..3280864b8fdb 100644
--- a/src/stories/RadioButtonWithLabel.stories.js
+++ b/src/stories/RadioButtonWithLabel.stories.tsx
@@ -1,35 +1,37 @@
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
import RadioButtonWithLabel from '@components/RadioButtonWithLabel';
+import type {RadioButtonWithLabelProps} from '@components/RadioButtonWithLabel';
+
+type RadioButtonWithLabelStory = ComponentStory;
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/RadioButtonWithLabel',
component: RadioButtonWithLabel,
};
-function Template(args) {
+function Template(props: RadioButtonWithLabelProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
- return ;
+ return ;
}
// Arguments can be passed to the component by binding
// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
-const Default = Template.bind({});
-const Checked = Template.bind({});
+const Default: RadioButtonWithLabelStory = Template.bind({});
+const Checked: RadioButtonWithLabelStory = Template.bind({});
Default.args = {
isChecked: false,
label: 'This radio button is unchecked',
- onInputChange: () => {},
};
Checked.args = {
isChecked: true,
label: 'This radio button is checked',
- onInputChange: () => {},
};
export default story;
diff --git a/src/stories/ReportActionItemImages.stories.js b/src/stories/ReportActionItemImages.stories.tsx
similarity index 84%
rename from src/stories/ReportActionItemImages.stories.js
rename to src/stories/ReportActionItemImages.stories.tsx
index fe86f50f7e34..810b3e18aaf3 100644
--- a/src/stories/ReportActionItemImages.stories.js
+++ b/src/stories/ReportActionItemImages.stories.tsx
@@ -1,18 +1,22 @@
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import type {ReportActionItemImagesProps} from '@components/ReportActionItem/ReportActionItemImages';
import ReportActionItemImages from '@components/ReportActionItem/ReportActionItemImages';
+type ReportActionItemImagesStory = ComponentStory;
+
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/ReportActionItemImages',
component: ReportActionItemImages,
};
-function Template(args) {
+function Template(props: ReportActionItemImagesProps) {
return (
(
)}
@@ -31,14 +35,14 @@ function Template(args) {
// Arguments can be passed to the component by binding
// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
-const Default = Template.bind({});
+const Default: ReportActionItemImagesStory = Template.bind({});
Default.args = {
images: [{image: 'https://c02.purpledshub.com/uploads/sites/41/2021/05/sleeping-cat-27126ee.jpg', thumbnail: ''}],
size: 1,
total: 1,
};
-const DisplayEReceipt = Template.bind({});
+const DisplayEReceipt: ReportActionItemImagesStory = Template.bind({});
DisplayEReceipt.args = {
images: [
{
@@ -53,6 +57,8 @@ DisplayEReceipt.args = {
mccGroup: 'Commuter',
created: '2023-07-24 13:46:20',
hasEReceipt: true,
+ comment: {},
+ reportID: 'REPORT_1',
},
},
],
@@ -60,7 +66,7 @@ DisplayEReceipt.args = {
total: 1,
};
-const DisplayMultipleEReceipts = Template.bind({});
+const DisplayMultipleEReceipts: ReportActionItemImagesStory = Template.bind({});
DisplayMultipleEReceipts.args = {
images: [
{
@@ -75,6 +81,8 @@ DisplayMultipleEReceipts.args = {
mccGroup: 'Commuter',
created: '2023-07-24 13:46:20',
hasEReceipt: true,
+ comment: {},
+ reportID: 'REPORT_1',
},
},
{
@@ -89,6 +97,8 @@ DisplayMultipleEReceipts.args = {
mccGroup: 'Goods',
created: '2022-03-21 13:46:20',
hasEReceipt: true,
+ comment: {},
+ reportID: 'REPORT_2',
},
},
{
@@ -103,6 +113,8 @@ DisplayMultipleEReceipts.args = {
mccGroup: 'Airlines',
created: '2023-07-24 13:46:20',
hasEReceipt: true,
+ comment: {},
+ reportID: 'REPORT_3',
},
},
],
@@ -110,7 +122,7 @@ DisplayMultipleEReceipts.args = {
total: 3,
};
-const TwoImages = Template.bind({});
+const TwoImages: ReportActionItemImagesStory = Template.bind({});
TwoImages.args = {
images: [
{
@@ -126,7 +138,7 @@ TwoImages.args = {
total: 2,
};
-const ThreeImages = Template.bind({});
+const ThreeImages: ReportActionItemImagesStory = Template.bind({});
ThreeImages.args = {
images: [
{
@@ -146,7 +158,7 @@ ThreeImages.args = {
total: 3,
};
-const FourImages = Template.bind({});
+const FourImages: ReportActionItemImagesStory = Template.bind({});
FourImages.args = {
images: [
{
@@ -170,7 +182,7 @@ FourImages.args = {
total: 4,
};
-const ThreePlusTwoImages = Template.bind({});
+const ThreePlusTwoImages: ReportActionItemImagesStory = Template.bind({});
ThreePlusTwoImages.args = {
images: [
{
@@ -190,7 +202,7 @@ ThreePlusTwoImages.args = {
total: 5,
};
-const ThreePlusTenImages = Template.bind({});
+const ThreePlusTenImages: ReportActionItemImagesStory = Template.bind({});
ThreePlusTenImages.args = {
images: [
{
diff --git a/src/stories/SelectionList.stories.js b/src/stories/SelectionList.stories.tsx
similarity index 60%
rename from src/stories/SelectionList.stories.js
rename to src/stories/SelectionList.stories.tsx
index 6c289097552b..92936d6d73a3 100644
--- a/src/stories/SelectionList.stories.js
+++ b/src/stories/SelectionList.stories.tsx
@@ -1,18 +1,22 @@
+import type {ComponentMeta} from '@storybook/react';
import React, {useMemo, useState} from 'react';
-import _ from 'underscore';
import Badge from '@components/Badge';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {BaseSelectionListProps, ListItem} from '@components/SelectionList/types';
+import withNavigationFallback from '@components/withNavigationFallback';
// eslint-disable-next-line no-restricted-imports
import {defaultStyles} from '@styles/index';
import CONST from '@src/CONST';
+const SelectionListWithNavigation = withNavigationFallback(SelectionList);
+
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/SelectionList',
component: SelectionList,
};
@@ -62,32 +66,32 @@ const SECTIONS = [
},
];
-function Default(args) {
+function Default(props: BaseSelectionListProps) {
const [selectedIndex, setSelectedIndex] = useState(1);
- const sections = _.map(args.sections, (section) => {
- const data = _.map(section.data, (item, index) => {
- const isSelected = selectedIndex === index + section.indexOffset;
+ const sections = props.sections.map((section) => {
+ const data = section.data.map((item, index) => {
+ const isSelected = selectedIndex === index + (section?.indexOffset ?? 0);
return {...item, isSelected};
});
return {...section, data};
});
- const onSelectRow = (item) => {
- _.forEach(sections, (section) => {
- const newSelectedIndex = _.findIndex(section.data, (option) => option.keyForList === item.keyForList);
+ const onSelectRow = (item: ListItem) => {
+ sections.forEach((section) => {
+ const newSelectedIndex = section.data.findIndex((option) => option.keyForList === item.keyForList);
if (newSelectedIndex >= 0) {
- setSelectedIndex(newSelectedIndex + section.indexOffset);
+ setSelectedIndex(newSelectedIndex + (section?.indexOffset ?? 0));
}
});
};
return (
- ) {
const [searchText, setSearchText] = useState('');
const [selectedIndex, setSelectedIndex] = useState(1);
- const sections = _.map(args.sections, (section) => {
- const data = _.reduce(
- section.data,
- (memo, item, index) => {
- if (!item.text.toLowerCase().includes(searchText.trim().toLowerCase())) {
- return memo;
- }
-
- const isSelected = selectedIndex === index + section.indexOffset;
- memo.push({...item, isSelected});
+ const sections = props.sections.map((section) => {
+ const data = section.data.reduce((memo, item, index) => {
+ if (!item.text?.toLowerCase().includes(searchText.trim().toLowerCase())) {
return memo;
- },
- [],
- );
+ }
+
+ const isSelected = selectedIndex === index + (section?.indexOffset ?? 0);
+ memo.push({...item, isSelected});
+ return memo;
+ }, []);
return {...section, data};
});
- const onSelectRow = (item) => {
- _.forEach(sections, (section) => {
- const newSelectedIndex = _.findIndex(section.data, (option) => option.keyForList === item.keyForList);
+ const onSelectRow = (item: ListItem) => {
+ sections.forEach((section) => {
+ const newSelectedIndex = section.data.findIndex((option) => option.keyForList === item.keyForList);
if (newSelectedIndex >= 0) {
- setSelectedIndex(newSelectedIndex + section.indexOffset);
+ setSelectedIndex(newSelectedIndex + (section?.indexOffset ?? 0));
}
});
};
return (
- {},
};
-function WithHeaderMessage(props) {
+function WithHeaderMessage(props: BaseSelectionListProps) {
return (
) {
const [selectedIndex, setSelectedIndex] = useState(1);
- const sections = _.map(args.sections, (section) => {
- const data = _.map(section.data, (item, index) => {
- const isSelected = selectedIndex === index + section.indexOffset;
+ const sections = props.sections.map((section) => {
+ const data = section.data.map((item, index) => {
+ const isSelected = selectedIndex === index + (section?.indexOffset ?? 0);
return {
...item,
@@ -189,21 +189,22 @@ function WithAlternateText(args) {
return {...section, data};
});
- const onSelectRow = (item) => {
- _.forEach(sections, (section) => {
- const newSelectedIndex = _.findIndex(section.data, (option) => option.keyForList === item.keyForList);
+ const onSelectRow = (item: ListItem) => {
+ sections.forEach((section) => {
+ const newSelectedIndex = section.data.findIndex((option) => option.keyForList === item.keyForList);
if (newSelectedIndex >= 0) {
- setSelectedIndex(newSelectedIndex + section.indexOffset);
+ setSelectedIndex(newSelectedIndex + (section?.indexOffset ?? 0));
}
});
};
return (
-
);
}
@@ -212,23 +213,25 @@ WithAlternateText.args = {
...Default.args,
};
-function MultipleSelection(args) {
+function MultipleSelection(props: BaseSelectionListProps) {
const [selectedIds, setSelectedIds] = useState(['option-1', 'option-2']);
const memo = useMemo(() => {
- const allIds = [];
+ const allIds: string[] = [];
- const sections = _.map(args.sections, (section) => {
- const data = _.map(section.data, (item, index) => {
- allIds.push(item.keyForList);
- const isSelected = _.contains(selectedIds, item.keyForList);
- const isAdmin = index + section.indexOffset === 0;
+ const sections = props.sections.map((section) => {
+ const data = section.data.map((item, index) => {
+ if (item.keyForList) {
+ allIds.push(item.keyForList);
+ }
+ const isSelected = item.keyForList ? selectedIds.includes(item.keyForList) : false;
+ const isAdmin = index + (section?.indexOffset ?? 0) === 0;
return {
...item,
isSelected,
alternateText: `${item.keyForList}@email.com`,
- accountID: item.keyForList,
+ accountID: Number(item.keyForList),
login: item.text,
rightElement: isAdmin && (
{
- const newSelectedIds = _.contains(selectedIds, item.keyForList) ? _.without(selectedIds, item.keyForList) : [...selectedIds, item.keyForList];
+ const onSelectRow = (item: ListItem) => {
+ if (!item.keyForList) {
+ return;
+ }
+ const newSelectedIds = selectedIds.includes(item.keyForList) ? selectedIds.filter((id) => id !== item.keyForList) : [...selectedIds, item.keyForList];
setSelectedIds(newSelectedIds);
};
@@ -260,9 +266,9 @@ function MultipleSelection(args) {
};
return (
- {},
};
-function WithSectionHeader(args) {
+function WithSectionHeader(props: BaseSelectionListProps) {
const [selectedIds, setSelectedIds] = useState(['option-1', 'option-2']);
const memo = useMemo(() => {
- const allIds = [];
+ const allIds: string[] = [];
- const sections = _.map(args.sections, (section, sectionIndex) => {
- const data = _.map(section.data, (item, itemIndex) => {
- allIds.push(item.keyForList);
- const isSelected = _.contains(selectedIds, item.keyForList);
- const isAdmin = itemIndex + section.indexOffset === 0;
+ const sections = props.sections.map((section, sectionIndex) => {
+ const data = section.data.map((item, itemIndex) => {
+ if (item.keyForList) {
+ allIds.push(item.keyForList);
+ }
+ const isSelected = item.keyForList ? selectedIds.includes(item.keyForList) : false;
+ const isAdmin = itemIndex + (section?.indexOffset ?? 0) === 0;
return {
...item,
isSelected,
alternateText: `${item.keyForList}@email.com`,
- accountID: item.keyForList,
+ accountID: Number(item.keyForList),
login: item.text,
rightElement: isAdmin && (
{
- const newSelectedIds = _.contains(selectedIds, item.keyForList) ? _.without(selectedIds, item.keyForList) : [...selectedIds, item.keyForList];
+ const onSelectRow = (item: ListItem) => {
+ if (!item.keyForList) {
+ return;
+ }
+ const newSelectedIds = selectedIds.includes(item.keyForList) ? selectedIds.filter((id) => id !== item.keyForList) : [...selectedIds, item.keyForList];
setSelectedIds(newSelectedIds);
};
@@ -325,9 +336,9 @@ function WithSectionHeader(args) {
};
return (
- ) {
const [selectedIds, setSelectedIds] = useState(['option-1', 'option-2']);
const memo = useMemo(() => {
- const allIds = [];
+ const allIds: string[] = [];
- const sections = _.map(args.sections, (section, sectionIndex) => {
- const data = _.map(section.data, (item, itemIndex) => {
- allIds.push(item.keyForList);
- const isSelected = _.contains(selectedIds, item.keyForList);
- const isAdmin = itemIndex + section.indexOffset === 0;
+ const sections = props.sections.map((section, sectionIndex) => {
+ const data = section.data.map((item, itemIndex) => {
+ if (item.keyForList) {
+ allIds.push(item.keyForList);
+ }
+ const isSelected = item.keyForList ? selectedIds.includes(item.keyForList) : false;
+ const isAdmin = itemIndex + (section.indexOffset ?? 0) === 0;
return {
...item,
isSelected,
alternateText: `${item.keyForList}@email.com`,
- accountID: item.keyForList,
+ accountID: Number(item.keyForList),
login: item.text,
rightElement: isAdmin && (
{
- const newSelectedIds = _.contains(selectedIds, item.keyForList) ? _.without(selectedIds, item.keyForList) : [...selectedIds, item.keyForList];
+ const onSelectRow = (item: ListItem) => {
+ if (!item.keyForList) {
+ return;
+ }
+ const newSelectedIds = selectedIds.includes(item.keyForList) ? selectedIds.filter((id) => id !== item.keyForList) : [...selectedIds, item.keyForList];
setSelectedIds(newSelectedIds);
};
@@ -388,9 +404,9 @@ function WithConfirmButton(args) {
};
return (
-
},
sidebarAvatar: {
- backgroundColor: theme.icon,
+ borderRadius: 28,
+ height: variables.componentSizeSmall,
+ width: variables.componentSizeSmall,
+ },
+
+ selectedAvatarBorder: {
+ padding: 2,
+ borderWidth: 2,
borderRadius: 20,
- height: variables.componentSizeNormal,
- width: variables.componentSizeNormal,
+ borderColor: theme.success,
},
statusIndicator: (backgroundColor = theme.danger) =>
@@ -1455,10 +1461,10 @@ const styles = (theme: ThemeColors) =>
borderRadius: 8,
borderWidth: 2,
position: 'absolute',
- right: -2,
- top: -1,
- height: 16,
- width: 16,
+ right: -4,
+ top: -3,
+ height: 12,
+ width: 12,
zIndex: 10,
} satisfies ViewStyle),
@@ -2648,6 +2654,12 @@ const styles = (theme: ThemeColors) =>
...spacing.pt0,
},
+ workspaceSettingsSectionContainer: {
+ borderBottomWidth: 1,
+ borderBottomColor: theme.border,
+ ...spacing.pt4,
+ },
+
centralPaneAnimation: {
height: CONST.CENTRAL_PANE_ANIMATION_HEIGHT,
},
@@ -3086,7 +3098,7 @@ const styles = (theme: ThemeColors) =>
smallEditIcon: {
alignItems: 'center',
backgroundColor: theme.buttonDefaultBG,
- borderColor: theme.cardBG,
+ borderColor: theme.appBG,
borderRadius: 20,
borderWidth: 3,
color: theme.textReversed,
diff --git a/src/styles/utils/addOutlineWidth/index.native.ts b/src/styles/utils/addOutlineWidth/index.native.ts
index 9a9942951cd0..fdc338caaffd 100644
--- a/src/styles/utils/addOutlineWidth/index.native.ts
+++ b/src/styles/utils/addOutlineWidth/index.native.ts
@@ -4,7 +4,6 @@
*/
import type AddOutlineWidth from './types';
-// eslint-disable-next-line @typescript-eslint/naming-convention
const addOutlineWidth: AddOutlineWidth = (_theme, obj) => obj;
export default addOutlineWidth;
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index f32d166d06e4..31a19904b81c 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -453,7 +453,7 @@ function getBackgroundColorWithOpacityStyle(backgroundColor: string, opacity: nu
return {};
}
-function getWidthAndHeightStyle(width: number, height?: number): ViewStyle {
+function getWidthAndHeightStyle(width: number, height?: number): Pick {
return {
width,
height: height ?? width,
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index 4f4ea821e794..56000a851e7b 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -167,6 +167,10 @@ export default {
marginLeft: 24,
},
+ ml7: {
+ marginLeft: 28,
+ },
+
ml8: {
marginLeft: 32,
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index d63fb5e9f339..61c16a8c2fd7 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -12,7 +12,7 @@ function getValueUsingPixelRatio(defaultValue: number, maxValue: number): number
}
export default {
- bottomTabHeight: 80,
+ bottomTabHeight: 72,
contentHeaderHeight: getValueUsingPixelRatio(72, 100),
contentHeaderDesktopHeight: getValueUsingPixelRatio(80, 100),
componentSizeSmall: getValueUsingPixelRatio(28, 32),
diff --git a/src/types/form/WorkspaceTagCreateForm.ts b/src/types/form/WorkspaceTagCreateForm.ts
new file mode 100644
index 000000000000..9a9670d84ae8
--- /dev/null
+++ b/src/types/form/WorkspaceTagCreateForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ TAG_NAME: 'tagName',
+} as const;
+
+type InputID = ValueOf;
+
+type WorkspaceTagCreateForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.TAG_NAME]: string;
+ }
+>;
+
+export type {WorkspaceTagCreateForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 5fe6eea5c3af..5a574de3db54 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -39,4 +39,5 @@ export type {WorkspaceSettingsForm} from './WorkspaceSettingsForm';
export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm';
export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm';
export type {PolicyTagNameForm} from './PolicyTagNameForm';
+export type {WorkspaceTagCreateForm} from './WorkspaceTagCreateForm';
export type {default as Form} from './Form';
diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts
index 21f1d620b14f..effbdd9d28fa 100644
--- a/src/types/modules/react-native.d.ts
+++ b/src/types/modules/react-native.d.ts
@@ -330,6 +330,14 @@ declare module 'react-native' {
}
interface PressableProps extends WebPressableProps {}
+ interface AppStateStatic {
+ emitCurrentTestState: (status: string) => void;
+ }
+
+ interface LinkingStatic {
+ setInitialURL: (url: string) => void;
+ }
+
/**
* Styles
*/
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index cf959194be5f..ed30f4b7d201 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -321,6 +321,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Email of the reimburser when reimbursement is set direct */
reimburserEmail?: string;
+ /** AccountID of the reimburser when reimbursement is set direct */
+ reimburserAccountID?: number;
+
/** ReportID of the admins room for this workspace */
chatReportIDAdmins?: number;
diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts
index a66fb731269e..70a0884c30bd 100644
--- a/src/types/onyx/PolicyTag.ts
+++ b/src/types/onyx/PolicyTag.ts
@@ -10,9 +10,12 @@ type PolicyTag = {
/** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */
// eslint-disable-next-line @typescript-eslint/naming-convention
'GL Code': string;
+
+ /** A list of errors keyed by microtime */
+ errors?: OnyxCommon.Errors | null;
};
-type PolicyTags = Record;
+type PolicyTags = Record>;
type PolicyTagList = Record<
T,
diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts
index 1f3b49ff77b0..c34534c0f420 100644
--- a/src/types/onyx/Report.ts
+++ b/src/types/onyx/Report.ts
@@ -32,6 +32,9 @@ type Participants = Record;
type Report = OnyxCommon.OnyxValueWithOfflineFeedback<
{
+ /** The URL of the Group Chat report custom avatar */
+ avatarUrl?: string;
+
/** The specific type of chat */
chatType?: ValueOf;
diff --git a/src/utils/times.ts b/src/utils/times.ts
index 1dc97eb74659..0f2a0766a8c3 100644
--- a/src/utils/times.ts
+++ b/src/utils/times.ts
@@ -1,5 +1,4 @@
function times(n: number, func: (index: number) => TReturnType = (i) => i as TReturnType): TReturnType[] {
- // eslint-disable-next-line @typescript-eslint/naming-convention
return Array.from({length: n}).map((_, i) => func(i));
}
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index 5f124f20e872..ea36172a52ff 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -117,7 +117,7 @@ components:
- Orchestrates the test suite.
- Runs the app with the tests on a device
- Responsible for gathering and comparing results
- - Located in `e2e/testRunner.js`.
+ - Located in `e2e/testRunner.ts`.
- Test server:
- A nodeJS application that starts an HTTP server.
diff --git a/tests/e2e/TestSpec.yml b/tests/e2e/TestSpec.yml
index 333d3af7d03d..3570bc11f3bb 100644
--- a/tests/e2e/TestSpec.yml
+++ b/tests/e2e/TestSpec.yml
@@ -21,7 +21,7 @@ phases:
test:
commands:
- cd zip
- - node testRunner.js -- --mainAppPath app-e2eRelease.apk --deltaAppPath app-e2edeltaRelease.apk
+ - node testRunner.ts -- --mainAppPath app-e2eRelease.apk --deltaAppPath app-e2edeltaRelease.apk
artifacts:
- $WORKING_DIRECTORY
diff --git a/tests/e2e/config.dev.js b/tests/e2e/config.dev.ts
similarity index 77%
rename from tests/e2e/config.dev.js
rename to tests/e2e/config.dev.ts
index 0e5d3dc01a95..cdd7bce756c8 100644
--- a/tests/e2e/config.dev.js
+++ b/tests/e2e/config.dev.ts
@@ -1,7 +1,9 @@
+import type {Config} from './config.local';
+
const packageName = 'com.expensify.chat.dev';
const appPath = './android/app/build/outputs/apk/development/debug/app-development-debug.apk';
-export default {
+const config: Config = {
MAIN_APP_PACKAGE: packageName,
DELTA_APP_PACKAGE: packageName,
MAIN_APP_PATH: appPath,
@@ -9,3 +11,5 @@ export default {
RUNS: 8,
BOOT_COOL_DOWN: 5 * 1000,
};
+
+export default config;
diff --git a/tests/e2e/config.local.ts b/tests/e2e/config.local.ts
index 40f7afde3985..8e90da9d3423 100644
--- a/tests/e2e/config.local.ts
+++ b/tests/e2e/config.local.ts
@@ -10,3 +10,4 @@ const config: Config = {
};
export default config;
+export type {Config};
diff --git a/tests/e2e/server/index.ts b/tests/e2e/server/index.ts
index 7e7c34959655..16f23fd325cb 100644
--- a/tests/e2e/server/index.ts
+++ b/tests/e2e/server/index.ts
@@ -1,5 +1,5 @@
-import {createServer} from 'http';
import type {IncomingMessage, ServerResponse} from 'http';
+import {createServer} from 'http';
import type {NativeCommand, TestResult} from '@libs/E2E/client';
import type {NetworkCacheMap, TestConfig} from '@libs/E2E/types';
import config from '../config';
@@ -166,7 +166,7 @@ const createServerInstance = (): ServerInstance => {
return;
}
- const cachedData = networkCache[appInstanceId] || {};
+ const cachedData = networkCache[appInstanceId] ?? {};
res.end(JSON.stringify(cachedData));
});
diff --git a/tests/e2e/testRunner.js b/tests/e2e/testRunner.ts
similarity index 82%
rename from tests/e2e/testRunner.js
rename to tests/e2e/testRunner.ts
index 35d653a1bd79..5edc8c068229 100644
--- a/tests/e2e/testRunner.js
+++ b/tests/e2e/testRunner.ts
@@ -16,7 +16,7 @@
/* eslint-disable @lwc/lwc/no-async-await,no-restricted-syntax,no-await-in-loop */
import {execSync} from 'child_process';
import fs from 'fs';
-import _ from 'underscore';
+import type {TestConfig} from '@libs/E2E/types';
import compare from './compare/compare';
import defaultConfig from './config';
import createServerInstance from './server';
@@ -28,9 +28,11 @@ import * as Logger from './utils/logger';
import sleep from './utils/sleep';
import withFailTimeout from './utils/withFailTimeout';
+type Result = Record;
+
// VARIABLE CONFIGURATION
const args = process.argv.slice(2);
-const getArg = (argName) => {
+const getArg = (argName: string): string | undefined => {
const argIndex = args.indexOf(argName);
if (argIndex === -1) {
return undefined;
@@ -39,13 +41,13 @@ const getArg = (argName) => {
};
let config = defaultConfig;
-const setConfigPath = (configPathParam) => {
+const setConfigPath = (configPathParam: string | undefined) => {
let configPath = configPathParam;
- if (!configPath.startsWith('.')) {
+ if (!configPath?.startsWith('.')) {
configPath = `./${configPath}`;
}
const customConfig = require(configPath).default;
- config = _.extend(defaultConfig, customConfig);
+ config = Object.assign(defaultConfig, customConfig);
};
if (args.includes('--config')) {
@@ -54,8 +56,8 @@ if (args.includes('--config')) {
}
// Important: set app path only after correct config file has been loaded
-const mainAppPath = getArg('--mainAppPath') || config.MAIN_APP_PATH;
-const deltaAppPath = getArg('--deltaAppPath') || config.DELTA_APP_PATH;
+const mainAppPath = getArg('--mainAppPath') ?? config.MAIN_APP_PATH;
+const deltaAppPath = getArg('--deltaAppPath') ?? config.DELTA_APP_PATH;
// Check if files exists:
if (!fs.existsSync(mainAppPath)) {
throw new Error(`Main app path does not exist: ${mainAppPath}`);
@@ -76,8 +78,7 @@ try {
}
// START OF TEST CODE
-
-const runTests = async () => {
+const runTests = async (): Promise => {
Logger.info('Installing apps and reversing port');
await installApp(config.MAIN_APP_PACKAGE, mainAppPath);
await installApp(config.DELTA_APP_PACKAGE, deltaAppPath);
@@ -88,36 +89,38 @@ const runTests = async () => {
await server.start();
// Create a dict in which we will store the run durations for all tests
- const results = {};
+ const results: Record = {};
// Collect results while tests are being executed
server.addTestResultListener((testResult) => {
- if (testResult.error != null) {
+ if (testResult?.error != null) {
throw new Error(`Test '${testResult.name}' failed with error: ${testResult.error}`);
}
let result = 0;
- if ('duration' in testResult) {
+ if (testResult?.duration !== undefined) {
if (testResult.duration < 0) {
return;
}
result = testResult.duration;
}
- if ('renderCount' in testResult) {
+ if (testResult?.renderCount !== undefined) {
result = testResult.renderCount;
}
- Logger.log(`[LISTENER] Test '${testResult.name}' on '${testResult.branch}' measured ${result}`);
+ Logger.log(`[LISTENER] Test '${testResult?.name}' on '${testResult?.branch}' measured ${result}`);
- if (!results[testResult.branch]) {
+ if (testResult?.branch && !results[testResult.branch]) {
results[testResult.branch] = {};
}
- results[testResult.branch][testResult.name] = (results[testResult.branch][testResult.name] || []).concat(result);
+ if (testResult?.branch && testResult?.name) {
+ results[testResult.branch][testResult.name] = (results[testResult.branch][testResult.name] ?? []).concat(result);
+ }
});
// Function to run a single test iteration
- async function runTestIteration(appPackage, iterationText, launchArgs) {
+ async function runTestIteration(appPackage: string, iterationText: string, launchArgs: Record = {}): Promise {
Logger.info(iterationText);
// Making sure the app is really killed (e.g. if a prior test run crashed)
@@ -128,10 +131,9 @@ const runTests = async () => {
await launchApp('android', appPackage, config.ACTIVITY_PATH, launchArgs);
await withFailTimeout(
- new Promise((resolve) => {
- const cleanup = server.addTestDoneListener(() => {
+ new Promise((resolve) => {
+ server.addTestDoneListener(() => {
Logger.success(iterationText);
- cleanup();
resolve();
});
}),
@@ -143,9 +145,9 @@ const runTests = async () => {
}
// Run the tests
- const tests = _.values(config.TESTS_CONFIG);
+ const tests = Object.keys(config.TESTS_CONFIG);
for (let testIndex = 0; testIndex < tests.length; testIndex++) {
- const test = _.values(config.TESTS_CONFIG)[testIndex];
+ const test = Object.values(config.TESTS_CONFIG)[testIndex];
// check if we want to skip the test
if (args.includes('--includes')) {
@@ -164,7 +166,7 @@ const runTests = async () => {
Logger.info(`Cooling down for ${config.BOOT_COOL_DOWN / 1000}s`);
await sleep(config.BOOT_COOL_DOWN);
- server.setTestConfig(test);
+ server.setTestConfig(test as TestConfig);
const warmupText = `Warmup for test '${test.name}' [${testIndex + 1}/${tests.length}]`;
@@ -182,7 +184,7 @@ const runTests = async () => {
// We run each test multiple time to average out the results
for (let testIteration = 0; testIteration < config.RUNS; testIteration++) {
- const onError = (e) => {
+ const onError = (e: Error) => {
errorCountRef.errorCount += 1;
if (testIteration === 0 || errorCountRef.errorCount === errorCountRef.allowedExceptions) {
Logger.error("There was an error running the test and we've reached the maximum number of allowed exceptions. Stopping the test run.");
@@ -191,6 +193,7 @@ const runTests = async () => {
// maximum number of allowed exceptions, we should stop the test run.
throw e;
}
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Logger.warn(`There was an error running the test. Continuing the test run. Error: ${e}`);
};
@@ -208,7 +211,7 @@ const runTests = async () => {
// Run the test on the delta app:
await runTestIteration(config.DELTA_APP_PACKAGE, deltaIterationText, launchArgs);
} catch (e) {
- onError(e);
+ onError(e as Error);
}
}
}
@@ -228,7 +231,7 @@ const run = async () => {
process.exit(0);
} catch (e) {
- Logger.info('\n\nE2E test suite failed due to error:', e, '\nPrinting full logs:\n\n');
+ Logger.info('\n\nE2E test suite failed due to error:', e as string, '\nPrinting full logs:\n\n');
// Write logcat, meminfo, emulator info to file as well:
execSync(`adb logcat -d > ${config.OUTPUT_DIR}/logcat.txt`);
@@ -237,7 +240,7 @@ const run = async () => {
execSync(`cat ${config.LOG_FILE}`);
try {
- execSync(`cat ~/.android/avd/${process.env.AVD_NAME || 'test'}.avd/config.ini > ${config.OUTPUT_DIR}/emulator-config.ini`);
+ execSync(`cat ~/.android/avd/${process.env.AVD_NAME ?? 'test'}.avd/config.ini > ${config.OUTPUT_DIR}/emulator-config.ini`);
} catch (ignoredError) {
// the error is ignored, as the file might not exist if the test
// run wasn't started with an emulator
diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts
index a57eaf593cff..8566abb97c7f 100644
--- a/tests/perf-test/SidebarUtils.perf-test.ts
+++ b/tests/perf-test/SidebarUtils.perf-test.ts
@@ -2,15 +2,13 @@ import {rand} from '@ngneat/falso';
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import {measureFunction} from 'reassure';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
import SidebarUtils from '@libs/SidebarUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetails, ReportActions, TransactionViolation} from '@src/types/onyx';
+import type {PersonalDetails, TransactionViolation} from '@src/types/onyx';
import type Policy from '@src/types/onyx/Policy';
import type Report from '@src/types/onyx/Report';
import type ReportAction from '@src/types/onyx/ReportAction';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
import createCollection from '../utils/collections/createCollection';
import createPersonalDetails from '../utils/collections/personalDetails';
import createRandomPolicy from '../utils/collections/policies';
@@ -56,8 +54,8 @@ const mockedBetas = Object.values(CONST.BETAS);
const allReportActions = Object.fromEntries(
Object.keys(reportActions).map((key) => [
`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${key}`,
- {
- [reportActions[key].reportActionID]: {
+ [
+ {
errors: reportActions[key].errors ?? [],
message: [
{
@@ -68,24 +66,13 @@ const allReportActions = Object.fromEntries(
],
reportActionID: reportActions[key].reportActionID,
},
- },
+ ],
]),
-) as unknown as OnyxCollection;
+) as unknown as OnyxCollection;
const currentReportId = '1';
const transactionViolations = {} as OnyxCollection;
-const reportKeys = Object.keys(allReports);
-const reportIDsWithErrors = reportKeys.reduce((errorsMap, reportKey) => {
- const report = allReports[reportKey];
- const allReportsActions = allReportActions?.[reportKey.replace('report_', 'reportActions_')] ?? null;
- const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions) || {};
- if (isEmptyObject(errors)) {
- return errorsMap;
- }
- return {...errorsMap, [reportKey.replace('report_', '')]: errors};
-}, {});
-
describe('SidebarUtils', () => {
beforeAll(() => {
Onyx.init({
@@ -113,11 +100,11 @@ describe('SidebarUtils', () => {
await measureFunction(() =>
SidebarUtils.getOptionData({
report,
+ reportActions,
personalDetails,
preferredLocale,
policy,
parentReportAction,
- reportErrors: undefined,
hasViolations: false,
}),
);
@@ -126,36 +113,12 @@ describe('SidebarUtils', () => {
test('[SidebarUtils] getOrderedReportIDs on 15k reports for default priorityMode', async () => {
await waitForBatchedUpdates();
await measureFunction(() =>
- SidebarUtils.getOrderedReportIDs(
- currentReportId,
- allReports,
- mockedBetas,
- policies,
- CONST.PRIORITY_MODE.DEFAULT,
- allReportActions,
- transactionViolations,
- undefined,
- undefined,
- reportIDsWithErrors,
- ),
+ SidebarUtils.getOrderedReportIDs(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.DEFAULT, allReportActions, transactionViolations),
);
});
test('[SidebarUtils] getOrderedReportIDs on 15k reports for GSD priorityMode', async () => {
await waitForBatchedUpdates();
- await measureFunction(() =>
- SidebarUtils.getOrderedReportIDs(
- currentReportId,
- allReports,
- mockedBetas,
- policies,
- CONST.PRIORITY_MODE.GSD,
- allReportActions,
- transactionViolations,
- undefined,
- undefined,
- reportIDsWithErrors,
- ),
- );
+ await measureFunction(() => SidebarUtils.getOrderedReportIDs(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.GSD, allReportActions, transactionViolations));
});
});
diff --git a/tests/perf-test/setupAfterEnv.js b/tests/perf-test/setupAfterEnv.ts
similarity index 100%
rename from tests/perf-test/setupAfterEnv.js
rename to tests/perf-test/setupAfterEnv.ts
diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.tsx
similarity index 85%
rename from tests/ui/UnreadIndicatorsTest.js
rename to tests/ui/UnreadIndicatorsTest.tsx
index 9c2ff134f21a..cbfb0b66d493 100644
--- a/tests/ui/UnreadIndicatorsTest.js
+++ b/tests/ui/UnreadIndicatorsTest.tsx
@@ -1,26 +1,30 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import type * as NativeNavigation from '@react-navigation/native';
import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native';
import {addSeconds, format, subMinutes, subSeconds} from 'date-fns';
import {utcToZonedTime} from 'date-fns-tz';
-import lodashGet from 'lodash/get';
import React from 'react';
import {AppState, DeviceEventEmitter, Linking} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
+import type Animated from 'react-native-reanimated';
+import * as CollectionUtils from '@libs/CollectionUtils';
+import DateUtils from '@libs/DateUtils';
+import * as Localize from '@libs/Localize';
+import LocalNotification from '@libs/Notification/LocalNotification';
+import * as NumberUtils from '@libs/NumberUtils';
+import * as Pusher from '@libs/Pusher/pusher';
+import PusherConnectionManager from '@libs/PusherConnectionManager';
import FontUtils from '@styles/utils/FontUtils';
-import App from '../../src/App';
-import CONFIG from '../../src/CONFIG';
-import CONST from '../../src/CONST';
-import * as AppActions from '../../src/libs/actions/App';
-import * as Report from '../../src/libs/actions/Report';
-import * as User from '../../src/libs/actions/User';
-import * as CollectionUtils from '../../src/libs/CollectionUtils';
-import DateUtils from '../../src/libs/DateUtils';
-import * as Localize from '../../src/libs/Localize';
-import LocalNotification from '../../src/libs/Notification/LocalNotification';
-import * as NumberUtils from '../../src/libs/NumberUtils';
-import * as Pusher from '../../src/libs/Pusher/pusher';
-import PusherConnectionManager from '../../src/libs/PusherConnectionManager';
-import ONYXKEYS from '../../src/ONYXKEYS';
-import appSetup from '../../src/setup';
+import * as AppActions from '@userActions/App';
+import * as Report from '@userActions/Report';
+import * as User from '@userActions/User';
+import App from '@src/App';
+import CONFIG from '@src/CONFIG';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import appSetup from '@src/setup';
+import type {ReportAction, ReportActions} from '@src/types/onyx';
import PusherHelper from '../utils/PusherHelper';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
@@ -43,7 +47,7 @@ jest.mock('react-native/Libraries/LogBox/LogBox', () => ({
}));
jest.mock('react-native-reanimated', () => ({
- ...jest.requireActual('react-native-reanimated/mock'),
+ ...jest.requireActual('react-native-reanimated/mock'),
createAnimatedPropAdapter: jest.fn,
useReducedMotion: jest.fn,
}));
@@ -51,7 +55,12 @@ jest.mock('react-native-reanimated', () => ({
/**
* We need to keep track of the transitionEnd callback so we can trigger it in our tests
*/
-let transitionEndCB;
+let transitionEndCB: () => void;
+
+type ListenerMock = {
+ triggerTransitionEnd: () => void;
+ addListener: jest.Mock;
+};
/**
* This is a helper function to create a mock for the addListener function of the react-navigation library.
@@ -60,15 +69,15 @@ let transitionEndCB;
*
* P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope.
*
- * @returns {Object} An object with two functions: triggerTransitionEnd and addListener
+ * @returns An object with two functions: triggerTransitionEnd and addListener
*/
-const createAddListenerMock = () => {
- const transitionEndListeners = [];
+const createAddListenerMock = (): ListenerMock => {
+ const transitionEndListeners: Array<() => void> = [];
const triggerTransitionEnd = () => {
transitionEndListeners.forEach((transitionEndListener) => transitionEndListener());
};
- const addListener = jest.fn().mockImplementation((listener, callback) => {
+ const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback) => {
if (listener === 'transitionEnd') {
transitionEndListeners.push(callback);
}
@@ -85,14 +94,16 @@ jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
const {triggerTransitionEnd, addListener} = createAddListenerMock();
transitionEndCB = triggerTransitionEnd;
- const useNavigation = () => ({
- navigate: jest.fn(),
- ...actualNav.useNavigation,
- getState: () => ({
- routes: [],
- }),
- addListener,
- });
+
+ const useNavigation = () =>
+ ({
+ navigate: jest.fn(),
+ ...actualNav.useNavigation,
+ getState: () => ({
+ routes: [],
+ }),
+ addListener,
+ } as typeof NativeNavigation.useNavigation);
return {
...actualNav,
@@ -100,7 +111,7 @@ jest.mock('@react-navigation/native', () => {
getState: () => ({
routes: [],
}),
- };
+ } as typeof NativeNavigation;
});
beforeAll(() => {
@@ -109,6 +120,7 @@ beforeAll(() => {
// fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling
// behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to
// simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc.
+ // @ts-expect-error -- TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated
global.fetch = TestHelper.getGlobalFetchMock();
Linking.setInitialURL('https://new.expensify.com/');
@@ -125,7 +137,7 @@ beforeAll(() => {
function scrollUpToRevealNewMessagesBadge() {
const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages');
- fireEvent.scroll(screen.queryByLabelText(hintText), {
+ fireEvent.scroll(screen.getByLabelText(hintText), {
nativeEvent: {
contentOffset: {
y: 250,
@@ -144,43 +156,33 @@ function scrollUpToRevealNewMessagesBadge() {
});
}
-/**
- * @return {Boolean}
- */
-function isNewMessagesBadgeVisible() {
+function isNewMessagesBadgeVisible(): boolean {
const hintText = Localize.translateLocal('accessibilityHints.scrollToNewestMessages');
const badge = screen.queryByAccessibilityHint(hintText);
- return Math.round(badge.props.style.transform[0].translateY) === -40;
+ return Math.round(badge?.props.style.transform[0].translateY) === -40;
}
-/**
- * @return {Promise}
- */
-function navigateToSidebar() {
+function navigateToSidebar(): Promise {
const hintText = Localize.translateLocal('accessibilityHints.navigateToChatsList');
const reportHeaderBackButton = screen.queryByAccessibilityHint(hintText);
- fireEvent(reportHeaderBackButton, 'press');
+ if (reportHeaderBackButton) {
+ fireEvent(reportHeaderBackButton, 'press');
+ }
return waitForBatchedUpdates();
}
-/**
- * @param {Number} index
- * @return {Promise}
- */
-async function navigateToSidebarOption(index) {
+async function navigateToSidebarOption(index: number): Promise {
const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat');
const optionRows = screen.queryAllByAccessibilityHint(hintText);
fireEvent(optionRows[index], 'press');
await waitForBatchedUpdatesWithAct();
}
-/**
- * @return {Boolean}
- */
-function areYouOnChatListScreen() {
+function areYouOnChatListScreen(): boolean {
const hintText = Localize.translateLocal('sidebarScreen.listOfChats');
const sidebarLinks = screen.queryAllByLabelText(hintText);
- return !lodashGet(sidebarLinks, [0, 'props', 'accessibilityElementsHidden']);
+
+ return !sidebarLinks?.[0]?.props?.accessibilityElementsHidden;
}
const REPORT_ID = '1';
@@ -190,15 +192,13 @@ const USER_B_ACCOUNT_ID = 2;
const USER_B_EMAIL = 'user_b@test.com';
const USER_C_ACCOUNT_ID = 3;
const USER_C_EMAIL = 'user_c@test.com';
-let reportAction3CreatedDate;
-let reportAction9CreatedDate;
+let reportAction3CreatedDate: string;
+let reportAction9CreatedDate: string;
/**
* Sets up a test with a logged in user that has one unread chat from another user. Returns the test instance.
- *
- * @returns {Promise}
*/
-function signInAndGetAppWithUnreadChat() {
+function signInAndGetAppWithUnreadChat(): Promise {
// Render the App and sign in as a test user.
render();
return waitForBatchedUpdatesWithAct()
@@ -268,7 +268,7 @@ function signInAndGetAppWithUnreadChat() {
});
// We manually setting the sidebar as loaded since the onLayout event does not fire in tests
- AppActions.setSidebarLoaded(true);
+ AppActions.setSidebarLoaded();
return waitForBatchedUpdatesWithAct();
});
}
@@ -286,7 +286,7 @@ describe('Unread Indicators', () => {
signInAndGetAppWithUnreadChat()
.then(() => {
// Verify no notifications are created for these older messages
- expect(LocalNotification.showCommentNotification.mock.calls).toHaveLength(0);
+ expect((LocalNotification.showCommentNotification as jest.Mock).mock.calls).toHaveLength(0);
// Verify the sidebar links are rendered
const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats');
@@ -301,12 +301,12 @@ describe('Unread Indicators', () => {
// And that the text is bold
const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames');
const displayNameText = screen.queryByLabelText(displayNameHintText);
- expect(lodashGet(displayNameText, ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold);
+ expect(displayNameText?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold);
return navigateToSidebarOption(0);
})
.then(async () => {
- await act(() => transitionEndCB && transitionEndCB());
+ await act(() => transitionEndCB?.());
// That the report actions are visible along with the created action
const welcomeMessageHintText = Localize.translateLocal('accessibilityHints.chatWelcomeMessage');
@@ -320,7 +320,7 @@ describe('Unread Indicators', () => {
const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator');
const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText);
expect(unreadIndicator).toHaveLength(1);
- const reportActionID = lodashGet(unreadIndicator, [0, 'props', 'data-action-id']);
+ const reportActionID = unreadIndicator[0]?.props?.['data-action-id'];
expect(reportActionID).toBe('4');
// Scroll up and verify that the "New messages" badge appears
scrollUpToRevealNewMessagesBadge();
@@ -331,7 +331,7 @@ describe('Unread Indicators', () => {
// Navigate to the unread chat from the sidebar
.then(() => navigateToSidebarOption(0))
.then(async () => {
- await act(() => transitionEndCB && transitionEndCB());
+ await act(() => transitionEndCB?.());
// Verify the unread indicator is present
const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator');
const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText);
@@ -413,6 +413,7 @@ describe('Unread Indicators', () => {
reportActionID: commentReportActionID,
},
},
+ // @ts-expect-error -- it's necessary for the test
shouldNotify: true,
},
{
@@ -439,27 +440,27 @@ describe('Unread Indicators', () => {
const displayNameTexts = screen.queryAllByLabelText(displayNameHintTexts);
expect(displayNameTexts).toHaveLength(2);
const firstReportOption = displayNameTexts[0];
- expect(lodashGet(firstReportOption, ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold);
- expect(lodashGet(firstReportOption, ['props', 'children', 0])).toBe('C User');
+ expect(firstReportOption?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold);
+ expect(firstReportOption?.props?.children?.[0]).toBe('C User');
const secondReportOption = displayNameTexts[1];
- expect(lodashGet(secondReportOption, ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold);
- expect(lodashGet(secondReportOption, ['props', 'children', 0])).toBe('B User');
+ expect(secondReportOption?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold);
+ expect(secondReportOption?.props?.children?.[0]).toBe('B User');
// Tap the new report option and navigate back to the sidebar again via the back button
return navigateToSidebarOption(0);
})
.then(waitForBatchedUpdates)
.then(async () => {
- await act(() => transitionEndCB && transitionEndCB());
+ await act(() => transitionEndCB?.());
// Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread
const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames');
const displayNameTexts = screen.queryAllByLabelText(hintText);
expect(displayNameTexts).toHaveLength(2);
- expect(lodashGet(displayNameTexts[0], ['props', 'style', 'fontWeight'])).toBe(undefined);
- expect(lodashGet(displayNameTexts[0], ['props', 'children', 0])).toBe('C User');
- expect(lodashGet(displayNameTexts[1], ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold);
- expect(lodashGet(displayNameTexts[1], ['props', 'children', 0])).toBe('B User');
+ expect(displayNameTexts[0]?.props?.style?.fontWeight).toBe(undefined);
+ expect(displayNameTexts[0]?.props?.children?.[0]).toBe('C User');
+ expect(displayNameTexts[1]?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold);
+ expect(displayNameTexts[1]?.props?.children?.[0]).toBe('B User');
}));
xit('Manually marking a chat message as unread shows the new line indicator and updates the LHN', () =>
@@ -477,7 +478,7 @@ describe('Unread Indicators', () => {
const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator');
const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText);
expect(unreadIndicator).toHaveLength(1);
- const reportActionID = lodashGet(unreadIndicator, [0, 'props', 'data-action-id']);
+ const reportActionID = unreadIndicator[0]?.props?.['data-action-id'];
expect(reportActionID).toBe('3');
// Scroll up and verify the new messages badge appears
scrollUpToRevealNewMessagesBadge();
@@ -490,8 +491,8 @@ describe('Unread Indicators', () => {
const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames');
const displayNameTexts = screen.queryAllByLabelText(hintText);
expect(displayNameTexts).toHaveLength(1);
- expect(lodashGet(displayNameTexts[0], ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold);
- expect(lodashGet(displayNameTexts[0], ['props', 'children', 0])).toBe('B User');
+ expect(displayNameTexts[0]?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold);
+ expect(displayNameTexts[0]?.props?.children?.[0]).toBe('B User');
// Navigate to the report again and back to the sidebar
return navigateToSidebarOption(0);
@@ -502,8 +503,8 @@ describe('Unread Indicators', () => {
const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames');
const displayNameTexts = screen.queryAllByLabelText(hintText);
expect(displayNameTexts).toHaveLength(1);
- expect(lodashGet(displayNameTexts[0], ['props', 'style', 'fontWeight'])).toBe(undefined);
- expect(lodashGet(displayNameTexts[0], ['props', 'children', 0])).toBe('B User');
+ expect(displayNameTexts[0]?.props?.style?.fontWeight).toBe(undefined);
+ expect(displayNameTexts[0]?.props?.children?.[0]).toBe('B User');
// Navigate to the report again and verify the new line indicator is missing
return navigateToSidebarOption(0);
@@ -528,7 +529,7 @@ describe('Unread Indicators', () => {
return navigateToSidebarOption(0);
})
.then(async () => {
- await act(() => transitionEndCB && transitionEndCB());
+ await act(() => transitionEndCB?.());
const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator');
const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText);
expect(unreadIndicator).toHaveLength(1);
@@ -585,8 +586,8 @@ describe('Unread Indicators', () => {
}));
it('Displays the correct chat message preview in the LHN when a comment is added then deleted', () => {
- let reportActions;
- let lastReportAction;
+ let reportActions: OnyxEntry;
+ let lastReportAction: ReportAction | undefined;
Onyx.connect({
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
callback: (val) => (reportActions = val),
@@ -602,11 +603,11 @@ describe('Unread Indicators', () => {
})
.then(() => {
// Simulate the response from the server so that the comment can be deleted in this test
- lastReportAction = {...CollectionUtils.lastItem(reportActions)};
+ lastReportAction = reportActions ? CollectionUtils.lastItem(reportActions) : undefined;
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, {
- lastMessageText: lastReportAction.message[0].text,
- lastVisibleActionCreated: DateUtils.getDBTime(lastReportAction.timestamp),
- lastActorAccountID: lastReportAction.actorAccountID,
+ lastMessageText: lastReportAction?.message?.[0].text,
+ lastVisibleActionCreated: DateUtils.getDBTime(lastReportAction?.timestamp),
+ lastActorAccountID: lastReportAction?.actorAccountID,
reportID: REPORT_ID,
});
return waitForBatchedUpdates();
@@ -618,7 +619,9 @@ describe('Unread Indicators', () => {
expect(alternateText).toHaveLength(1);
expect(alternateText[0].props.children).toBe('Current User Comment 1');
- Report.deleteReportComment(REPORT_ID, lastReportAction);
+ if (lastReportAction) {
+ Report.deleteReportComment(REPORT_ID, lastReportAction);
+ }
return waitForBatchedUpdates();
})
.then(() => {
diff --git a/tests/unit/checkDeployBlockersTest.js b/tests/unit/checkDeployBlockersTest.ts
similarity index 63%
rename from tests/unit/checkDeployBlockersTest.js
rename to tests/unit/checkDeployBlockersTest.ts
index 354ab132f601..5a35fdf4f681 100644
--- a/tests/unit/checkDeployBlockersTest.js
+++ b/tests/unit/checkDeployBlockersTest.ts
@@ -2,12 +2,18 @@
* @jest-environment node
*/
import * as core from '@actions/core';
-import _ from 'underscore';
+import asMutable from '@src/types/utils/asMutable';
import run from '../../.github/actions/javascript/checkDeployBlockers/checkDeployBlockers';
import GithubUtils from '../../.github/libs/GithubUtils';
+type CommentData = {body: string};
+
+type Comment = {data?: CommentData[]};
+
+type PullRequest = {url: string; isQASuccess: boolean};
+
// Static mock function for core.getInput
-const mockGetInput = jest.fn().mockImplementation((arg) => {
+const mockGetInput = jest.fn().mockImplementation((arg: string): string | number | undefined => {
if (arg === 'GITHUB_TOKEN') {
return 'fake_token';
}
@@ -23,8 +29,8 @@ const mockListComments = jest.fn();
beforeAll(() => {
// Mock core module
- core.getInput = mockGetInput;
- core.setOutput = mockSetOutput;
+ asMutable(core).getInput = mockGetInput;
+ asMutable(core).setOutput = mockSetOutput;
// Mock octokit module
const moctokit = {
@@ -35,10 +41,12 @@ beforeAll(() => {
},
},
};
+
+ // @ts-expect-error TODO: Remove this once GithubUtils (https://github.com/Expensify/App/issues/25382) is migrated to TypeScript.
GithubUtils.internalOctokit = moctokit;
});
-let baseComments = [];
+let baseComments: Comment = {};
beforeEach(() => {
baseComments = {
data: [
@@ -65,11 +73,11 @@ afterAll(() => {
jest.clearAllMocks();
});
-function checkbox(isClosed) {
+function checkbox(isClosed: boolean): string {
return isClosed ? '[x]' : '[ ]';
}
-function mockIssue(prList, deployBlockerList) {
+function mockIssue(prList: PullRequest[], deployBlockerList?: PullRequest[]) {
return {
data: {
number: 1,
@@ -79,25 +87,27 @@ function mockIssue(prList, deployBlockerList) {
**Compare Changes:** https://github.com/Expensify/App/compare/production...staging
**This release contains changes from the following pull requests:**
-${_.map(
- prList,
- ({url, isQASuccess}) => `
+${prList
+ .map(
+ ({url, isQASuccess}) => `
- ${checkbox(isQASuccess)} ${url}
`,
-)}
+ )
+ .join('\n')}
${
- !_.isEmpty(deployBlockerList)
+ !deployBlockerList || deployBlockerList.length < 0
? `
**Deploy Blockers:**`
: ''
-}
-${_.map(
- deployBlockerList,
- ({url, isQASuccess}) => `
+}
+${deployBlockerList
+ ?.map(
+ ({url, isQASuccess}) => `
- ${checkbox(isQASuccess)} ${url}
`,
-)}
+ )
+ .join('\n')}
cc @Expensify/applauseleads
`,
},
@@ -108,51 +118,46 @@ describe('checkDeployBlockers', () => {
const allClearIssue = mockIssue([{url: 'https://github.com/Expensify/App/pull/6882', isQASuccess: true}]);
describe('checkDeployBlockers', () => {
- test('Test an issue with all checked items and :shipit:', () => {
+ test('Test an issue with all checked items and :shipit:', async () => {
mockGetIssue.mockResolvedValue(allClearIssue);
mockListComments.mockResolvedValue(baseComments);
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', false);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', false);
});
- test('Test an issue with all boxes checked but no :shipit:', () => {
+ test('Test an issue with all boxes checked but no :shipit:', async () => {
mockGetIssue.mockResolvedValue(allClearIssue);
const extraComments = {
- data: [...baseComments.data, {body: 'This issue either has unchecked QA steps or has not yet been stamped with a :shipit: comment. Reopening!'}],
+ data: [...(baseComments?.data ?? []), {body: 'This issue either has unchecked QA steps or has not yet been stamped with a :shipit: comment. Reopening!'}],
};
mockListComments.mockResolvedValue(extraComments);
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
});
- test('Test an issue with all boxes checked but no comments', () => {
+ test('Test an issue with all boxes checked but no comments', async () => {
mockGetIssue.mockResolvedValue(allClearIssue);
mockListComments.mockResolvedValue({data: []});
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
});
- test('Test an issue with all QA checked but not all deploy blockers', () => {
+ test('Test an issue with all QA checked but not all deploy blockers', async () => {
mockGetIssue.mockResolvedValue(
mockIssue([{url: 'https://github.com/Expensify/App/pull/6882', isQASuccess: true}], [{url: 'https://github.com/Expensify/App/pull/6883', isQASuccess: false}]),
);
mockListComments.mockResolvedValue(baseComments);
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
});
- test('Test an issue with all QA checked and all deploy blockers resolved', () => {
+ test('Test an issue with all QA checked and all deploy blockers resolved', async () => {
mockGetIssue.mockResolvedValue(
mockIssue([{url: 'https://github.com/Expensify/App/pull/6882', isQASuccess: true}], [{url: 'https://github.com/Expensify/App/pull/6883', isQASuccess: true}]),
);
mockListComments.mockResolvedValue(baseComments);
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', false);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', false);
});
});
});
diff --git a/tests/utils/getIsUsingFakeTimers.js b/tests/utils/getIsUsingFakeTimers.js
deleted file mode 100644
index 376312ac6c06..000000000000
--- a/tests/utils/getIsUsingFakeTimers.js
+++ /dev/null
@@ -1 +0,0 @@
-export default () => Boolean(global.setTimeout.mock || global.setTimeout.clock);
diff --git a/tests/utils/getIsUsingFakeTimers.ts b/tests/utils/getIsUsingFakeTimers.ts
new file mode 100644
index 000000000000..52138276928c
--- /dev/null
+++ b/tests/utils/getIsUsingFakeTimers.ts
@@ -0,0 +1,3 @@
+type SetTimeout = typeof global.setTimeout & jest.Mock & typeof jasmine;
+
+export default () => Boolean((global.setTimeout as SetTimeout).mock || (global.setTimeout as SetTimeout).clock);