diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml
index d118b3fee252..cb5dc6d28b32 100644
--- a/.github/workflows/deployBlocker.yml
+++ b/.github/workflows/deployBlocker.yml
@@ -22,6 +22,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
+ - name: Escape html characters in GH issue title
+ env:
+ GH_ISSUE_TITLE: ${{ github.event.issue.title }}
+ run: |
+ escaped_title=$(echo "$GH_ISSUE_TITLE" | sed -e 's/&/\&/g; s/\</g; s/>/\>/g; s/"/\"/g; s/'"'"'/\'/g; s/|/\|/g')
+ echo "GH_ISSUE_TITLE=$escaped_title" >> "$GITHUB_ENV"
+
- name: 'Post the issue in the #expensify-open-source slack room'
if: ${{ success() }}
uses: 8398a7/action-slack@v3
@@ -32,7 +39,7 @@ jobs:
channel: '#expensify-open-source',
attachments: [{
color: "#DB4545",
- text: 'đź’Ą We have found a New Expensify Deploy Blocker, if you have any idea which PR could be causing this, please comment in the issue: <${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>'.replace(/[&<>"'|]/g, function(m) { return {'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '|': '|'}[m]; }),
+ text: 'đź’Ą We have found a New Expensify Deploy Blocker, if you have any idea which PR could be causing this, please comment in the issue: <${{ github.event.issue.html_url }}|${{ env.GH_ISSUE_TITLE }}>'
}]
}
env:
diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml
index 016fe89ccfce..92b4e52c159c 100644
--- a/.github/workflows/e2ePerformanceTests.yml
+++ b/.github/workflows/e2ePerformanceTests.yml
@@ -211,7 +211,9 @@ jobs:
test_spec_file: tests/e2e/TestSpec.yml
test_spec_type: APPIUM_NODE_TEST_SPEC
remote_src: false
- file_artifacts: Customer Artifacts.zip
+ file_artifacts: |
+ Customer Artifacts.zip
+ Test spec output.txt
log_artifacts: debug.log
cleanup: true
timeout: 5400
@@ -220,6 +222,7 @@ jobs:
if: failure()
run: |
echo ${{ steps.schedule-awsdf-main.outputs.data }}
+ cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/Test spec output.txt"
unzip "Customer Artifacts.zip" -d mainResults
cat ./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/debug.log
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 291bd80816b9..bf27006e34a2 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -56,6 +56,12 @@ jobs:
- name: Setup Node
uses: ./.github/actions/composite/setupNode
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'oracle'
+ java-version: '17'
+
- name: Setup Ruby
uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011
with:
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 6f222398d04b..94a51a2d11bd 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -84,6 +84,12 @@ jobs:
- name: Setup Node
uses: ./.github/actions/composite/setupNode
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'oracle'
+ java-version: '17'
+
- name: Setup Ruby
uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011
with:
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1d6ed247e1b9..e015ebeb4d27 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -91,8 +91,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001040401
- versionName "1.4.4-1"
+ versionCode 1001040503
+ versionName "1.4.5-3"
}
flavorDimensions "default"
diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md
index d7a94c2a4337..32d3919efbe4 100644
--- a/contributingGuides/NAVIGATION.md
+++ b/contributingGuides/NAVIGATION.md
@@ -40,9 +40,22 @@ When creating RHP flows, you have to remember a couple things:
An example of adding `Settings_Workspaces` page:
-1. Add path to `ROUTES.js`: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/ROUTES.js#L36
+1. Add path to `ROUTES.ts`: https://github.com/Expensify/App/blob/main/src/ROUTES.ts
+
+```ts
+export const ROUTES = {
+ // static route
+ SETTINGS_WORKSPACES: 'settings/workspaces',
+ // dynamic route
+ SETTINGS_WORKSPACES: {
+ route: 'settings/:accountID',
+ getRoute: (accountID: number) => `settings/${accountID}` as const,
+ },
+};
+
+```
-2. Add `Settings_Workspaces` page to proper RHP flow in `linkingConfig.js`: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/libs/Navigation/linkingConfig.js#L40-L42
+2. Add `Settings_Workspaces` page to proper RHP flow in `linkingConfig.ts`: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/libs/Navigation/linkingConfig.js#L40-L42
3. Add your page to proper navigator (it should be aligned with where you've put it in the previous step) https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/libs/Navigation/AppNavigator/ModalStackNavigators.js#L334-L338
diff --git a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md
index 669d960275e6..25ccdefad261 100644
--- a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md
+++ b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md
@@ -24,30 +24,30 @@ After downloading the app, log into your new.expensify.com account (you’ll use
## How to send messages
-Click **+** then **Send message** in New Expensify
-Choose **Chat**
-Search for any name, email or phone number
-Select the individual to begin chatting
+1. Click **+** then **Send message** in New Expensify
+2. Choose **Chat**
+3. Search for any name, email or phone number
+4. Select the individual to begin chatting
## How to create a group
-Click **+**, then **Send message** in New Expensify
-Search for any name, email or phone number
-Click **Add to group**
-Group participants are listed with a green check
-Repeat steps 1-3 to add more participants to the group
-Click **Create chat** to start chatting
+1. Click **+**, then **Send message** in New Expensify
+2. Search for any name, email or phone number
+3. Click **Add to group**
+4. Group participants are listed with a green check
+5. Repeat steps 1-3 to add more participants to the group
+6. Click **Create chat** to start chatting
## How to create a room
-Click **+**, then **Send message** in New Expensify
-Click **Room**
-Enter a room name that doesn’t already exist on the intended Workspace
-Choose the Workspace you want to associate the room with.
-Choose the room’s visibility setting:
-Private: Only people explicitly invited can find the room*
-Restricted: Workspace members can find the room*
-Public: Anyone can find the room
+1. Click **+**, then **Send message** in New Expensify
+2. Click **Room**
+3. Enter a room name that doesn’t already exist on the intended Workspace
+4. Choose the Workspace you want to associate the room with.
+5. Choose the room’s visibility setting:
+6. Private: Only people explicitly invited can find the room*
+7. Restricted: Workspace members can find the room*
+8. Public: Anyone can find the room
*Anyone, including non-Workspace Members, can be invited to a Private or Restricted room.
@@ -56,26 +56,29 @@ Public: Anyone can find the room
You can invite people to a Group or Room by @mentioning them or from the Members pane.
## Mentions:
-Type **@** and start typing the person’s name or email address
-Choose one or more contacts
-Input message, if desired, then send
+
+1. Type **@** and start typing the person’s name or email address
+2. Choose one or more contacts
+3. Input message, if desired, then send
## Members pane invites:
-Click the **Room** or **Group** header
-Select **Members**
-Click **Invite**
-Find and select any contact/s you’d like to invite
-Click **Next**
-Write a custom invitation if you like
-Click **Invite**
+
+1. Click the **Room** or **Group** header
+2. Select **Members**
+3. Click **Invite**
+4. Find and select any contact/s you’d like to invite
+5. Click **Next**
+6. Write a custom invitation if you like
+7. Click **Invite**
## Members pane removals:
-Click the **Room** or **Group** header
-Select **Members**
-Find and select any contact/s you’d like to remove
-Click **Remove**
-Click **Remove members**
+
+1. Click the **Room** or **Group** header
+2. Select **Members**
+3. Find and select any contact/s you’d like to remove
+4. Click **Remove**
+5. Click **Remove members**
## How to format text
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 836abbcb500d..6112ea67761f 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.4
+ 1.4.5
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.4.1
+ 1.4.5.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 1fee9bb4d417..ff992cb90a6f 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.4
+ 1.4.5
CFBundleSignature
????
CFBundleVersion
- 1.4.4.1
+ 1.4.5.3
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index e47a3448f7f9..79f6e6298f6a 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -256,7 +256,7 @@ PODS:
- Onfido (~> 28.3.0)
- React
- OpenSSL-Universal (1.1.1100)
- - Plaid (4.1.0)
+ - Plaid (4.7.0)
- PromisesObjC (2.2.0)
- RCT-Folly (2021.07.22.00):
- boost
@@ -585,12 +585,12 @@ PODS:
- React-Core
- react-native-pager-view (6.2.0):
- React-Core
- - react-native-pdf (6.7.1):
+ - react-native-pdf (6.7.3):
- React-Core
- react-native-performance (5.1.0):
- React-Core
- - react-native-plaid-link-sdk (10.0.0):
- - Plaid (~> 4.1.0)
+ - react-native-plaid-link-sdk (10.8.0):
+ - Plaid (~> 4.7.0)
- React-Core
- react-native-quick-sqlite (8.0.0-beta.2):
- React
@@ -1207,7 +1207,7 @@ SPEC CHECKSUMS:
Onfido: c7d010d9793790d44a07799d9be25aa8e3814ee7
onfido-react-native-sdk: b346a620af5669f9fecb6dc3052314a35a94ad9f
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
- Plaid: 7d340abeadb46c7aa1a91f896c5b22395a31fcf2
+ Plaid: 431ef9be5314a1345efb451bc5e6b067bfb3b4c6
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: c0569ecc035894e4a68baecb30fe6a7ea6e399f9
@@ -1236,9 +1236,9 @@ SPEC CHECKSUMS:
react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
react-native-pager-view: 0ccb8bf60e2ebd38b1f3669fa3650ecce81db2df
- react-native-pdf: 7c0e91ada997bac8bac3bb5bea5b6b81f5a3caae
+ react-native-pdf: b4ca3d37a9a86d9165287741c8b2ef4d8940c00e
react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886
- react-native-plaid-link-sdk: 9eb0f71dad94b3bdde649c7a384cba93024af46c
+ react-native-plaid-link-sdk: df1618a85a615d62ff34e34b76abb7a56497fbc1
react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4
react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
diff --git a/package-lock.json b/package-lock.json
index b91075092a3a..abc7dfd877d0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.4-1",
+ "version": "1.4.5-3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.4-1",
+ "version": "1.4.5-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -93,11 +93,11 @@
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.118",
"react-native-pager-view": "^6.2.0",
- "react-native-pdf": "^6.7.1",
+ "react-native-pdf": "^6.7.3",
"react-native-performance": "^5.1.0",
"react-native-permissions": "^3.9.3",
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
- "react-native-plaid-link-sdk": "^10.0.0",
+ "react-native-plaid-link-sdk": "10.8.0",
"react-native-qrcode-svg": "^6.2.0",
"react-native-quick-sqlite": "^8.0.0-beta.2",
"react-native-reanimated": "3.5.4",
@@ -25876,10 +25876,9 @@
}
},
"node_modules/crypto-js": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz",
- "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==",
- "license": "MIT"
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/css-box-model": {
"version": "1.2.1",
@@ -44466,11 +44465,11 @@
}
},
"node_modules/react-native-pdf": {
- "version": "6.7.1",
- "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.1.tgz",
- "integrity": "sha512-zszQygtNBYoUfEtP/fV7zhzGeohDlUksh2p3OzshLrxdY9mw7Tm5VXAxYq4d8HsomRJUbFlJ7rHaTU9AQL800g==",
+ "version": "6.7.3",
+ "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.3.tgz",
+ "integrity": "sha512-bK1fVkj18kBA5YlRFNJ3/vJ1bEX3FDHyAPY6ArtIdVs+vv0HzcK5WH9LSd2bxUsEMIyY9CSjP4j8BcxNXTiQkQ==",
"dependencies": {
- "crypto-js": "^3.2.0",
+ "crypto-js": "4.2.0",
"deprecated-react-native-prop-types": "^2.3.0"
},
"peerDependencies": {
@@ -44526,8 +44525,12 @@
}
},
"node_modules/react-native-plaid-link-sdk": {
- "version": "10.0.0",
- "license": "MIT",
+ "version": "10.8.0",
+ "resolved": "https://registry.npmjs.org/react-native-plaid-link-sdk/-/react-native-plaid-link-sdk-10.8.0.tgz",
+ "integrity": "sha512-rhyI19SZdwKCsHtkJ0ZOCD/r0vNLS1vqUAS3HPPa97IIN6nS2ln9krLA7lFfMKtWxY5Z5d73SqTmqhd1qqdNuA==",
+ "dependencies": {
+ "react-native-plaid-link-sdk": "^10.4.0"
+ },
"peerDependencies": {
"react": "*",
"react-native": ">=0.66.0"
@@ -71498,9 +71501,9 @@
}
},
"crypto-js": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz",
- "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q=="
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"css-box-model": {
"version": "1.2.1",
@@ -84820,11 +84823,11 @@
"requires": {}
},
"react-native-pdf": {
- "version": "6.7.1",
- "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.1.tgz",
- "integrity": "sha512-zszQygtNBYoUfEtP/fV7zhzGeohDlUksh2p3OzshLrxdY9mw7Tm5VXAxYq4d8HsomRJUbFlJ7rHaTU9AQL800g==",
+ "version": "6.7.3",
+ "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.3.tgz",
+ "integrity": "sha512-bK1fVkj18kBA5YlRFNJ3/vJ1bEX3FDHyAPY6ArtIdVs+vv0HzcK5WH9LSd2bxUsEMIyY9CSjP4j8BcxNXTiQkQ==",
"requires": {
- "crypto-js": "^3.2.0",
+ "crypto-js": "4.2.0",
"deprecated-react-native-prop-types": "^2.3.0"
}
},
@@ -84856,8 +84859,12 @@
}
},
"react-native-plaid-link-sdk": {
- "version": "10.0.0",
- "requires": {}
+ "version": "10.8.0",
+ "resolved": "https://registry.npmjs.org/react-native-plaid-link-sdk/-/react-native-plaid-link-sdk-10.8.0.tgz",
+ "integrity": "sha512-rhyI19SZdwKCsHtkJ0ZOCD/r0vNLS1vqUAS3HPPa97IIN6nS2ln9krLA7lFfMKtWxY5Z5d73SqTmqhd1qqdNuA==",
+ "requires": {
+ "react-native-plaid-link-sdk": "^10.4.0"
+ }
},
"react-native-qrcode-svg": {
"version": "6.2.0",
diff --git a/package.json b/package.json
index 6c2bac926d6e..ebffa4a3aeae 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.4-1",
+ "version": "1.4.5-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.",
@@ -140,11 +140,11 @@
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.118",
"react-native-pager-view": "^6.2.0",
- "react-native-pdf": "^6.7.1",
+ "react-native-pdf": "^6.7.3",
"react-native-performance": "^5.1.0",
"react-native-permissions": "^3.9.3",
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
- "react-native-plaid-link-sdk": "^10.0.0",
+ "react-native-plaid-link-sdk": "10.8.0",
"react-native-qrcode-svg": "^6.2.0",
"react-native-quick-sqlite": "^8.0.0-beta.2",
"react-native-reanimated": "3.5.4",
diff --git a/scripts/start-android.sh b/scripts/start-android.sh
old mode 100644
new mode 100755
diff --git a/src/CONST.ts b/src/CONST.ts
index 9b284752d074..dd6eafc7f0e6 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -556,6 +556,7 @@ const CONST = {
UPDATE_REIMBURSEMENT_CHOICE: 'POLICYCHANGELOG_UPDATE_REIMBURSEMENT_CHOICE',
UPDATE_REPORT_FIELD: 'POLICYCHANGELOG_UPDATE_REPORT_FIELD',
UPDATE_TAG: 'POLICYCHANGELOG_UPDATE_TAG',
+ UPDATE_TAG_ENABLED: 'POLICYCHANGELOG_UPDATE_TAG_ENABLED',
UPDATE_TAG_LIST_NAME: 'POLICYCHANGELOG_UPDATE_TAG_LIST_NAME',
UPDATE_TAG_NAME: 'POLICYCHANGELOG_UPDATE_TAG_NAME',
UPDATE_TIME_ENABLED: 'POLICYCHANGELOG_UPDATE_TIME_ENABLED',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 26589a3db0e0..24b698c24619 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1,30 +1,28 @@
-import {ValueOf} from 'type-fest';
+import {IsEqual, ValueOf} from 'type-fest';
import CONST from './CONST';
-/**
- * This is a file containing constants for all the routes we want to be able to go to
- */
+// This is a file containing constants for all the routes we want to be able to go to
/**
* Builds a URL with an encoded URI component for the `backTo` param which can be added to the end of URLs
*/
-function getUrlWithBackToParam(url: string, backTo?: string): string {
- const backToParam = backTo ? `${url.includes('?') ? '&' : '?'}backTo=${encodeURIComponent(backTo)}` : '';
- return url + backToParam;
+function getUrlWithBackToParam(url: TUrl, backTo?: string): `${TUrl}` | `${TUrl}?backTo=${string}` | `${TUrl}&backTo=${string}` {
+ const backToParam = backTo ? (`${url.includes('?') ? '&' : '?'}backTo=${encodeURIComponent(backTo)}` as const) : '';
+ return `${url}${backToParam}` as const;
}
-export default {
+const ROUTES = {
HOME: '',
/** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */
CONCIERGE: 'concierge',
FLAG_COMMENT: {
route: 'flag/:reportID/:reportActionID',
- getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}`,
+ getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` as const,
},
SEARCH: 'search',
DETAILS: {
route: 'details',
- getRoute: (login: string) => `details?login=${encodeURIComponent(login)}`,
+ getRoute: (login: string) => `details?login=${encodeURIComponent(login)}` as const,
},
PROFILE: {
route: 'a/:accountID',
@@ -35,7 +33,7 @@ export default {
VALIDATE_LOGIN: 'v/:accountID/:validateCode',
GET_ASSISTANCE: {
route: 'get-assistance/:taskID',
- getRoute: (taskID: string) => `get-assistance/${taskID}`,
+ getRoute: (taskID: string) => `get-assistance/${taskID}` as const,
},
UNLINK_LOGIN: 'u/:accountID/:validateCode',
APPLE_SIGN_IN: 'sign-in-with-apple',
@@ -54,7 +52,7 @@ export default {
BANK_ACCOUNT_PERSONAL: 'bank-account/personal',
BANK_ACCOUNT_WITH_STEP_TO_OPEN: {
route: 'bank-account/:stepToOpen?',
- getRoute: (stepToOpen = '', policyID = '', backTo?: string): string => getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}`, backTo),
+ getRoute: (stepToOpen = '', policyID = '', backTo?: string) => getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}`, backTo),
},
SETTINGS: 'settings',
@@ -77,44 +75,44 @@ export default {
SETTINGS_WALLET: 'settings/wallet',
SETTINGS_WALLET_DOMAINCARD: {
route: '/settings/wallet/card/:domain',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}`,
+ getRoute: (domain: string) => `/settings/wallet/card/${domain}` as const,
},
SETTINGS_REPORT_FRAUD: {
route: '/settings/wallet/card/:domain/report-virtual-fraud',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-virtual-fraud`,
+ getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-virtual-fraud` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: {
route: '/settings/wallet/card/:domain/get-physical/name',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/name`,
+ getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/name` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE: {
route: '/settings/wallet/card/:domain/get-physical/phone',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/phone`,
+ getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/phone` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS: {
route: '/settings/wallet/card/:domain/get-physical/address',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/address`,
+ getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/address` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_CONFIRM: {
route: '/settings/wallet/card/:domain/get-physical/confirm',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/confirm`,
+ getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/confirm` as const,
},
SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card',
SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account',
SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments',
SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: {
route: 'settings/wallet/card/:domain/digital-details/update-address',
- getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address`,
+ getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address` as const,
},
SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance',
SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account',
SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: {
route: '/settings/wallet/card/:domain/report-card-lost-or-damaged',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-card-lost-or-damaged`,
+ getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-card-lost-or-damaged` as const,
},
SETTINGS_WALLET_CARD_ACTIVATE: {
route: 'settings/wallet/card/:domain/activate',
- getRoute: (domain: string) => `settings/wallet/card/${domain}/activate`,
+ getRoute: (domain: string) => `settings/wallet/card/${domain}/activate` as const,
},
SETTINGS_PERSONAL_DETAILS: 'settings/profile/personal-details',
SETTINGS_PERSONAL_DETAILS_LEGAL_NAME: 'settings/profile/personal-details/legal-name',
@@ -130,10 +128,13 @@ export default {
},
SETTINGS_CONTACT_METHOD_DETAILS: {
route: 'settings/profile/contact-methods/:contactMethod/details',
- getRoute: (contactMethod: string) => `settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`,
+ getRoute: (contactMethod: string) => `settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details` as const,
},
SETTINGS_NEW_CONTACT_METHOD: 'settings/profile/contact-methods/new',
- SETTINGS_2FA: 'settings/security/two-factor-auth',
+ SETTINGS_2FA: {
+ route: 'settings/security/two-factor-auth',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('settings/security/two-factor-auth', backTo),
+ },
SETTINGS_STATUS: 'settings/profile/status',
SETTINGS_STATUS_SET: 'settings/profile/status/set',
@@ -146,157 +147,158 @@ export default {
REPORT: 'r',
REPORT_WITH_ID: {
route: 'r/:reportID?/:reportActionID?',
- getRoute: (reportID: string) => `r/${reportID}`,
+ getRoute: (reportID: string) => `r/${reportID}` as const,
},
EDIT_REQUEST: {
route: 'r/:threadReportID/edit/:field',
- getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}`,
+ getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}` as const,
},
EDIT_CURRENCY_REQUEST: {
route: 'r/:threadReportID/edit/currency',
- getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}`,
+ getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}` as const,
},
REPORT_WITH_ID_DETAILS_SHARE_CODE: {
route: 'r/:reportID/details/shareCode',
- getRoute: (reportID: string) => `r/${reportID}/details/shareCode`,
+ getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const,
},
REPORT_ATTACHMENTS: {
route: 'r/:reportID/attachment',
- getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}`,
+ getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}` as const,
},
REPORT_PARTICIPANTS: {
route: 'r/:reportID/participants',
- getRoute: (reportID: string) => `r/${reportID}/participants`,
+ getRoute: (reportID: string) => `r/${reportID}/participants` as const,
},
REPORT_WITH_ID_DETAILS: {
route: 'r/:reportID/details',
- getRoute: (reportID: string) => `r/${reportID}/details`,
+ getRoute: (reportID: string) => `r/${reportID}/details` as const,
},
REPORT_SETTINGS: {
route: 'r/:reportID/settings',
- getRoute: (reportID: string) => `r/${reportID}/settings`,
+ getRoute: (reportID: string) => `r/${reportID}/settings` as const,
},
REPORT_SETTINGS_ROOM_NAME: {
route: 'r/:reportID/settings/room-name',
- getRoute: (reportID: string) => `r/${reportID}/settings/room-name`,
+ getRoute: (reportID: string) => `r/${reportID}/settings/room-name` as const,
},
REPORT_SETTINGS_NOTIFICATION_PREFERENCES: {
route: 'r/:reportID/settings/notification-preferences',
- getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences`,
+ getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences` as const,
},
REPORT_SETTINGS_WRITE_CAPABILITY: {
route: 'r/:reportID/settings/who-can-post',
- getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post`,
+ getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` as const,
},
REPORT_WELCOME_MESSAGE: {
route: 'r/:reportID/welcomeMessage',
- getRoute: (reportID: string) => `r/${reportID}/welcomeMessage`,
+ getRoute: (reportID: string) => `r/${reportID}/welcomeMessage` as const,
},
SPLIT_BILL_DETAILS: {
route: 'r/:reportID/split/:reportActionID',
- getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}`,
+ getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const,
},
EDIT_SPLIT_BILL: {
route: `r/:reportID/split/:reportActionID/edit/:field`,
- getRoute: (reportID: string, reportActionID: string, field: ValueOf) => `r/${reportID}/split/${reportActionID}/edit/${field}`,
+ getRoute: (reportID: string, reportActionID: string, field: ValueOf) => `r/${reportID}/split/${reportActionID}/edit/${field}` as const,
},
EDIT_SPLIT_BILL_CURRENCY: {
route: 'r/:reportID/split/:reportActionID/edit/currency',
- getRoute: (reportID: string, reportActionID: string, currency: string, backTo: string) => `r/${reportID}/split/${reportActionID}/edit/currency?currency=${currency}&backTo=${backTo}`,
+ getRoute: (reportID: string, reportActionID: string, currency: string, backTo: string) =>
+ `r/${reportID}/split/${reportActionID}/edit/currency?currency=${currency}&backTo=${backTo}` as const,
},
TASK_TITLE: {
route: 'r/:reportID/title',
- getRoute: (reportID: string) => `r/${reportID}/title`,
+ getRoute: (reportID: string) => `r/${reportID}/title` as const,
},
TASK_DESCRIPTION: {
route: 'r/:reportID/description',
- getRoute: (reportID: string) => `r/${reportID}/description`,
+ getRoute: (reportID: string) => `r/${reportID}/description` as const,
},
TASK_ASSIGNEE: {
route: 'r/:reportID/assignee',
- getRoute: (reportID: string) => `r/${reportID}/assignee`,
+ getRoute: (reportID: string) => `r/${reportID}/assignee` as const,
},
PRIVATE_NOTES_VIEW: {
route: 'r/:reportID/notes/:accountID',
- getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}`,
+ getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}` as const,
},
PRIVATE_NOTES_LIST: {
route: 'r/:reportID/notes',
- getRoute: (reportID: string) => `r/${reportID}/notes`,
+ getRoute: (reportID: string) => `r/${reportID}/notes` as const,
},
PRIVATE_NOTES_EDIT: {
route: 'r/:reportID/notes/:accountID/edit',
- getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit`,
+ getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit` as const,
},
ROOM_MEMBERS: {
route: 'r/:reportID/members',
- getRoute: (reportID: string) => `r/${reportID}/members`,
+ getRoute: (reportID: string) => `r/${reportID}/members` as const,
},
ROOM_INVITE: {
route: 'r/:reportID/invite',
- getRoute: (reportID: string) => `r/${reportID}/invite`,
+ getRoute: (reportID: string) => `r/${reportID}/invite` as const,
},
// To see the available iouType, please refer to CONST.IOU.TYPE
MONEY_REQUEST: {
route: ':iouType/new/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}` as const,
},
MONEY_REQUEST_AMOUNT: {
route: ':iouType/new/amount/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}` as const,
},
MONEY_REQUEST_PARTICIPANTS: {
route: ':iouType/new/participants/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}` as const,
},
MONEY_REQUEST_CONFIRMATION: {
route: ':iouType/new/confirmation/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}` as const,
},
MONEY_REQUEST_DATE: {
route: ':iouType/new/date/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}` as const,
},
MONEY_REQUEST_CURRENCY: {
route: ':iouType/new/currency/:reportID?',
- getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}`,
+ getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}` as const,
},
MONEY_REQUEST_DESCRIPTION: {
route: ':iouType/new/description/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}` as const,
},
MONEY_REQUEST_CATEGORY: {
route: ':iouType/new/category/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}` as const,
},
MONEY_REQUEST_TAG: {
route: ':iouType/new/tag/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}` as const,
},
MONEY_REQUEST_MERCHANT: {
route: ':iouType/new/merchant/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}` as const,
},
MONEY_REQUEST_WAYPOINT: {
route: ':iouType/new/waypoint/:waypointIndex',
- getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}`,
+ getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}` as const,
},
MONEY_REQUEST_RECEIPT: {
route: ':iouType/new/receipt/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const,
},
MONEY_REQUEST_DISTANCE: {
route: ':iouType/new/address/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const,
},
MONEY_REQUEST_EDIT_WAYPOINT: {
route: 'r/:threadReportID/edit/distance/:transactionID/waypoint/:waypointIndex',
- getRoute: (threadReportID: number, transactionID: string, waypointIndex: number) => `r/${threadReportID}/edit/distance/${transactionID}/waypoint/${waypointIndex}`,
+ getRoute: (threadReportID: number, transactionID: string, waypointIndex: number) => `r/${threadReportID}/edit/distance/${transactionID}/waypoint/${waypointIndex}` as const,
},
MONEY_REQUEST_DISTANCE_TAB: {
route: ':iouType/new/:reportID?/distance',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance`,
+ getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` as const,
},
MONEY_REQUEST_MANUAL_TAB: ':iouType/new/:reportID?/manual',
MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan',
@@ -321,63 +323,63 @@ export default {
ERECEIPT: {
route: 'eReceipt/:transactionID',
- getRoute: (transactionID: string) => `eReceipt/${transactionID}`,
+ getRoute: (transactionID: string) => `eReceipt/${transactionID}` as const,
},
WORKSPACE_NEW: 'workspace/new',
WORKSPACE_NEW_ROOM: 'workspace/new-room',
WORKSPACE_INITIAL: {
route: 'workspace/:policyID',
- getRoute: (policyID: string) => `workspace/${policyID}`,
+ getRoute: (policyID: string) => `workspace/${policyID}` as const,
},
WORKSPACE_INVITE: {
route: 'workspace/:policyID/invite',
- getRoute: (policyID: string) => `workspace/${policyID}/invite`,
+ getRoute: (policyID: string) => `workspace/${policyID}/invite` as const,
},
WORKSPACE_INVITE_MESSAGE: {
route: 'workspace/:policyID/invite-message',
- getRoute: (policyID: string) => `workspace/${policyID}/invite-message`,
+ getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const,
},
WORKSPACE_SETTINGS: {
route: 'workspace/:policyID/settings',
- getRoute: (policyID: string) => `workspace/${policyID}/settings`,
+ getRoute: (policyID: string) => `workspace/${policyID}/settings` as const,
},
WORKSPACE_SETTINGS_CURRENCY: {
route: 'workspace/:policyID/settings/currency',
- getRoute: (policyID: string) => `workspace/${policyID}/settings/currency`,
+ getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const,
},
WORKSPACE_CARD: {
route: 'workspace/:policyID/card',
- getRoute: (policyID: string) => `workspace/${policyID}/card`,
+ getRoute: (policyID: string) => `workspace/${policyID}/card` as const,
},
WORKSPACE_REIMBURSE: {
route: 'workspace/:policyID/reimburse',
- getRoute: (policyID: string) => `workspace/${policyID}/reimburse`,
+ getRoute: (policyID: string) => `workspace/${policyID}/reimburse` as const,
},
WORKSPACE_RATE_AND_UNIT: {
route: 'workspace/:policyID/rateandunit',
- getRoute: (policyID: string) => `workspace/${policyID}/rateandunit`,
+ getRoute: (policyID: string) => `workspace/${policyID}/rateandunit` as const,
},
WORKSPACE_BILLS: {
route: 'workspace/:policyID/bills',
- getRoute: (policyID: string) => `workspace/${policyID}/bills`,
+ getRoute: (policyID: string) => `workspace/${policyID}/bills` as const,
},
WORKSPACE_INVOICES: {
route: 'workspace/:policyID/invoices',
- getRoute: (policyID: string) => `workspace/${policyID}/invoices`,
+ getRoute: (policyID: string) => `workspace/${policyID}/invoices` as const,
},
WORKSPACE_TRAVEL: {
route: 'workspace/:policyID/travel',
- getRoute: (policyID: string) => `workspace/${policyID}/travel`,
+ getRoute: (policyID: string) => `workspace/${policyID}/travel` as const,
},
WORKSPACE_MEMBERS: {
route: 'workspace/:policyID/members',
- getRoute: (policyID: string) => `workspace/${policyID}/members`,
+ getRoute: (policyID: string) => `workspace/${policyID}/members` as const,
},
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
- getRoute: (contentType: string) => `referral/${contentType}`,
+ getRoute: (contentType: string) => `referral/${contentType}` as const,
},
// These are some one-off routes that will be removed once they're no longer needed (see GH issues for details)
@@ -385,3 +387,24 @@ export default {
SBE: 'sbe',
MONEY2020: 'money2020',
} as const;
+
+export default ROUTES;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type ExtractRouteName = TRoute extends {getRoute: (...args: any[]) => infer TRouteName} ? TRouteName : TRoute;
+
+type AllRoutes = {
+ [K in keyof typeof ROUTES]: ExtractRouteName<(typeof ROUTES)[K]>;
+}[keyof typeof ROUTES];
+
+type RouteIsPlainString = IsEqual;
+
+/**
+ * Represents all routes in the app as a union of literal strings.
+ *
+ * If this type resolves to `never`, it implies that one or more routes defined within `ROUTES` have not correctly used
+ * `as const` in their `getRoute` function return value.
+ */
+type Route = RouteIsPlainString extends true ? never : AllRoutes;
+
+export type {Route};
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index f957a1dbb25e..f4cbcf4f2564 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -9,13 +9,13 @@ const PROTECTED_SCREENS = {
REPORT_ATTACHMENTS: 'ReportAttachments',
} as const;
-export default {
+const SCREENS = {
...PROTECTED_SCREENS,
- LOADING: 'Loading',
REPORT: 'Report',
NOT_FOUND: 'not-found',
TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps',
VALIDATE_LOGIN: 'ValidateLogin',
+ UNLINK_LOGIN: 'UnlinkLogin',
SETTINGS: {
ROOT: 'Settings_Root',
PREFERENCES: 'Settings_Preferences',
@@ -40,4 +40,5 @@ export default {
SAML_SIGN_IN: 'SAMLSignIn',
} as const;
+export default SCREENS;
export {PROTECTED_SCREENS};
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js
index ec4ddd623929..0b23704b5b26 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.js
@@ -209,7 +209,7 @@ function AddPlaidBankAccount({
// Handle Plaid login errors (will potentially reset plaid token and item depending on the error)
if (event === 'ERROR') {
Log.hmmm('[PlaidLink] Error: ', metadata);
- if (bankAccountID && metadata.error_code) {
+ if (bankAccountID && metadata && metadata.error_code) {
BankAccounts.handlePlaidError(bankAccountID, metadata.error_code, metadata.error_message, metadata.request_id);
}
}
diff --git a/src/components/AddressForm.js b/src/components/AddressForm.js
index 19ab35f036c1..98650f94232b 100644
--- a/src/components/AddressForm.js
+++ b/src/components/AddressForm.js
@@ -7,7 +7,7 @@ import _ from 'underscore';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import * as ValidationUtils from '@libs/ValidationUtils';
-import styles from '@styles/styles';
+import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import AddressSearch from './AddressSearch';
import CountrySelector from './CountrySelector';
@@ -63,6 +63,7 @@ const defaultProps = {
};
function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldSaveDraft, state, street1, street2, submitButtonText, zip}) {
+ const styles = useThemeStyles();
const {translate} = useLocalize();
const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], '');
const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat});
@@ -122,7 +123,6 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
submitButtonText={submitButtonText}
enabledWhenOffline
>
-
void;
};
-function getAnimationStyle(direction: AnimationDirection) {
- let transitionValue;
+function AnimatedStep({onAnimationEnd, direction = CONST.ANIMATION_DIRECTION.IN, style, children}: AnimatedStepProps) {
+ const styles = useThemeStyles();
- if (direction === 'in') {
- transitionValue = CONST.ANIMATED_TRANSITION_FROM_VALUE;
- } else {
- transitionValue = -CONST.ANIMATED_TRANSITION_FROM_VALUE;
- }
- return styles.makeSlideInTranslation('translateX', transitionValue);
-}
+ const animationStyle = useMemo(() => {
+ const transitionValue = direction === 'in' ? CONST.ANIMATED_TRANSITION_FROM_VALUE : -CONST.ANIMATED_TRANSITION_FROM_VALUE;
+
+ return styles.makeSlideInTranslation('translateX', transitionValue);
+ }, [direction, styles]);
-function AnimatedStep({onAnimationEnd, direction = CONST.ANIMATION_DIRECTION.IN, style = [], children}: AnimatedStepProps) {
return (
{
@@ -39,7 +36,7 @@ function AnimatedStep({onAnimationEnd, direction = CONST.ANIMATION_DIRECTION.IN,
onAnimationEnd();
}}
duration={CONST.ANIMATED_TRANSITION}
- animation={getAnimationStyle(direction)}
+ animation={animationStyle}
useNativeDriver={useNativeDriver}
style={style}
>
diff --git a/src/components/ArchivedReportFooter.js b/src/components/ArchivedReportFooter.js
deleted file mode 100644
index b1fac827d273..000000000000
--- a/src/components/ArchivedReportFooter.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import _ from 'lodash';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React from 'react';
-import {withOnyx} from 'react-native-onyx';
-import compose from '@libs/compose';
-import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import personalDetailsPropType from '@pages/personalDetailsPropType';
-import reportPropTypes from '@pages/reportPropTypes';
-import useThemeStyles from '@styles/useThemeStyles';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import Banner from './Banner';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
-
-const propTypes = {
- /** The reason this report was archived */
- reportClosedAction: PropTypes.shape({
- /** Message attached to the report closed action */
- originalMessage: PropTypes.shape({
- /** The reason the report was closed */
- reason: PropTypes.string.isRequired,
-
- /** (For accountMerged reason only), the accountID of the previous owner of this report. */
- oldAccountID: PropTypes.number,
-
- /** (For accountMerged reason only), the accountID of the account the previous owner was merged into */
- newAccountID: PropTypes.number,
- }).isRequired,
- }),
-
- /** The archived report */
- report: reportPropTypes.isRequired,
-
- /** Personal details of all users */
- personalDetails: PropTypes.objectOf(personalDetailsPropType),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- reportClosedAction: {
- originalMessage: {
- reason: CONST.REPORT.ARCHIVE_REASON.DEFAULT,
- },
- },
- personalDetails: {},
-};
-
-function ArchivedReportFooter(props) {
- const styles = useThemeStyles();
- const archiveReason = lodashGet(props.reportClosedAction, 'originalMessage.reason', CONST.REPORT.ARCHIVE_REASON.DEFAULT);
- let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetails, [props.report.ownerAccountID, 'displayName']);
-
- let oldDisplayName;
- if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) {
- const newAccountID = props.reportClosedAction.originalMessage.newAccountID;
- const oldAccountID = props.reportClosedAction.originalMessage.oldAccountID;
- displayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetails, [newAccountID, 'displayName']);
- oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetails, [oldAccountID, 'displayName']);
- }
-
- const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT;
-
- let policyName = ReportUtils.getPolicyName(props.report);
-
- if (shouldRenderHTML) {
- oldDisplayName = _.escape(oldDisplayName);
- displayName = _.escape(displayName);
- policyName = _.escape(policyName);
- }
-
- return (
- ${displayName}`,
- oldDisplayName: `${oldDisplayName}`,
- policyName: `${policyName}`,
- })}
- shouldRenderHTML={shouldRenderHTML}
- shouldShowIcon
- />
- );
-}
-
-ArchivedReportFooter.propTypes = propTypes;
-ArchivedReportFooter.defaultProps = defaultProps;
-ArchivedReportFooter.displayName = 'ArchivedReportFooter';
-
-export default compose(
- withLocalize,
- withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- reportClosedAction: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
- canEvict: false,
- selector: ReportActionsUtils.getLastClosedReportAction,
- },
- }),
-)(ArchivedReportFooter);
diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx
new file mode 100644
index 000000000000..3187bf3604e8
--- /dev/null
+++ b/src/components/ArchivedReportFooter.tsx
@@ -0,0 +1,82 @@
+import lodashEscape from 'lodash/escape';
+import React from 'react';
+import {OnyxEntry, withOnyx} from 'react-native-onyx';
+import useLocalize from '@hooks/useLocalize';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import useThemeStyles from '@styles/useThemeStyles';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx';
+import Banner from './Banner';
+
+type ArchivedReportFooterOnyxProps = {
+ /** The reason this report was archived */
+ reportClosedAction: OnyxEntry;
+
+ /** Personal details of all users */
+ personalDetails: OnyxEntry>;
+};
+
+type ArchivedReportFooterProps = ArchivedReportFooterOnyxProps & {
+ /** The archived report */
+ report: Report;
+};
+
+function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}}: ArchivedReportFooterProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null;
+ const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT;
+ let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [report.ownerAccountID, 'displayName']);
+
+ let oldDisplayName: string | undefined;
+ if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) {
+ const newAccountID = originalMessage?.newAccountID;
+ const oldAccountID = originalMessage?.oldAccountID;
+ displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [newAccountID, 'displayName']);
+ oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [oldAccountID, 'displayName']);
+ }
+
+ const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT;
+
+ let policyName = ReportUtils.getPolicyName(report);
+
+ if (shouldRenderHTML) {
+ oldDisplayName = lodashEscape(oldDisplayName);
+ displayName = lodashEscape(displayName);
+ policyName = lodashEscape(policyName);
+ }
+
+ const text = shouldRenderHTML
+ ? translate(`reportArchiveReasons.${archiveReason}`, {
+ displayName: `${displayName}`,
+ oldDisplayName: `${oldDisplayName}`,
+ policyName: `${policyName}`,
+ })
+ : translate(`reportArchiveReasons.${archiveReason}`);
+
+ return (
+
+ );
+}
+
+ArchivedReportFooter.displayName = 'ArchivedReportFooter';
+
+export default withOnyx({
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ reportClosedAction: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ canEvict: false,
+ selector: ReportActionsUtils.getLastClosedReportAction,
+ },
+})(ArchivedReportFooter);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js
index 46afd23daa4c..b9dd65e2716b 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js
@@ -3,11 +3,12 @@ import {StyleSheet, View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import Animated, {useSharedValue} from 'react-native-reanimated';
import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
-import styles from '@styles/styles';
+import useThemeStyles from '@styles/useThemeStyles';
import BaseAttachmentViewPdf from './BaseAttachmentViewPdf';
import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes';
function AttachmentViewPdf(props) {
+ const styles = useThemeStyles();
const {onScaleChanged, ...restProps} = props;
const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);
const scaleRef = useSharedValue(1);
@@ -41,7 +42,7 @@ function AttachmentViewPdf(props) {
return (
diff --git a/src/components/CheckboxWithLabel.js b/src/components/CheckboxWithLabel.js
index 0f106b856522..92cd7ea38eea 100644
--- a/src/components/CheckboxWithLabel.js
+++ b/src/components/CheckboxWithLabel.js
@@ -34,7 +34,7 @@ const propTypes = {
isChecked: PropTypes.bool,
/** Called when the checkbox or label is pressed */
- onInputChange: PropTypes.func.isRequired,
+ onInputChange: PropTypes.func,
/** Container styles */
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
@@ -77,10 +77,11 @@ const defaultProps = {
errorText: '',
shouldSaveDraft: false,
isChecked: false,
- value: false,
+ value: undefined,
defaultValue: false,
forwardedRef: () => {},
accessibilityLabel: undefined,
+ onInputChange: () => {},
};
function CheckboxWithLabel(props) {
diff --git a/src/components/ConfirmedRoute.js b/src/components/ConfirmedRoute.js
index 656e419449b3..4c4c34851afb 100644
--- a/src/components/ConfirmedRoute.js
+++ b/src/components/ConfirmedRoute.js
@@ -1,13 +1,13 @@
import lodashGet from 'lodash/get';
import lodashIsNil from 'lodash/isNil';
import PropTypes from 'prop-types';
-import React, {useEffect} from 'react';
+import React, {useCallback, useEffect} from 'react';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import useNetwork from '@hooks/useNetwork';
import * as TransactionUtils from '@libs/TransactionUtils';
-import styles from '@styles/styles';
-import theme from '@styles/themes/default';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
import * as MapboxToken from '@userActions/MapboxToken';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -37,46 +37,53 @@ const defaultProps = {
},
};
-const getWaypointMarkers = (waypoints) => {
- const numberOfWaypoints = _.size(waypoints);
- const lastWaypointIndex = numberOfWaypoints - 1;
- return _.filter(
- _.map(waypoints, (waypoint, key) => {
- if (!waypoint || lodashIsNil(waypoint.lat) || lodashIsNil(waypoint.lng)) {
- return;
- }
-
- const index = TransactionUtils.getWaypointIndex(key);
- let MarkerComponent;
- if (index === 0) {
- MarkerComponent = Expensicons.DotIndicatorUnfilled;
- } else if (index === lastWaypointIndex) {
- MarkerComponent = Expensicons.Location;
- } else {
- MarkerComponent = Expensicons.DotIndicator;
- }
-
- return {
- id: `${waypoint.lng},${waypoint.lat},${index}`,
- coordinate: [waypoint.lng, waypoint.lat],
- markerComponent: () => (
-
- ),
- };
- }),
- (waypoint) => waypoint,
- );
-};
-
function ConfirmedRoute({mapboxAccessToken, transaction}) {
const {isOffline} = useNetwork();
const {route0: route} = transaction.routes || {};
const waypoints = lodashGet(transaction, 'comment.waypoints', {});
const coordinates = lodashGet(route, 'geometry.coordinates', []);
+ const styles = useThemeStyles();
+ const theme = useTheme();
+
+ const getWaypointMarkers = useCallback(
+ (waypointsData) => {
+ const numberOfWaypoints = _.size(waypointsData);
+ const lastWaypointIndex = numberOfWaypoints - 1;
+
+ return _.filter(
+ _.map(waypointsData, (waypoint, key) => {
+ if (!waypoint || lodashIsNil(waypoint.lat) || lodashIsNil(waypoint.lng)) {
+ return;
+ }
+
+ const index = TransactionUtils.getWaypointIndex(key);
+ let MarkerComponent;
+ if (index === 0) {
+ MarkerComponent = Expensicons.DotIndicatorUnfilled;
+ } else if (index === lastWaypointIndex) {
+ MarkerComponent = Expensicons.Location;
+ } else {
+ MarkerComponent = Expensicons.DotIndicator;
+ }
+
+ return {
+ id: `${waypoint.lng},${waypoint.lat},${index}`,
+ coordinate: [waypoint.lng, waypoint.lat],
+ markerComponent: () => (
+
+ ),
+ };
+ }),
+ (waypoint) => waypoint,
+ );
+ },
+ [theme],
+ );
+
const waypointMarkers = getWaypointMarkers(waypoints);
useEffect(() => {
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index d0a43badc5e3..a8f7e3172c81 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -98,7 +98,7 @@ function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini,
success={!isThrottledButtonActive}
description={description}
descriptionTextStyle={styles.breakAll}
- style={getContextMenuItemStyles(windowWidth)}
+ style={getContextMenuItemStyles(styles, windowWidth)}
isAnonymousAction={isAnonymousAction}
focused={isFocused}
interactive={isThrottledButtonActive}
diff --git a/src/components/CustomDevMenu/index.js b/src/components/CustomDevMenu/index.js
deleted file mode 100644
index b8944c185d13..000000000000
--- a/src/components/CustomDevMenu/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const CustomDevMenu = () => {};
-
-CustomDevMenu.displayName = 'CustomDevMenu';
-
-export default CustomDevMenu;
diff --git a/src/components/CustomDevMenu/index.native.js b/src/components/CustomDevMenu/index.native.js
deleted file mode 100644
index c8d0e1e099d4..000000000000
--- a/src/components/CustomDevMenu/index.native.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import {useEffect} from 'react';
-import DevMenu from 'react-native-dev-menu';
-import toggleTestToolsModal from '@userActions/TestTool';
-
-function CustomDevMenu() {
- useEffect(() => {
- DevMenu.addItem('Open Test Preferences', toggleTestToolsModal);
- }, []);
-
- return null;
-}
-
-CustomDevMenu.displayName = 'CustomDevMenu';
-
-export default CustomDevMenu;
diff --git a/src/components/CustomDevMenu/index.native.tsx b/src/components/CustomDevMenu/index.native.tsx
new file mode 100644
index 000000000000..d8a0ea987171
--- /dev/null
+++ b/src/components/CustomDevMenu/index.native.tsx
@@ -0,0 +1,18 @@
+import {useEffect} from 'react';
+import DevMenu from 'react-native-dev-menu';
+import toggleTestToolsModal from '@userActions/TestTool';
+import CustomDevMenuElement from './types';
+
+const CustomDevMenu: CustomDevMenuElement = Object.assign(
+ () => {
+ useEffect(() => {
+ DevMenu.addItem('Open Test Preferences', toggleTestToolsModal);
+ }, []);
+ return <>>;
+ },
+ {
+ displayName: 'CustomDevMenu',
+ },
+);
+
+export default CustomDevMenu;
diff --git a/src/components/CustomDevMenu/index.tsx b/src/components/CustomDevMenu/index.tsx
new file mode 100644
index 000000000000..c8eae861b676
--- /dev/null
+++ b/src/components/CustomDevMenu/index.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import CustomDevMenuElement from './types';
+
+const CustomDevMenu: CustomDevMenuElement = Object.assign(() => <>>, {displayName: 'CustomDevMenu'});
+
+export default CustomDevMenu;
diff --git a/src/components/CustomDevMenu/types.ts b/src/components/CustomDevMenu/types.ts
new file mode 100644
index 000000000000..bdfc800a17f0
--- /dev/null
+++ b/src/components/CustomDevMenu/types.ts
@@ -0,0 +1,8 @@
+import {ReactElement} from 'react';
+
+type CustomDevMenuElement = {
+ (): ReactElement;
+ displayName: string;
+};
+
+export default CustomDevMenuElement;
diff --git a/src/components/CustomStatusBar/index.android.js b/src/components/CustomStatusBar/index.android.tsx
similarity index 50%
rename from src/components/CustomStatusBar/index.android.js
rename to src/components/CustomStatusBar/index.android.tsx
index a7bf509114e6..81b4f1d25f67 100644
--- a/src/components/CustomStatusBar/index.android.js
+++ b/src/components/CustomStatusBar/index.android.tsx
@@ -1,10 +1,15 @@
/**
* On Android we setup the status bar in native code.
*/
+import type CustomStatusBarType from './types';
-export default function CustomStatusBar() {
+// eslint-disable-next-line react/function-component-definition
+const CustomStatusBar: CustomStatusBarType = () =>
// Prefer to not render the StatusBar component in Android as it can cause
// issues with edge to edge display. We setup the status bar appearance in
// MainActivity.java and styles.xml.
- return null;
-}
+ null;
+
+CustomStatusBar.displayName = 'CustomStatusBar';
+
+export default CustomStatusBar;
diff --git a/src/components/CustomStatusBar/index.js b/src/components/CustomStatusBar/index.tsx
similarity index 87%
rename from src/components/CustomStatusBar/index.js
rename to src/components/CustomStatusBar/index.tsx
index a724c71059ef..c5c013c2bef9 100644
--- a/src/components/CustomStatusBar/index.js
+++ b/src/components/CustomStatusBar/index.tsx
@@ -2,8 +2,10 @@ import React, {useEffect} from 'react';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import StatusBar from '@libs/StatusBar';
import useTheme from '@styles/themes/useTheme';
+import type CustomStatusBarType from './types';
-function CustomStatusBar() {
+// eslint-disable-next-line react/function-component-definition
+const CustomStatusBar: CustomStatusBarType = () => {
const theme = useTheme();
useEffect(() => {
Navigation.isNavigationReady().then(() => {
@@ -20,7 +22,7 @@ function CustomStatusBar() {
});
}, [theme.PAGE_BACKGROUND_COLORS, theme.appBG]);
return ;
-}
+};
CustomStatusBar.displayName = 'CustomStatusBar';
diff --git a/src/components/CustomStatusBar/types.ts b/src/components/CustomStatusBar/types.ts
new file mode 100644
index 000000000000..7fecd02beba0
--- /dev/null
+++ b/src/components/CustomStatusBar/types.ts
@@ -0,0 +1,6 @@
+type CustomStatusBar = {
+ (): React.ReactNode;
+ displayName: string;
+};
+
+export default CustomStatusBar;
diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx
index 475de82fac35..34bce2133a89 100644
--- a/src/components/FixedFooter.tsx
+++ b/src/components/FixedFooter.tsx
@@ -7,12 +7,12 @@ type FixedFooterProps = {
children: ReactNode;
/** Styles to be assigned to Container */
- style?: StyleProp;
+ style: Array>;
};
function FixedFooter({style = [], children}: FixedFooterProps) {
const styles = useThemeStyles();
- return {children};
+ return {children};
}
FixedFooter.displayName = 'FixedFooter';
diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js
index 638b6e5f8d19..4f7346a94a2d 100644
--- a/src/components/Form/FormWrapper.js
+++ b/src/components/Form/FormWrapper.js
@@ -5,7 +5,6 @@ import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import FormSubmit from '@components/FormSubmit';
-import refPropTypes from '@components/refPropTypes';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import ScrollViewWithContext from '@components/ScrollViewWithContext';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -65,7 +64,7 @@ const propTypes = {
errors: errorsPropType.isRequired,
- inputRefs: PropTypes.objectOf(refPropTypes).isRequired,
+ inputRefs: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object])).isRequired,
};
const defaultProps = {
diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.js
index 5fdbe205f87b..86850a8af96d 100644
--- a/src/components/GrowlNotification/index.js
+++ b/src/components/GrowlNotification/index.js
@@ -7,26 +7,11 @@ import * as Pressables from '@components/Pressable';
import Text from '@components/Text';
import * as Growl from '@libs/Growl';
import useNativeDriver from '@libs/useNativeDriver';
-import styles from '@styles/styles';
-import themeColors from '@styles/themes/default';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import GrowlNotificationContainer from './GrowlNotificationContainer';
-const types = {
- [CONST.GROWL.SUCCESS]: {
- icon: Expensicons.Checkmark,
- iconColor: themeColors.success,
- },
- [CONST.GROWL.ERROR]: {
- icon: Expensicons.Exclamation,
- iconColor: themeColors.danger,
- },
- [CONST.GROWL.WARNING]: {
- icon: Expensicons.Exclamation,
- iconColor: themeColors.warning,
- },
-};
-
const INACTIVE_POSITION_Y = -255;
const PressableWithoutFeedback = Pressables.PressableWithoutFeedback;
@@ -36,6 +21,23 @@ function GrowlNotification(_, ref) {
const [bodyText, setBodyText] = useState('');
const [type, setType] = useState('success');
const [duration, setDuration] = useState();
+ const styles = useThemeStyles();
+ const theme = useTheme();
+
+ const types = {
+ [CONST.GROWL.SUCCESS]: {
+ icon: Expensicons.Checkmark,
+ iconColor: theme.success,
+ },
+ [CONST.GROWL.ERROR]: {
+ icon: Expensicons.Exclamation,
+ iconColor: theme.danger,
+ },
+ [CONST.GROWL.WARNING]: {
+ icon: Expensicons.Exclamation,
+ iconColor: theme.warning,
+ },
+ };
/**
* Show the growl notification
diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
index c4eed0134008..8cddd3c017de 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
@@ -4,7 +4,7 @@ import {defaultHTMLElementModels, RenderHTMLConfigProvider, TRenderEngineProvide
import _ from 'underscore';
import convertToLTR from '@libs/convertToLTR';
import singleFontFamily from '@styles/fontFamily/singleFontFamily';
-import styles from '@styles/styles';
+import useThemeStyles from '@styles/useThemeStyles';
import * as HTMLEngineUtils from './htmlEngineUtils';
import htmlRenderers from './HTMLRenderers';
@@ -24,45 +24,49 @@ const defaultProps = {
enableExperimentalBRCollapsing: false,
};
-// Declare nonstandard tags and their content model here
-const customHTMLElementModels = {
- edited: defaultHTMLElementModels.span.extend({
- tagName: 'edited',
- }),
- 'alert-text': defaultHTMLElementModels.div.extend({
- tagName: 'alert-text',
- mixedUAStyles: {...styles.formError, ...styles.mb0},
- }),
- 'muted-text': defaultHTMLElementModels.div.extend({
- tagName: 'muted-text',
- mixedUAStyles: {...styles.colorMuted, ...styles.mb0},
- }),
- comment: defaultHTMLElementModels.div.extend({
- tagName: 'comment',
- mixedUAStyles: {whiteSpace: 'pre'},
- }),
- 'email-comment': defaultHTMLElementModels.div.extend({
- tagName: 'email-comment',
- mixedUAStyles: {whiteSpace: 'normal'},
- }),
- strong: defaultHTMLElementModels.span.extend({
- tagName: 'strong',
- mixedUAStyles: {whiteSpace: 'pre'},
- }),
- 'mention-user': defaultHTMLElementModels.span.extend({tagName: 'mention-user'}),
- 'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}),
-};
-
-const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]};
-
// We are using the explicit composite architecture for performance gains.
// Configuration for RenderHTML is handled in a top-level component providing
// context to RenderHTMLSource components. See https://git.io/JRcZb
// Beware that each prop should be referentialy stable between renders to avoid
// costly invalidations and commits.
function BaseHTMLEngineProvider(props) {
+ const styles = useThemeStyles();
+
+ // Declare nonstandard tags and their content model here
+ const customHTMLElementModels = useMemo(
+ () => ({
+ edited: defaultHTMLElementModels.span.extend({
+ tagName: 'edited',
+ }),
+ 'alert-text': defaultHTMLElementModels.div.extend({
+ tagName: 'alert-text',
+ mixedUAStyles: {...styles.formError, ...styles.mb0},
+ }),
+ 'muted-text': defaultHTMLElementModels.div.extend({
+ tagName: 'muted-text',
+ mixedUAStyles: {...styles.colorMuted, ...styles.mb0},
+ }),
+ comment: defaultHTMLElementModels.div.extend({
+ tagName: 'comment',
+ mixedUAStyles: {whiteSpace: 'pre'},
+ }),
+ 'email-comment': defaultHTMLElementModels.div.extend({
+ tagName: 'email-comment',
+ mixedUAStyles: {whiteSpace: 'normal'},
+ }),
+ strong: defaultHTMLElementModels.span.extend({
+ tagName: 'strong',
+ mixedUAStyles: {whiteSpace: 'pre'},
+ }),
+ 'mention-user': defaultHTMLElementModels.span.extend({tagName: 'mention-user'}),
+ 'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}),
+ }),
+ [styles.colorMuted, styles.formError, styles.mb0],
+ );
+
// We need to memoize this prop to make it referentially stable.
const defaultTextProps = useMemo(() => ({selectable: props.textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [props.textSelectable]);
+ const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]};
return (
func,
+ shouldNavigateToTopMostReport = false,
}) {
const styles = useThemeStyles();
const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState();
@@ -74,7 +75,12 @@ function HeaderWithBackButton({
if (isKeyboardShown) {
Keyboard.dismiss();
}
- onBackButtonPress();
+ const topmostReportId = Navigation.getTopmostReportId();
+ if (shouldNavigateToTopMostReport && topmostReportId) {
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(topmostReportId));
+ } else {
+ onBackButtonPress();
+ }
}}
style={[styles.touchableButtonImage]}
role="button"
diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx
index 022c740907ea..82a5045b7ad4 100644
--- a/src/components/Icon/index.tsx
+++ b/src/components/Icon/index.tsx
@@ -1,6 +1,6 @@
import React, {PureComponent} from 'react';
import {StyleProp, View, ViewStyle} from 'react-native';
-import styles from '@styles/styles';
+import withThemeStyles, {ThemeStylesProps} from '@components/withThemeStyles';
import * as StyleUtils from '@styles/StyleUtils';
import themeColors from '@styles/themes/default';
import variables from '@styles/variables';
@@ -41,7 +41,7 @@ type IconProps = {
/** Additional styles to add to the Icon */
additionalStyles?: StyleProp;
-};
+} & ThemeStylesProps;
// We must use a class component to create an animatable component with the Animated API
// eslint-disable-next-line react/prefer-stateless-function
@@ -61,13 +61,13 @@ class Icon extends PureComponent {
render() {
const width = this.props.small ? variables.iconSizeSmall : this.props.width;
const height = this.props.small ? variables.iconSizeSmall : this.props.height;
- const iconStyles = [StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, styles.pAbsolute, this.props.additionalStyles];
+ const iconStyles = [StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, this.props.themeStyles.pAbsolute, this.props.additionalStyles];
if (this.props.inline) {
return (
{
}
}
-export default Icon;
+export default withThemeStyles(Icon);
diff --git a/src/components/InlineSystemMessage.tsx b/src/components/InlineSystemMessage.tsx
index e9de0111cd23..6e6423a19a35 100644
--- a/src/components/InlineSystemMessage.tsx
+++ b/src/components/InlineSystemMessage.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import {View} from 'react-native';
-import styles from '@styles/styles';
-import theme from '@styles/themes/default';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import Text from './Text';
@@ -12,6 +12,9 @@ type InlineSystemMessageProps = {
};
function InlineSystemMessage({message = ''}: InlineSystemMessageProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+
if (!message) {
return null;
}
diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js
index 4e01ee4d7830..3b2de574ba17 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.js
+++ b/src/components/LHNOptionsList/OptionRowLHN.js
@@ -103,7 +103,7 @@ function OptionRowLHN(props) {
props.style,
);
const contentContainerStyles =
- props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, optionRowStyles.compactContentContainerStyles] : [styles.flex1];
+ props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, optionRowStyles.compactContentContainerStyles(styles)] : [styles.flex1];
const sidebarInnerRowStyle = StyleSheet.flatten(
props.viewMode === CONST.OPTION_MODE.COMPACT
? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter]
diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js
index 677fe1c38827..1f3dd061a4bd 100644
--- a/src/components/MagicCodeInput.js
+++ b/src/components/MagicCodeInput.js
@@ -1,8 +1,10 @@
import PropTypes from 'prop-types';
import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {StyleSheet, View} from 'react-native';
+import {TapGestureHandler} from 'react-native-gesture-handler';
import _ from 'underscore';
import useNetwork from '@hooks/useNetwork';
+import * as Browser from '@libs/Browser';
import * as ValidationUtils from '@libs/ValidationUtils';
import * as StyleUtils from '@styles/StyleUtils';
import useThemeStyles from '@styles/useThemeStyles';
@@ -13,6 +15,8 @@ import {withNetwork} from './OnyxProvider';
import Text from './Text';
import TextInput from './TextInput';
+const TEXT_INPUT_EMPTY_STATE = '';
+
const propTypes = {
/** Information about the network */
network: networkPropTypes.isRequired,
@@ -104,23 +108,53 @@ const getInputPlaceholderSlots = (length) => Array.from(Array(length).keys());
function MagicCodeInput(props) {
const styles = useThemeStyles();
- const inputRefs = useRef([]);
- const [input, setInput] = useState('');
+ const inputRefs = useRef();
+ const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE);
const [focusedIndex, setFocusedIndex] = useState(0);
const [editIndex, setEditIndex] = useState(0);
const [wasSubmitted, setWasSubmitted] = useState(false);
+ const shouldFocusLast = useRef(false);
+ const inputWidth = useRef(0);
+ const lastFocusedIndex = useRef(0);
+ const lastValue = useRef(TEXT_INPUT_EMPTY_STATE);
+
+ useEffect(() => {
+ lastValue.current = input.length;
+ }, [input]);
const blurMagicCodeInput = () => {
- inputRefs.current[editIndex].blur();
+ inputRefs.current.blur();
setFocusedIndex(undefined);
};
+ const focusMagicCodeInput = () => {
+ setFocusedIndex(0);
+ lastFocusedIndex.current = 0;
+ setEditIndex(0);
+ inputRefs.current.focus();
+ };
+
+ const setInputAndIndex = (index) => {
+ setInput(TEXT_INPUT_EMPTY_STATE);
+ setFocusedIndex(index);
+ setEditIndex(index);
+ };
+
useImperativeHandle(props.innerRef, () => ({
focus() {
- inputRefs.current[0].focus();
+ focusMagicCodeInput();
+ },
+ focusLastSelected() {
+ inputRefs.current.focus();
+ },
+ resetFocus() {
+ setInput(TEXT_INPUT_EMPTY_STATE);
+ focusMagicCodeInput();
},
clear() {
- inputRefs.current[0].focus();
+ lastFocusedIndex.current = 0;
+ setInputAndIndex(0);
+ inputRefs.current.focus();
props.onChangeText('');
},
blur() {
@@ -140,6 +174,7 @@ function MagicCodeInput(props) {
// on complete, it will call the onFulfill callback.
blurMagicCodeInput();
props.onFulfill(props.value);
+ lastValue.current = '';
};
useNetwork({onReconnect: validateAndSubmit});
@@ -154,17 +189,34 @@ function MagicCodeInput(props) {
}, [props.value, props.shouldSubmitOnComplete]);
/**
- * Callback for the onFocus event, updates the indexes
- * of the currently focused input.
+ * Focuses on the input when it is pressed.
*
* @param {Object} event
* @param {Number} index
*/
- const onFocus = (event, index) => {
+ const onFocus = (event) => {
+ if (shouldFocusLast.current) {
+ lastValue.current = TEXT_INPUT_EMPTY_STATE;
+ setInputAndIndex(lastFocusedIndex.current);
+ }
event.preventDefault();
- setInput('');
- setFocusedIndex(index);
- setEditIndex(index);
+ };
+
+ /**
+ * Callback for the onPress event, updates the indexes
+ * of the currently focused input.
+ *
+ * @param {Number} index
+ */
+ const onPress = (index) => {
+ shouldFocusLast.current = false;
+ // TapGestureHandler works differently on mobile web and native app
+ // On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually
+ if (!Browser.isMobileChrome() && !Browser.isMobileSafari()) {
+ inputRefs.current.focus();
+ }
+ setInputAndIndex(index);
+ lastFocusedIndex.current = index;
};
/**
@@ -181,9 +233,16 @@ function MagicCodeInput(props) {
return;
}
+ // Checks if one new character was added, or if the content was replaced
+ const hasToSlice = value.length - 1 === lastValue.current.length && value.slice(0, value.length - 1) === lastValue.current;
+
+ // Gets the new value added by the user
+ const addedValue = hasToSlice ? value.slice(lastValue.current.length, value.length) : value;
+
+ lastValue.current = value;
// Updates the focused input taking into consideration the last input
// edited and the number of digits added by the user.
- const numbersArr = value
+ const numbersArr = addedValue
.trim()
.split('')
.slice(0, props.maxLength - editIndex);
@@ -192,7 +251,7 @@ function MagicCodeInput(props) {
let numbers = decomposeString(props.value, props.maxLength);
numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, props.maxLength)];
- inputRefs.current[updatedFocusedIndex].focus();
+ setInputAndIndex(updatedFocusedIndex);
const finalInput = composeToString(numbers);
props.onChangeText(finalInput);
@@ -225,7 +284,7 @@ function MagicCodeInput(props) {
// If the currently focused index already has a value, it will delete
// that value but maintain the focus on the same input.
if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) {
- setInput('');
+ setInput(TEXT_INPUT_EMPTY_STATE);
numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, props.maxLength)];
setEditIndex(focusedIndex);
props.onChangeText(composeToString(numbers));
@@ -244,24 +303,31 @@ function MagicCodeInput(props) {
}
const newFocusedIndex = Math.max(0, focusedIndex - 1);
+
+ // Saves the input string so that it can compare to the change text
+ // event that will be triggered, this is a workaround for mobile that
+ // triggers the change text on the event after the key press.
+ setInputAndIndex(newFocusedIndex);
props.onChangeText(composeToString(numbers));
if (!_.isUndefined(newFocusedIndex)) {
- inputRefs.current[newFocusedIndex].focus();
+ inputRefs.current.focus();
}
}
if (keyValue === 'ArrowLeft' && !_.isUndefined(focusedIndex)) {
const newFocusedIndex = Math.max(0, focusedIndex - 1);
- inputRefs.current[newFocusedIndex].focus();
+ setInputAndIndex(newFocusedIndex);
+ inputRefs.current.focus();
} else if (keyValue === 'ArrowRight' && !_.isUndefined(focusedIndex)) {
const newFocusedIndex = Math.min(focusedIndex + 1, props.maxLength - 1);
- inputRefs.current[newFocusedIndex].focus();
+ setInputAndIndex(newFocusedIndex);
+ inputRefs.current.focus();
} else if (keyValue === 'Enter') {
// We should prevent users from submitting when it's offline.
if (props.network.isOffline) {
return;
}
- setInput('');
+ setInput(TEXT_INPUT_EMPTY_STATE);
props.onFulfill(props.value);
}
};
@@ -290,6 +356,49 @@ function MagicCodeInput(props) {
return (
<>
+ {
+ onPress(Math.floor(e.nativeEvent.x / (inputWidth.current / props.maxLength)));
+ }}
+ >
+ {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */}
+
+ {
+ inputWidth.current = e.nativeEvent.layout.width;
+ }}
+ ref={(ref) => (inputRefs.current = ref)}
+ autoFocus={props.autoFocus}
+ inputMode="numeric"
+ textContentType="oneTimeCode"
+ name={props.name}
+ maxLength={props.maxLength}
+ value={input}
+ hideFocusedState
+ autoComplete={input.length === 0 && props.autoComplete}
+ shouldDelayFocus={input.length === 0 && props.shouldDelayFocus}
+ keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
+ onChangeText={(value) => {
+ onChangeText(value);
+ }}
+ onKeyPress={onKeyPress}
+ onFocus={onFocus}
+ onBlur={() => {
+ shouldFocusLast.current = true;
+ lastFocusedIndex.current = focusedIndex;
+ setFocusedIndex(undefined);
+ }}
+ selectionColor="transparent"
+ inputStyle={[styles.inputTransparent]}
+ role={CONST.ACCESSIBILITY_ROLE.TEXT}
+ style={[styles.inputTransparent]}
+ textInputContainerStyles={[styles.borderNone]}
+ />
+
+
{_.map(getInputPlaceholderSlots(props.maxLength), (index) => (
{decomposeString(props.value, props.maxLength)[index] || ''}
- {/* Hide the input above the text. Cannot set opacity to 0 as it would break pasting on iOS Safari. */}
-
- {
- inputRefs.current[index] = ref;
- // Setting attribute type to "search" to prevent Password Manager from appearing in Mobile Chrome
- if (ref && ref.setAttribute) {
- ref.setAttribute('type', 'search');
- }
- }}
- disableKeyboard={props.isDisableKeyboard}
- autoFocus={index === 0 && props.autoFocus}
- shouldDelayFocus={index === 0 && props.shouldDelayFocus}
- inputMode={props.isDisableKeyboard ? 'none' : 'numeric'}
- textContentType="oneTimeCode"
- name={props.name}
- maxLength={props.maxLength}
- value={input}
- hideFocusedState
- autoComplete={index === 0 ? props.autoComplete : 'off'}
- onChangeText={(value) => {
- // Do not run when the event comes from an input that is
- // not currently being responsible for the input, this is
- // necessary to avoid calls when the input changes due to
- // deleted characters. Only happens in mobile.
- if (index !== editIndex || _.isUndefined(focusedIndex)) {
- return;
- }
- onChangeText(value);
- }}
- onKeyPress={onKeyPress}
- onFocus={(event) => onFocus(event, index)}
- // Manually set selectionColor to make caret transparent.
- // We cannot use caretHidden as it breaks the pasting function on Android.
- selectionColor="transparent"
- textInputContainerStyles={[styles.borderNone]}
- inputStyle={[styles.inputTransparent]}
- role={CONST.ACCESSIBILITY_ROLE.TEXT}
- />
-
))}
diff --git a/src/components/MapView/PendingMapView.tsx b/src/components/MapView/PendingMapView.tsx
index eed879596888..2acdb59d3782 100644
--- a/src/components/MapView/PendingMapView.tsx
+++ b/src/components/MapView/PendingMapView.tsx
@@ -4,13 +4,13 @@ import {View} from 'react-native';
import BlockingView from '@components/BlockingViews/BlockingView';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
-import styles from '@styles/styles';
+import useThemeStyles from '@styles/useThemeStyles';
import variables from '@styles/variables';
import {PendingMapViewProps} from './MapViewTypes';
function PendingMapView({title = '', subtitle = '', style}: PendingMapViewProps) {
const hasTextContent = !_.isEmpty(title) || !_.isEmpty(subtitle);
-
+ const styles = useThemeStyles();
return (
{hasTextContent ? (
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index 9883672976e8..e88be246336c 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -7,9 +7,9 @@ import ControlSelection from '@libs/ControlSelection';
import convertToLTR from '@libs/convertToLTR';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import getButtonState from '@libs/getButtonState';
-import styles from '@styles/styles';
import * as StyleUtils from '@styles/StyleUtils';
-import themeColors from '@styles/themes/default';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
import variables from '@styles/variables';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
@@ -38,11 +38,11 @@ const defaultProps = {
shouldShowHeaderTitle: false,
shouldParseTitle: false,
wrapperStyle: [],
- style: styles.popoverMenuItem,
+ style: undefined,
titleStyle: {},
shouldShowTitleIcon: false,
titleIcon: () => {},
- descriptionTextStyle: styles.breakWord,
+ descriptionTextStyle: undefined,
success: false,
icon: undefined,
secondaryIcon: undefined,
@@ -86,10 +86,13 @@ const defaultProps = {
};
const MenuItem = React.forwardRef((props, ref) => {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const style = props.style || styles.popoverMenuItem;
const {isSmallScreenWidth} = useWindowDimensions();
const [html, setHtml] = React.useState('');
- const isDeleted = _.contains(props.style, styles.offlineFeedback.deleted);
+ const isDeleted = _.contains(style, styles.offlineFeedback.deleted);
const descriptionVerticalMargin = props.shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1;
const titleTextStyle = StyleUtils.combineStyles(
[
@@ -109,7 +112,7 @@ const MenuItem = React.forwardRef((props, ref) => {
styles.textLabelSupporting,
props.icon && !_.isArray(props.icon) ? styles.ml3 : undefined,
props.title ? descriptionVerticalMargin : StyleUtils.getFontSizeStyle(variables.fontSizeNormal),
- props.descriptionTextStyle,
+ props.descriptionTextStyle || styles.breakWord,
isDeleted ? styles.offlineFeedback.deleted : undefined,
]);
@@ -176,7 +179,7 @@ const MenuItem = React.forwardRef((props, ref) => {
onPressOut={ControlSelection.unblock}
onSecondaryInteraction={props.onSecondaryInteraction}
style={({pressed}) => [
- props.style,
+ style,
!props.interactive && styles.cursorDefault,
StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true),
(isHovered || pressed) && props.hoverAndPressStyle,
@@ -206,9 +209,9 @@ const MenuItem = React.forwardRef((props, ref) => {
icons={props.icon}
size={props.avatarSize}
secondAvatarStyle={[
- StyleUtils.getBackgroundAndBorderStyle(themeColors.sidebar),
- pressed && props.interactive ? StyleUtils.getBackgroundAndBorderStyle(themeColors.buttonPressedBG) : undefined,
- isHovered && !pressed && props.interactive ? StyleUtils.getBackgroundAndBorderStyle(themeColors.border) : undefined,
+ StyleUtils.getBackgroundAndBorderStyle(theme.sidebar),
+ pressed && props.interactive ? StyleUtils.getBackgroundAndBorderStyle(theme.buttonPressedBG) : undefined,
+ isHovered && !pressed && props.interactive ? StyleUtils.getBackgroundAndBorderStyle(theme.border) : undefined,
]}
/>
)}
@@ -291,7 +294,7 @@ const MenuItem = React.forwardRef((props, ref) => {
)}
@@ -342,7 +345,7 @@ const MenuItem = React.forwardRef((props, ref) => {
{/* Since subtitle can be of type number, we should allow 0 to be shown */}
{(props.subtitle || props.subtitle === 0) && (
- {props.subtitle}
+ {props.subtitle}
)}
{!_.isEmpty(props.floatRightAvatars) && (
@@ -361,7 +364,7 @@ const MenuItem = React.forwardRef((props, ref) => {
)}
diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx
index febe18f30c7d..6f15612736ef 100644
--- a/src/components/MultipleAvatars.tsx
+++ b/src/components/MultipleAvatars.tsx
@@ -2,9 +2,9 @@ import React, {memo, useMemo} from 'react';
import {StyleProp, View, ViewStyle} from 'react-native';
import {ValueOf} from 'type-fest';
import {AvatarSource} from '@libs/UserUtils';
-import styles from '@styles/styles';
import * as StyleUtils from '@styles/StyleUtils';
-import themeColors from '@styles/themes/default';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {Icon} from '@src/types/onyx/OnyxCommon';
@@ -63,26 +63,11 @@ type AvatarSizeToStyles = typeof CONST.AVATAR_SIZE.SMALL | typeof CONST.AVATAR_S
type AvatarSizeToStylesMap = Record;
-const avatarSizeToStylesMap: AvatarSizeToStylesMap = {
- [CONST.AVATAR_SIZE.SMALL]: {
- singleAvatarStyle: styles.singleAvatarSmall,
- secondAvatarStyles: styles.secondAvatarSmall,
- },
- [CONST.AVATAR_SIZE.LARGE]: {
- singleAvatarStyle: styles.singleAvatarMedium,
- secondAvatarStyles: styles.secondAvatarMedium,
- },
- [CONST.AVATAR_SIZE.DEFAULT]: {
- singleAvatarStyle: styles.singleAvatar,
- secondAvatarStyles: styles.secondAvatar,
- },
-};
-
function MultipleAvatars({
fallbackIcon,
icons = [],
size = CONST.AVATAR_SIZE.DEFAULT,
- secondAvatarStyle = [StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)],
+ secondAvatarStyle: secondAvatarStyleProp,
shouldStackHorizontally = false,
shouldDisplayAvatarsInRows = false,
isHovered = false,
@@ -93,8 +78,31 @@ function MultipleAvatars({
shouldUseCardBackground = false,
maxAvatarsInRow = CONST.AVATAR_ROW_SIZE.DEFAULT,
}: MultipleAvatarsProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+
+ const avatarSizeToStylesMap: AvatarSizeToStylesMap = useMemo(
+ () => ({
+ [CONST.AVATAR_SIZE.SMALL]: {
+ singleAvatarStyle: styles.singleAvatarSmall,
+ secondAvatarStyles: styles.secondAvatarSmall,
+ },
+ [CONST.AVATAR_SIZE.LARGE]: {
+ singleAvatarStyle: styles.singleAvatarMedium,
+ secondAvatarStyles: styles.secondAvatarMedium,
+ },
+ [CONST.AVATAR_SIZE.DEFAULT]: {
+ singleAvatarStyle: styles.singleAvatar,
+ secondAvatarStyles: styles.secondAvatar,
+ },
+ }),
+ [styles],
+ );
+
+ const secondAvatarStyle = secondAvatarStyleProp ?? [StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)];
+
let avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction);
- const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size]);
+ const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size, avatarSizeToStylesMap]);
const tooltipTexts = useMemo(() => (shouldShowTooltip ? icons.map((icon) => icon.name) : ['']), [shouldShowTooltip, icons]);
const avatarSize = useMemo(() => {
@@ -143,7 +151,7 @@ function MultipleAvatars({
addMonths(new Date(this.state.currentDateView), 1);
- const hasAvailableDatesPrevMonth = startOfDay(new Date(this.props.minDate)) < endOfMonth(subMonths(new Date(this.state.currentDateView), 1));
+ const hasAvailableDatesNextMonth = startOfDay(new Date(this.props.maxDate)) > endOfMonth(new Date(this.state.currentDateView));
+ const hasAvailableDatesPrevMonth = endOfDay(new Date(this.props.minDate)) < startOfMonth(new Date(this.state.currentDateView));
return (
@@ -219,7 +219,7 @@ class CalendarPicker extends React.PureComponent {
const isBeforeMinDate = currentDate < startOfDay(new Date(this.props.minDate));
const isAfterMaxDate = currentDate > startOfDay(new Date(this.props.maxDate));
const isDisabled = !day || isBeforeMinDate || isAfterMaxDate;
- const isSelected = isSameDay(parseISO(this.props.value), new Date(currentYearView, currentMonthView, day));
+ const isSelected = !!day && isSameDay(parseISO(this.props.value), new Date(currentYearView, currentMonthView, day));
return (
{
- if (containerStyles.length) {
- return containerStyles;
- }
- return isSmallScreenWidth ? styles.offlineIndicatorMobile : styles.offlineIndicator;
-};
-
function OfflineIndicator(props) {
+ const styles = useThemeStyles();
+
+ const computedStyles = useMemo(() => {
+ if (props.containerStyles.length) {
+ return props.containerStyles;
+ }
+ return props.isSmallScreenWidth ? styles.offlineIndicatorMobile : styles.offlineIndicator;
+ }, [props.containerStyles, props.isSmallScreenWidth, styles.offlineIndicatorMobile, styles.offlineIndicator]);
+
if (!props.network.isOffline) {
return null;
}
return (
-
+
{
if (!React.isValidElement(child)) {
return child;
}
const props = {style: StyleUtils.combineStyles(child.props.style, styles.offlineFeedback.deleted, styles.userSelectNone)};
if (child.props.children) {
- props.children = applyStrikeThrough(child.props.children);
+ props.children = applyStrikeThrough(child.props.children, styles);
}
return React.cloneElement(child, props);
});
}
function OfflineWithFeedback(props) {
+ const styles = useThemeStyles();
const {isOffline} = useNetwork();
const hasErrors = !_.isEmpty(props.errors);
@@ -109,7 +111,7 @@ function OfflineWithFeedback(props) {
// Apply strikethrough to children if needed, but skip it if we are not going to render them
if (needsStrikeThrough && !hideChildren) {
- children = applyStrikeThrough(children);
+ children = applyStrikeThrough(children, styles);
}
return (
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index be89132a0731..ba6a3067284f 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -16,13 +16,13 @@ import Text from '@components/Text';
import TextInput from '@components/TextInput';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withNavigationFocus from '@components/withNavigationFocus';
+import withTheme, {withThemePropTypes} from '@components/withTheme';
+import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles';
import compose from '@libs/compose';
import getPlatform from '@libs/getPlatform';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import Navigation from '@libs/Navigation/Navigation';
import setSelection from '@libs/setSelection';
-import colors from '@styles/colors';
-import styles from '@styles/styles';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import {defaultProps as optionsSelectorDefaultProps, propTypes as optionsSelectorPropTypes} from './optionsSelectorPropTypes';
@@ -51,6 +51,8 @@ const propTypes = {
...optionsSelectorPropTypes,
...withLocalizePropTypes,
+ ...withThemeStylesPropTypes,
+ ...withThemePropTypes,
};
const defaultProps = {
@@ -59,7 +61,7 @@ const defaultProps = {
referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND,
safeAreaPaddingBottomStyle: {},
contentContainerStyles: [],
- listContainerStyles: [styles.flex1],
+ listContainerStyles: undefined,
listStyles: [],
...optionsSelectorDefaultProps,
};
@@ -460,6 +462,8 @@ class BaseOptionsSelector extends Component {
const defaultConfirmButtonText = _.isUndefined(this.props.confirmButtonText) ? this.props.translate('common.confirm') : this.props.confirmButtonText;
const shouldShowDefaultConfirmButton = !this.props.footerContent && defaultConfirmButtonText;
const safeAreaPaddingBottomStyle = shouldShowFooter ? undefined : this.props.safeAreaPaddingBottomStyle;
+ const listContainerStyles = this.props.listContainerStyles || [this.props.themeStyles.flex1];
+
const textInput = (
(this.textInput = el)}
@@ -516,7 +520,7 @@ class BaseOptionsSelector extends Component {
}}
contentContainerStyles={[safeAreaPaddingBottomStyle, ...this.props.contentContainerStyles]}
sectionHeaderStyle={this.props.sectionHeaderStyle}
- listContainerStyles={this.props.listContainerStyles}
+ listContainerStyles={listContainerStyles}
listStyles={this.props.listStyles}
isLoading={!this.props.shouldShowOptions}
showScrollIndicator={this.props.showScrollIndicator}
@@ -528,7 +532,7 @@ class BaseOptionsSelector extends Component {
renderFooterContent={() =>
shouldShowShowMoreButton && (
- {optionsList}
-
+
+ {optionsList}
+
+
{this.props.children}
{this.props.shouldShowTextInput && textInput}
@@ -556,18 +576,18 @@ class BaseOptionsSelector extends Component {
onFocusedIndexChanged={this.props.disableArrowKeysActions ? () => {} : this.updateFocusedIndex}
shouldResetIndexOnEndReached={false}
>
-
+
{/*
* The OptionsList component uses a SectionList which uses a VirtualizedList internally.
* VirtualizedList cannot be directly nested within ScrollViews of the same orientation.
* To work around this, we wrap the OptionsList component with a horizontal ScrollView.
*/}
{this.props.shouldTextInputAppearBelowOptions && this.props.shouldAllowScrollingChildren && (
-
+
{optionsAndInputsBelowThem}
@@ -578,13 +598,13 @@ class BaseOptionsSelector extends Component {
{!this.props.shouldTextInputAppearBelowOptions && (
<>
-
+
{this.props.children}
{this.props.shouldShowTextInput && textInput}
{Boolean(this.props.textInputAlert) && (
)}
@@ -594,20 +614,29 @@ class BaseOptionsSelector extends Component {
)}
{this.props.shouldShowReferralCTA && (
-
+
{
Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(this.props.referralContentType));
}}
- style={[styles.p5, styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10}]}
+ style={[
+ this.props.themeStyles.p5,
+ this.props.themeStyles.w100,
+ this.props.themeStyles.br2,
+ this.props.themeStyles.highlightBG,
+ this.props.themeStyles.flexRow,
+ this.props.themeStyles.justifyContentBetween,
+ this.props.themeStyles.alignItemsCenter,
+ {gap: 10},
+ ]}
accessibilityLabel="referral"
role={CONST.ACCESSIBILITY_ROLE.BUTTON}
>
{this.props.translate(`referralProgram.${this.props.referralContentType}.buttonText1`)}
{this.props.translate(`referralProgram.${this.props.referralContentType}.buttonText2`)}
@@ -626,7 +655,7 @@ class BaseOptionsSelector extends Component {
{shouldShowDefaultConfirmButton && (
- {shouldShowMerchant && (
+ {shouldShowMerchant && !props.isBillSplit && (
{hasPendingWaypoints ? requestMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd')) : requestMerchant}
@@ -334,7 +333,9 @@ function MoneyRequestPreview(props) {
{!isCurrentUserManager && props.shouldShowPendingConversionMessage && (
{props.translate('iou.pendingConversionMessage')}
)}
- {shouldShowDescription && {description}}
+ {(shouldShowDescription || (shouldShowMerchant && props.isBillSplit)) && (
+ {shouldShowDescription ? description : requestMerchant}
+ )}
{props.isBillSplit && !_.isEmpty(participantAccountIDs) && requestAmount > 0 && (
diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js
index f9173c15da7d..16bf9ba0761c 100644
--- a/src/components/ScreenWrapper/index.js
+++ b/src/components/ScreenWrapper/index.js
@@ -16,7 +16,7 @@ import useKeyboardState from '@hooks/useKeyboardState';
import useNetwork from '@hooks/useNetwork';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Browser from '@libs/Browser';
-import styles from '@styles/styles';
+import useThemeStyles from '@styles/useThemeStyles';
import toggleTestToolsModal from '@userActions/TestTool';
import CONST from '@src/CONST';
import {defaultProps, propTypes} from './propTypes';
@@ -44,6 +44,7 @@ const ScreenWrapper = React.forwardRef(
) => {
const {windowHeight, isSmallScreenWidth} = useWindowDimensions();
const {initialHeight} = useInitialDimensions();
+ const styles = useThemeStyles();
const keyboardState = useKeyboardState();
const {isDevelopment} = useEnvironment();
const {isOffline} = useNetwork();
@@ -59,14 +60,14 @@ const ScreenWrapper = React.forwardRef(
const panResponder = useRef(
PanResponder.create({
- onStartShouldSetPanResponderCapture: (e, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS,
+ onStartShouldSetPanResponderCapture: (_e, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS,
onPanResponderRelease: toggleTestToolsModal,
}),
).current;
const keyboardDissmissPanResponder = useRef(
PanResponder.create({
- onMoveShouldSetPanResponderCapture: (e, gestureState) => {
+ onMoveShouldSetPanResponderCapture: (_e, gestureState) => {
const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && isKeyboardShown && Browser.isMobile();
diff --git a/src/components/SelectCircle.tsx b/src/components/SelectCircle.tsx
index cf8ee6af975d..7a7e23388722 100644
--- a/src/components/SelectCircle.tsx
+++ b/src/components/SelectCircle.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import {StyleProp, View, ViewStyle} from 'react-native';
-import globalStyles from '@styles/styles';
-import themeColors from '@styles/themes/default';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
@@ -10,16 +10,19 @@ type SelectCircleProps = {
isChecked: boolean;
/** Additional styles to pass to SelectCircle */
- styles?: StyleProp;
+ selectCircleStyles?: StyleProp;
};
-function SelectCircle({isChecked = false, styles}: SelectCircleProps) {
+function SelectCircle({isChecked = false, selectCircleStyles}: SelectCircleProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+
return (
-
+
{isChecked && (
)}
diff --git a/src/components/SingleChoiceQuestion.tsx b/src/components/SingleChoiceQuestion.tsx
deleted file mode 100644
index 07d4dfe817dd..000000000000
--- a/src/components/SingleChoiceQuestion.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, {ForwardedRef, forwardRef} from 'react';
-import {Text as RNText} from 'react-native';
-import useThemeStyles from '@styles/useThemeStyles';
-import FormHelpMessage from './FormHelpMessage';
-import RadioButtons, {Choice} from './RadioButtons';
-import Text from './Text';
-
-type SingleChoiceQuestionProps = {
- prompt: string;
- errorText?: string | string[];
- possibleAnswers: Choice[];
- currentQuestionIndex: number;
- onInputChange: (value: string) => void;
-};
-
-function SingleChoiceQuestion({prompt, errorText, possibleAnswers, currentQuestionIndex, onInputChange}: SingleChoiceQuestionProps, ref: ForwardedRef) {
- const styles = useThemeStyles();
-
- return (
- <>
-
- {prompt}
-
-
-
- >
- );
-}
-
-SingleChoiceQuestion.displayName = 'SingleChoiceQuestion';
-
-export default forwardRef(SingleChoiceQuestion);
diff --git a/src/components/SingleOptionSelector.js b/src/components/SingleOptionSelector.js
index 967ae5443a81..9567ad2331ef 100644
--- a/src/components/SingleOptionSelector.js
+++ b/src/components/SingleOptionSelector.js
@@ -51,7 +51,7 @@ function SingleOptionSelector({options, selectedOptionKey, onSelectOption, trans
>
{translate(option.label)}
diff --git a/src/components/TabSelector/TabSelector.js b/src/components/TabSelector/TabSelector.js
index b6ee359dc4ca..cdec2a7e91e1 100644
--- a/src/components/TabSelector/TabSelector.js
+++ b/src/components/TabSelector/TabSelector.js
@@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
-import React, {useMemo, useState} from 'react';
+import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import * as Expensicons from '@components/Icon/Expensicons';
import useLocalize from '@hooks/useLocalize';
-import styles from '@styles/styles';
-import themeColors from '@styles/themes/default';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import TabSelectorItem from './TabSelectorItem';
@@ -68,24 +68,28 @@ const getOpacity = (position, routesLength, tabIndex, active, affectedTabs) => {
return activeValue;
};
-const getBackgroundColor = (position, routesLength, tabIndex, affectedTabs) => {
- if (routesLength > 1) {
- const inputRange = Array.from({length: routesLength}, (v, i) => i);
-
- return position.interpolate({
- inputRange,
- outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? themeColors.border : themeColors.appBG)),
- });
- }
- return themeColors.border;
-};
-
function TabSelector({state, navigation, onTabPress, position}) {
const {translate} = useLocalize();
-
+ const styles = useThemeStyles();
+ const theme = useTheme();
const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: state.routes.length}, (v, i) => i), [state.routes.length]);
const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs);
+ const getBackgroundColor = useCallback(
+ (routesLength, tabIndex, affectedTabs) => {
+ if (routesLength > 1) {
+ const inputRange = Array.from({length: routesLength}, (v, i) => i);
+
+ return position.interpolate({
+ inputRange,
+ outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)),
+ });
+ }
+ return theme.border;
+ },
+ [theme, position],
+ );
+
React.useEffect(() => {
// It is required to wait transition end to reset affectedAnimatedTabs because tabs style is still animating during transition.
setTimeout(() => {
diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx
index 22da02159073..3ce9eeae37b5 100644
--- a/src/components/withCurrentReportID.tsx
+++ b/src/components/withCurrentReportID.tsx
@@ -8,6 +8,7 @@ type CurrentReportIDContextValue = {
updateCurrentReportID: (state: NavigationState) => void;
currentReportID: string;
};
+
type CurrentReportIDContextProviderProps = {
/** Actual content wrapped by this component */
children: React.ReactNode;
diff --git a/src/components/withNavigation.tsx b/src/components/withNavigation.tsx
index 0834eabc2adb..88788edafb79 100644
--- a/src/components/withNavigation.tsx
+++ b/src/components/withNavigation.tsx
@@ -1,9 +1,10 @@
import {NavigationProp, useNavigation} from '@react-navigation/native';
import React, {ComponentType, ForwardedRef, RefAttributes} from 'react';
import getComponentDisplayName from '@libs/getComponentDisplayName';
+import {RootStackParamList} from '@libs/Navigation/types';
type WithNavigationProps = {
- navigation: NavigationProp;
+ navigation: NavigationProp;
};
export default function withNavigation(
diff --git a/src/components/withThemeStyles.tsx b/src/components/withThemeStyles.tsx
index d95122c3e2ba..8ea55c5fbef8 100644
--- a/src/components/withThemeStyles.tsx
+++ b/src/components/withThemeStyles.tsx
@@ -29,4 +29,4 @@ export default function withThemeStyles(
return forwardRef(WithThemeStyles);
}
-export {withThemeStylesPropTypes};
+export {withThemeStylesPropTypes, type ThemeStylesProps};
diff --git a/src/hooks/useFlipper/index.js b/src/hooks/useFlipper/index.js
deleted file mode 100644
index 2d1ec238274a..000000000000
--- a/src/hooks/useFlipper/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default () => {};
diff --git a/src/hooks/useFlipper/index.native.js b/src/hooks/useFlipper/index.native.js
deleted file mode 100644
index 90779d5b8a14..000000000000
--- a/src/hooks/useFlipper/index.native.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import {useFlipper} from '@react-navigation/devtools';
-
-export default useFlipper;
diff --git a/src/hooks/useFlipper/index.native.ts b/src/hooks/useFlipper/index.native.ts
new file mode 100644
index 000000000000..df1aa3bf513b
--- /dev/null
+++ b/src/hooks/useFlipper/index.native.ts
@@ -0,0 +1,6 @@
+import {useFlipper as useFlipperRN} from '@react-navigation/devtools';
+import UseFlipper from './types';
+
+const useFlipper: UseFlipper = useFlipperRN;
+
+export default useFlipper;
diff --git a/src/hooks/useFlipper/index.ts b/src/hooks/useFlipper/index.ts
new file mode 100644
index 000000000000..26d4c9659ad8
--- /dev/null
+++ b/src/hooks/useFlipper/index.ts
@@ -0,0 +1,5 @@
+import UseFlipper from './types';
+
+const useFlipper: UseFlipper = () => {};
+
+export default useFlipper;
diff --git a/src/hooks/useFlipper/types.ts b/src/hooks/useFlipper/types.ts
new file mode 100644
index 000000000000..a995414e5dd1
--- /dev/null
+++ b/src/hooks/useFlipper/types.ts
@@ -0,0 +1,6 @@
+import {NavigationContainerRefWithCurrent} from '@react-navigation/core';
+import {RootStackParamList} from '@libs/Navigation/types';
+
+type UseFlipper = (ref: NavigationContainerRefWithCurrent) => void;
+
+export default UseFlipper;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index d661ee1ad97b..ea0444342ce8 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1191,7 +1191,7 @@ export default {
noBankAccountAvailable: 'Sorry, no bank account is available',
noBankAccountSelected: 'Please choose an account',
taxID: 'Please enter a valid tax ID number',
- website: 'Please enter a valid website',
+ website: 'Please enter a valid website. The website should be in lowercase.',
zipCode: `Incorrect zip code format. Acceptable format: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`,
phoneNumber: 'Please enter a valid phone number',
companyName: 'Please enter a valid legal business name',
@@ -1952,7 +1952,7 @@ export default {
buttonText1: 'Request money, ',
buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`,
header: `Request money, get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
- body1: `Request money from a new Expensify account. Get $${CONST.REFERRAL_PROGRAM.REVENUE} once they start an annual subscription with two or more active members and make the first two payments toward their Expensify bill.`,
+ body1: `Request money from a new Expensify account. Get $${CONST.REFERRAL_PROGRAM.REVENUE} once they start an annual subscription with two or more active members and make the first two payments toward their Expensify bill.`,
},
[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: {
buttonText1: 'Send money, ',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 6ea01dc4bd14..a69edd7355ad 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1208,7 +1208,7 @@ export default {
noBankAccountAvailable: 'Lo sentimos, no hay ninguna cuenta bancaria disponible',
noBankAccountSelected: 'Por favor, elige una cuenta bancaria',
taxID: 'Por favor, introduce un número de identificación fiscal válido',
- website: 'Por favor, introduce un sitio web válido',
+ website: 'Por favor, introduce un sitio web válido. El sitio web debe estar en minúsculas.',
zipCode: `Formato de cĂłdigo postal incorrecto. Formato aceptable: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`,
phoneNumber: 'Por favor, introduce un teléfono válido',
companyName: 'Por favor, introduce un nombre comercial legal válido',
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index 0dc483aff50e..b0d426c9774a 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -37,6 +37,14 @@ function isExpensifyCard(cardID: number) {
return card.bank === CONST.EXPENSIFY_CARD.BANK;
}
+/**
+ * @param cardID
+ * @returns boolean if the cardID is in the cardList from ONYX. Includes Expensify Cards.
+ */
+function isCorporateCard(cardID: number) {
+ return !!allCards[cardID];
+}
+
/**
* @param cardID
* @returns string in format % - %.
@@ -99,4 +107,4 @@ function findPhysicalCard(cards: Card[]) {
return cards.find((card) => !card.isVirtual);
}
-export {isExpensifyCard, getDomainCards, getMonthFromExpirationDateString, getYearFromExpirationDateString, maskCard, getCardDescription, findPhysicalCard};
+export {isExpensifyCard, isCorporateCard, getDomainCards, getMonthFromExpirationDateString, getYearFromExpirationDateString, maskCard, getCardDescription, findPhysicalCard};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index aedb2fa8d741..4c610bc12099 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -144,7 +144,7 @@ const defaultProps = {
function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, session, lastOpenedPublicRoomID, demoInfo}) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
- const screenOptions = getRootNavigatorScreenOptions(isSmallScreenWidth);
+ const screenOptions = getRootNavigatorScreenOptions(isSmallScreenWidth, styles);
const isInitialRender = useRef(true);
if (isInitialRender.current) {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index 01573cb434b4..be803e62a98b 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -1,15 +1,9 @@
import {CardStyleInterpolators, createStackNavigator} from '@react-navigation/stack';
-import React from 'react';
+import React, {useMemo} from 'react';
import _ from 'underscore';
-import styles from '@styles/styles';
+import useThemeStyles from '@styles/useThemeStyles';
import SCREENS from '@src/SCREENS';
-const defaultSubRouteOptions = {
- cardStyle: styles.navigationScreenCardStyle,
- headerShown: false,
- cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
-};
-
/**
* Create a modal stack navigator with an array of sub-screens.
*
@@ -20,6 +14,17 @@ function createModalStackNavigator(screens) {
const ModalStackNavigator = createStackNavigator();
function ModalStack() {
+ const styles = useThemeStyles();
+
+ const defaultSubRouteOptions = useMemo(
+ () => ({
+ cardStyle: styles.navigationScreenCardStyle,
+ headerShown: false,
+ cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
+ }),
+ [styles],
+ );
+
return (
{_.map(screens, (getComponent, name) => (
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js
index a1646011e560..d23b03c8c73e 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js
@@ -2,7 +2,7 @@ import {createStackNavigator} from '@react-navigation/stack';
import React from 'react';
import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper';
import getCurrentUrl from '@libs/Navigation/currentUrl';
-import styles from '@styles/styles';
+import useThemeStyles from '@styles/useThemeStyles';
import SCREENS from '@src/SCREENS';
const Stack = createStackNavigator();
@@ -11,6 +11,7 @@ const url = getCurrentUrl();
const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined;
function BaseCentralPaneNavigator() {
+ const styles = useThemeStyles();
return (
RHPScreenOptions(styles), [styles]);
return (
{!isSmallScreenWidth && }
-
+
diff --git a/src/libs/Navigation/AppNavigator/RHPScreenOptions.js b/src/libs/Navigation/AppNavigator/RHPScreenOptions.js
index 6adf5bd2b507..02354b90591f 100644
--- a/src/libs/Navigation/AppNavigator/RHPScreenOptions.js
+++ b/src/libs/Navigation/AppNavigator/RHPScreenOptions.js
@@ -1,12 +1,17 @@
import {CardStyleInterpolators} from '@react-navigation/stack';
-import styles from '@styles/styles';
-const RHPScreenOptions = {
+/**
+ * RHP stack navigator screen options generator function
+ * @function
+ * @param {Object} styles - The styles object
+ * @returns {Object} - The screen options object
+ */
+const RHPScreenOptions = (styles) => ({
headerShown: false,
animationEnabled: true,
gestureDirection: 'horizontal',
cardStyle: styles.navigationScreenCardStyle,
cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
-};
+});
export default RHPScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js
index 04784fb9d0e1..44fa7b6c0b09 100644
--- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js
+++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js
@@ -1,5 +1,4 @@
import getNavigationModalCardStyle from '@styles/getNavigationModalCardStyles';
-import styles from '@styles/styles';
import variables from '@styles/variables';
import CONFIG from '@src/CONFIG';
import modalCardStyleInterpolator from './modalCardStyleInterpolator';
@@ -12,7 +11,7 @@ const commonScreenOptions = {
animationTypeForReplace: 'push',
};
-export default (isSmallScreenWidth) => ({
+export default (isSmallScreenWidth, styles) => ({
rightModalNavigator: {
...commonScreenOptions,
cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
diff --git a/src/libs/Navigation/FreezeWrapper.js b/src/libs/Navigation/FreezeWrapper.tsx
similarity index 65%
rename from src/libs/Navigation/FreezeWrapper.js
rename to src/libs/Navigation/FreezeWrapper.tsx
index 16a353ebddea..df3f117c9a2e 100644
--- a/src/libs/Navigation/FreezeWrapper.js
+++ b/src/libs/Navigation/FreezeWrapper.tsx
@@ -1,31 +1,24 @@
import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native';
-import lodashFindIndex from 'lodash/findIndex';
-import PropTypes from 'prop-types';
import React, {useEffect, useRef, useState} from 'react';
import {Freeze} from 'react-freeze';
import {InteractionManager} from 'react-native';
+import ChildrenProps from '@src/types/utils/ChildrenProps';
-const propTypes = {
+type FreezeWrapperProps = ChildrenProps & {
/** Prop to disable freeze */
- keepVisible: PropTypes.bool,
- /** Children to wrap in FreezeWrapper. */
- children: PropTypes.node.isRequired,
+ keepVisible?: boolean;
};
-const defaultProps = {
- keepVisible: false,
-};
-
-function FreezeWrapper(props) {
+function FreezeWrapper({keepVisible = false, children}: FreezeWrapperProps) {
const [isScreenBlurred, setIsScreenBlurred] = useState(false);
// we need to know the screen index to determine if the screen can be frozen
- const screenIndexRef = useRef(null);
+ const screenIndexRef = useRef(null);
const isFocused = useIsFocused();
const navigation = useNavigation();
const currentRoute = useRoute();
useEffect(() => {
- const index = lodashFindIndex(navigation.getState().routes, (route) => route.key === currentRoute.key);
+ const index = navigation.getState().routes.findIndex((route) => route.key === currentRoute.key);
screenIndexRef.current = index;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -35,7 +28,7 @@ function FreezeWrapper(props) {
// if the screen is more than 1 screen away from the current screen, freeze it,
// 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 - screenIndexRef.current > 1) {
+ if (navigation.getState().index - (screenIndexRef.current ?? 0) > 1) {
InteractionManager.runAfterInteractions(() => setIsScreenBlurred(true));
} else {
setIsScreenBlurred(false);
@@ -44,11 +37,9 @@ function FreezeWrapper(props) {
return () => unsubscribe();
}, [isFocused, isScreenBlurred, navigation]);
- return {props.children};
+ return {children};
}
-FreezeWrapper.propTypes = propTypes;
-FreezeWrapper.defaultProps = defaultProps;
FreezeWrapper.displayName = 'FreezeWrapper';
export default FreezeWrapper;
diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.ts
similarity index 61%
rename from src/libs/Navigation/Navigation.js
rename to src/libs/Navigation/Navigation.ts
index bfc0f509373e..c2dd3e76e7ad 100644
--- a/src/libs/Navigation/Navigation.js
+++ b/src/libs/Navigation/Navigation.ts
@@ -1,11 +1,10 @@
import {findFocusedRoute, getActionFromState} from '@react-navigation/core';
-import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native';
-import _ from 'lodash';
-import lodashGet from 'lodash/get';
+import {CommonActions, EventMapCore, getPathFromState, NavigationState, PartialState, StackActions} from '@react-navigation/native';
+import findLastIndex from 'lodash/findLastIndex';
import Log from '@libs/Log';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
-import ROUTES from '@src/ROUTES';
+import ROUTES, {Route} from '@src/ROUTES';
import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS';
import getStateFromPath from './getStateFromPath';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
@@ -13,13 +12,14 @@ import originalGetTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
import linkTo from './linkTo';
import navigationRef from './navigationRef';
+import {StackNavigationAction, StateOrRoute} from './types';
-let resolveNavigationIsReadyPromise;
-const navigationIsReadyPromise = new Promise((resolve) => {
+let resolveNavigationIsReadyPromise: () => void;
+const navigationIsReadyPromise = new Promise((resolve) => {
resolveNavigationIsReadyPromise = resolve;
});
-let pendingRoute = null;
+let pendingRoute: Route | null = null;
let shouldPopAllStateOnUP = false;
@@ -30,12 +30,7 @@ function setShouldPopAllStateOnUP() {
shouldPopAllStateOnUP = true;
}
-/**
- * @param {String} methodName
- * @param {Object} params
- * @returns {Boolean}
- */
-function canNavigate(methodName, params = {}) {
+function canNavigate(methodName: string, params: Record = {}): boolean {
if (navigationRef.isReady()) {
return true;
}
@@ -49,37 +44,32 @@ const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopm
// Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies.
const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state);
-/**
- * Method for finding on which index in stack we are.
- * @param {Object} route
- * @param {Number} index
- * @returns {Number}
- */
-const getActiveRouteIndex = function (route, index) {
- if (route.routes) {
- const childActiveRoute = route.routes[route.index || 0];
- return getActiveRouteIndex(childActiveRoute, route.index || 0);
+/** Method for finding on which index in stack we are. */
+function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number | undefined {
+ if ('routes' in stateOrRoute && stateOrRoute.routes) {
+ const childActiveRoute = stateOrRoute.routes[stateOrRoute.index ?? 0];
+ return getActiveRouteIndex(childActiveRoute, stateOrRoute.index ?? 0);
}
- if (route.state && route.state.routes) {
- const childActiveRoute = route.state.routes[route.state.index || 0];
- return getActiveRouteIndex(childActiveRoute, route.state.index || 0);
+ if ('state' in stateOrRoute && stateOrRoute.state?.routes) {
+ const childActiveRoute = stateOrRoute.state.routes[stateOrRoute.state.index ?? 0];
+ return getActiveRouteIndex(childActiveRoute, stateOrRoute.state.index ?? 0);
}
- if (route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ if ('name' in stateOrRoute && stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
return 0;
}
return index;
-};
+}
/**
* Gets distance from the path in root navigator. In other words how much screen you have to pop to get to the route with this path.
* The search is limited to 5 screens from the top for performance reasons.
- * @param {String} path - Path that you are looking for.
- * @return {Number} - Returns distance to path or -1 if the path is not found in root navigator.
+ * @param path - Path that you are looking for.
+ * @return - Returns distance to path or -1 if the path is not found in root navigator.
*/
-function getDistanceFromPathInRootNavigator(path) {
+function getDistanceFromPathInRootNavigator(path: string): number {
let currentState = navigationRef.getRootState();
for (let index = 0; index < 5; index++) {
@@ -98,14 +88,10 @@ function getDistanceFromPathInRootNavigator(path) {
return -1;
}
-/**
- * Returns the current active route
- * @returns {String}
- */
-function getActiveRoute() {
+/** Returns the current active route */
+function getActiveRoute(): string {
const currentRoute = navigationRef.current && navigationRef.current.getCurrentRoute();
- const currentRouteHasName = lodashGet(currentRoute, 'name', false);
- if (!currentRouteHasName) {
+ if (!currentRoute?.name) {
return '';
}
@@ -124,20 +110,19 @@ function getActiveRoute() {
* Building path with getPathFromState since navigationRef.current.getCurrentRoute().path
* is undefined in the first navigation.
*
- * @param {String} routePath Path to check
- * @return {Boolean} is active
+ * @param routePath Path to check
+ * @return is active
*/
-function isActiveRoute(routePath) {
+function isActiveRoute(routePath: Route): boolean {
// We remove First forward slash from the URL before matching
return getActiveRoute().substring(1) === routePath;
}
/**
* Main navigation method for redirecting to a route.
- * @param {String} route
- * @param {String} [type] - Type of action to perform. Currently UP is supported.
+ * @param [type] - Type of action to perform. Currently UP is supported.
*/
-function navigate(route = ROUTES.HOME, type) {
+function navigate(route: Route = ROUTES.HOME, type?: string) {
if (!canNavigate('navigate', {route})) {
// Store intended route if the navigator is not yet available,
// we will try again after the NavigationContainer is ready
@@ -149,11 +134,11 @@ function navigate(route = ROUTES.HOME, type) {
}
/**
- * @param {String} fallbackRoute - Fallback route if pop/goBack action should, but is not possible within RHP
- * @param {Boolean} shouldEnforceFallback - Enforces navigation to fallback route
- * @param {Boolean} shouldPopToTop - Should we navigate to LHN on back press
+ * @param fallbackRoute - Fallback route if pop/goBack action should, but is not possible within RHP
+ * @param shouldEnforceFallback - Enforces navigation to fallback route
+ * @param shouldPopToTop - Should we navigate to LHN on back press
*/
-function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = false) {
+function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopToTop = false) {
if (!canNavigate('goBack')) {
return;
}
@@ -161,12 +146,12 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f
if (shouldPopToTop) {
if (shouldPopAllStateOnUP) {
shouldPopAllStateOnUP = false;
- navigationRef.current.dispatch(StackActions.popToTop());
+ navigationRef.current?.dispatch(StackActions.popToTop());
return;
}
}
- if (!navigationRef.current.canGoBack()) {
+ if (!navigationRef.current?.canGoBack()) {
Log.hmmm('[Navigation] Unable to go back');
return;
}
@@ -174,9 +159,9 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f
const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState());
if (isFirstRouteInNavigator) {
const rootState = navigationRef.getRootState();
- const lastRoute = _.last(rootState.routes);
+ const lastRoute = rootState.routes.at(-1);
// If the user comes from a different flow (there is more than one route in RHP) we should go back to the previous flow on UP button press instead of using the fallbackRoute.
- if (lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state.index > 0) {
+ if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && (lastRoute.state?.index ?? 0) > 0) {
navigationRef.current.goBack();
return;
}
@@ -187,7 +172,7 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f
return;
}
- const isCentralPaneFocused = findFocusedRoute(navigationRef.current.getState()).name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR;
+ const isCentralPaneFocused = findFocusedRoute(navigationRef.current.getState())?.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR;
const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute);
// Allow CentralPane to use UP with fallback route if the path is not found in root navigator.
@@ -196,7 +181,7 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f
return;
}
- // Add posibility to go back more than one screen in root navigator if that screen is on the stack.
+ // Add possibility to go back more than one screen in root navigator if that screen is on the stack.
if (isCentralPaneFocused && fallbackRoute && distanceFromPathInRootNavigator > 0) {
navigationRef.current.dispatch(StackActions.pop(distanceFromPathInRootNavigator));
return;
@@ -207,12 +192,9 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f
/**
* Update route params for the specified route.
- *
- * @param {Object} params
- * @param {String} routeKey
*/
-function setParams(params, routeKey) {
- navigationRef.current.dispatch({
+function setParams(params: Record, routeKey: string) {
+ navigationRef.current?.dispatch({
...CommonActions.setParams(params),
source: routeKey,
});
@@ -221,15 +203,15 @@ function setParams(params, routeKey) {
/**
* Dismisses the last modal stack if there is any
*
- * @param {String | undefined} targetReportID - The reportID to navigate to after dismissing the modal
+ * @param targetReportID - The reportID to navigate to after dismissing the modal
*/
-function dismissModal(targetReportID) {
+function dismissModal(targetReportID?: string) {
if (!canNavigate('dismissModal')) {
return;
}
const rootState = navigationRef.getRootState();
- const lastRoute = _.last(rootState.routes);
- switch (lastRoute.name) {
+ const lastRoute = rootState.routes.at(-1);
+ switch (lastRoute?.name) {
case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
case SCREENS.NOT_FOUND:
case SCREENS.REPORT_ATTACHMENTS:
@@ -237,16 +219,18 @@ function dismissModal(targetReportID) {
if (targetReportID && targetReportID !== getTopmostReportId(rootState)) {
const state = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID));
- const action = getActionFromState(state, linkingConfig.config);
- action.type = 'REPLACE';
- navigationRef.current.dispatch(action);
+ const action: StackNavigationAction = getActionFromState(state, linkingConfig.config);
+ if (action) {
+ action.type = 'REPLACE';
+ navigationRef.current?.dispatch(action);
+ }
// If not-found page is in the route stack, we need to close it
- } else if (targetReportID && _.some(rootState.routes, (route) => route.name === SCREENS.NOT_FOUND)) {
+ } else if (targetReportID && rootState.routes.some((route) => route.name === SCREENS.NOT_FOUND)) {
const lastRouteIndex = rootState.routes.length - 1;
- const centralRouteIndex = _.findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
- navigationRef.current.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key});
+ const centralRouteIndex = findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
+ navigationRef.current?.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key});
} else {
- navigationRef.current.dispatch({...StackActions.pop(), target: rootState.key});
+ navigationRef.current?.dispatch({...StackActions.pop(), target: rootState.key});
}
break;
default: {
@@ -257,21 +241,17 @@ function dismissModal(targetReportID) {
/**
* Returns the current active route without the URL params
- * @returns {String}
*/
-function getActiveRouteWithoutParams() {
+function getActiveRouteWithoutParams(): string {
return getActiveRoute().replace(/\?.*/, '');
}
-/** Returns the active route name from a state event from the navigationRef
- * @param {Object} event
- * @returns {String | undefined}
- * */
-function getRouteNameFromStateEvent(event) {
+/** Returns the active route name from a state event from the navigationRef */
+function getRouteNameFromStateEvent(event: EventMapCore['state']): string | undefined {
if (!event.data.state) {
return;
}
- const currentRouteName = event.data.state.routes.slice(-1).name;
+ const currentRouteName = event.data.state.routes.at(-1)?.name;
// Check to make sure we have a route name
if (currentRouteName) {
@@ -292,10 +272,7 @@ function goToPendingRoute() {
pendingRoute = null;
}
-/**
- * @returns {Promise}
- */
-function isNavigationReady() {
+function isNavigationReady(): Promise {
return navigationIsReadyPromise;
}
@@ -307,57 +284,50 @@ function setIsNavigationReady() {
/**
* Checks if the navigation state contains routes that are protected (over the auth wall).
*
- * @function
- * @param {Object} state - react-navigation state object
- *
- * @returns {Boolean}
+ * @param state - react-navigation state object
*/
-function navContainsProtectedRoutes(state) {
- if (!state || !state.routeNames || !_.isArray(state.routeNames)) {
+function navContainsProtectedRoutes(state: NavigationState | PartialState | undefined): boolean {
+ if (!state?.routeNames || !Array.isArray(state.routeNames)) {
return false;
}
- const protectedScreensName = _.values(PROTECTED_SCREENS);
- const difference = _.difference(protectedScreensName, state.routeNames);
-
- return !difference.length;
+ const protectedScreensName = Object.values(PROTECTED_SCREENS);
+ return !protectedScreensName.some((screen) => !state.routeNames?.includes(screen));
}
/**
- * Waits for the navitgation state to contain protected routes specified in PROTECTED_SCREENS constant.
- * If the navigation is in a state, where protected routes are avilable, the promise resolve immediately.
+ * Waits for the navigation state to contain protected routes specified in PROTECTED_SCREENS constant.
+ * If the navigation is in a state, where protected routes are available, the promise resolve immediately.
*
* @function
- * @returns {Promise} A promise that resolves when the one of the PROTECTED_SCREENS screen is available in the nav tree.
+ * @returns A promise that resolves when the one of the PROTECTED_SCREENS screen is available in the nav tree.
*
* @example
* waitForProtectedRoutes()
* .then(()=> console.log('Protected routes are present!'))
*/
function waitForProtectedRoutes() {
- return new Promise((resolve) => {
+ return new Promise((resolve) => {
isNavigationReady().then(() => {
- const currentState = navigationRef.current.getState();
+ const currentState = navigationRef.current?.getState();
if (navContainsProtectedRoutes(currentState)) {
resolve();
return;
}
- let unsubscribe;
- const handleStateChange = ({data}) => {
- const state = lodashGet(data, 'state');
+
+ const unsubscribe = navigationRef.current?.addListener('state', ({data}) => {
+ const state = data?.state;
if (navContainsProtectedRoutes(state)) {
- unsubscribe();
+ unsubscribe?.();
resolve();
}
- };
- unsubscribe = navigationRef.current.addListener('state', handleStateChange);
+ });
});
});
}
export default {
setShouldPopAllStateOnUP,
- canNavigate,
navigate,
setParams,
dismissModal,
@@ -371,7 +341,6 @@ export default {
getRouteNameFromStateEvent,
getTopmostReportActionId,
waitForProtectedRoutes,
- navContainsProtectedRoutes,
};
export {navigationRef};
diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.tsx
similarity index 58%
rename from src/libs/Navigation/NavigationRoot.js
rename to src/libs/Navigation/NavigationRoot.tsx
index 2373066cf4bd..cbb2e62161f3 100644
--- a/src/libs/Navigation/NavigationRoot.js
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -1,39 +1,29 @@
-import {DefaultTheme, getPathFromState, NavigationContainer} from '@react-navigation/native';
-import PropTypes from 'prop-types';
-import React, {useEffect, useRef} from 'react';
-import {Easing, interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
+import {DefaultTheme, getPathFromState, NavigationContainer, NavigationState} from '@react-navigation/native';
+import React, {useEffect, useMemo, useRef} from 'react';
+import {ColorValue} from 'react-native';
+import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
import useCurrentReportID from '@hooks/useCurrentReportID';
import useFlipper from '@hooks/useFlipper';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Log from '@libs/Log';
import StatusBar from '@libs/StatusBar';
-import themeColors from '@styles/themes/default';
+import useTheme from '@styles/themes/useTheme';
import AppNavigator from './AppNavigator';
import linkingConfig from './linkingConfig';
import Navigation, {navigationRef} from './Navigation';
-// https://reactnavigation.org/docs/themes
-const navigationTheme = {
- ...DefaultTheme,
- colors: {
- ...DefaultTheme.colors,
- background: themeColors.appBG,
- },
-};
-
-const propTypes = {
+type NavigationRootProps = {
/** Whether the current user is logged in with an authToken */
- authenticated: PropTypes.bool.isRequired,
+ authenticated: boolean;
/** Fired when react-navigation is ready */
- onReady: PropTypes.func.isRequired,
+ onReady: () => void;
};
/**
* Intercept navigation state changes and log it
- * @param {NavigationState} state
*/
-function parseAndLogRoute(state) {
+function parseAndLogRoute(state: NavigationState) {
if (!state) {
return;
}
@@ -50,13 +40,26 @@ function parseAndLogRoute(state) {
Navigation.setIsNavigationReady();
}
-function NavigationRoot(props) {
+function NavigationRoot({authenticated, onReady}: NavigationRootProps) {
useFlipper(navigationRef);
+ const theme = useTheme();
const firstRenderRef = useRef(true);
- const {updateCurrentReportID} = useCurrentReportID();
+ const currentReportIDValue = useCurrentReportID();
const {isSmallScreenWidth} = useWindowDimensions();
+ // https://reactnavigation.org/docs/themes
+ const navigationTheme = useMemo(
+ () => ({
+ ...DefaultTheme,
+ colors: {
+ ...DefaultTheme.colors,
+ background: theme.appBG,
+ },
+ }),
+ [theme],
+ );
+
useEffect(() => {
if (firstRenderRef.current) {
// we don't want to make the report back button go back to LHN if the user
@@ -72,24 +75,24 @@ function NavigationRoot(props) {
}, [isSmallScreenWidth]);
useEffect(() => {
- if (!navigationRef.isReady() || !props.authenticated) {
+ if (!navigationRef.isReady() || !authenticated) {
return;
}
// We need to force state rehydration so the CustomRouter can add the CentralPaneNavigator route if necessary.
navigationRef.resetRoot(navigationRef.getRootState());
- }, [isSmallScreenWidth, props.authenticated]);
+ }, [isSmallScreenWidth, authenticated]);
- const prevStatusBarBackgroundColor = useRef(themeColors.appBG);
- const statusBarBackgroundColor = useRef(themeColors.appBG);
+ const prevStatusBarBackgroundColor = useRef(theme.appBG);
+ const statusBarBackgroundColor = useRef(theme.appBG);
const statusBarAnimation = useSharedValue(0);
- const updateStatusBarBackgroundColor = (color) => StatusBar.setBackgroundColor(color);
+ const updateStatusBarBackgroundColor = (color: ColorValue) => StatusBar.setBackgroundColor(color);
useAnimatedReaction(
() => statusBarAnimation.value,
(current, previous) => {
// Do not run if either of the animated value is null
// or previous animated value is greater than or equal to the current one
- if ([current, previous].includes(null) || current <= previous) {
+ if (previous === null || current === null || current <= previous) {
return;
}
const color = interpolateColor(statusBarAnimation.value, [0, 1], [prevStatusBarBackgroundColor.current, statusBarBackgroundColor.current]);
@@ -99,32 +102,34 @@ function NavigationRoot(props) {
const animateStatusBarBackgroundColor = () => {
const currentRoute = navigationRef.getCurrentRoute();
- const currentScreenBackgroundColor = (currentRoute.params && currentRoute.params.backgroundColor) || themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG;
+
+ const backgroundColorFromRoute =
+ currentRoute?.params && 'backgroundColor' in currentRoute.params && typeof currentRoute.params.backgroundColor === 'string' && currentRoute.params.backgroundColor;
+ const backgroundColorFallback = currentRoute?.name ? theme.PAGE_BACKGROUND_COLORS[currentRoute.name] || theme.appBG : theme.appBG;
+
+ // It's possible for backgroundColorFromRoute to be empty string, so we must use "||" to fallback to backgroundColorFallback.
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const currentScreenBackgroundColor = backgroundColorFromRoute || backgroundColorFallback;
prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current;
statusBarBackgroundColor.current = currentScreenBackgroundColor;
- if (currentScreenBackgroundColor === themeColors.appBG && prevStatusBarBackgroundColor.current === themeColors.appBG) {
+ if (currentScreenBackgroundColor === theme.appBG && prevStatusBarBackgroundColor.current === theme.appBG) {
return;
}
statusBarAnimation.value = 0;
- statusBarAnimation.value = withDelay(
- 300,
- withTiming(1, {
- duration: 300,
- easing: Easing.in,
- }),
- );
+ statusBarAnimation.value = withDelay(300, withTiming(1));
};
- const handleStateChange = (state) => {
+ const handleStateChange = (state: NavigationState | undefined) => {
if (!state) {
return;
}
+
// Performance optimization to avoid context consumers to delay first render
setTimeout(() => {
- updateCurrentReportID(state);
+ currentReportIDValue?.updateCurrentReportID(state);
}, 0);
parseAndLogRoute(state);
animateStatusBarBackgroundColor();
@@ -133,7 +138,7 @@ function NavigationRoot(props) {
return (
-
+
);
}
NavigationRoot.displayName = 'NavigationRoot';
-NavigationRoot.propTypes = propTypes;
+
export default NavigationRoot;
diff --git a/src/libs/Navigation/OnyxTabNavigator.js b/src/libs/Navigation/OnyxTabNavigator.tsx
similarity index 54%
rename from src/libs/Navigation/OnyxTabNavigator.js
rename to src/libs/Navigation/OnyxTabNavigator.tsx
index eeed3e0cd270..1ea57e773323 100644
--- a/src/libs/Navigation/OnyxTabNavigator.js
+++ b/src/libs/Navigation/OnyxTabNavigator.tsx
@@ -1,31 +1,33 @@
-import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
-import PropTypes from 'prop-types';
+import {createMaterialTopTabNavigator, MaterialTopTabNavigationEventMap} from '@react-navigation/material-top-tabs';
+import {EventMapCore, NavigationState, ScreenListeners} from '@react-navigation/native';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
+import {OnyxEntry} from 'react-native-onyx/lib/types';
import Tab from '@userActions/Tab';
import ONYXKEYS from '@src/ONYXKEYS';
+import ChildrenProps from '@src/types/utils/ChildrenProps';
-const propTypes = {
- /** ID of the tab component to be saved in onyx */
- id: PropTypes.string.isRequired,
+type OnyxTabNavigatorOnyxProps = {
+ selectedTab: OnyxEntry;
+};
- /** Name of the selected tab */
- selectedTab: PropTypes.string,
+type OnyxTabNavigatorProps = OnyxTabNavigatorOnyxProps &
+ ChildrenProps & {
+ /** ID of the tab component to be saved in onyx */
+ id: string;
- /** Children nodes */
- children: PropTypes.node.isRequired,
-};
+ /** Name of the selected tab */
+ selectedTab?: string;
-const defaultProps = {
- selectedTab: '',
-};
+ screenListeners?: ScreenListeners;
+ };
// eslint-disable-next-line rulesdir/no-inline-named-export
export const TopTab = createMaterialTopTabNavigator();
// This takes all the same props as MaterialTopTabsNavigator: https://reactnavigation.org/docs/material-top-tab-navigator/#props,
// except ID is now required, and it gets a `selectedTab` from Onyx
-function OnyxTabNavigator({id, selectedTab, children, ...rest}) {
+function OnyxTabNavigator({id, selectedTab = '', children, screenListeners, ...rest}: OnyxTabNavigatorProps) {
return (
{
+ state: (e) => {
+ const event = e as unknown as EventMapCore['state'];
const state = event.data.state;
const index = state.index;
const routeNames = state.routeNames;
Tab.setSelectedTab(id, routeNames[index]);
},
- ...(rest.screenListeners || {}),
+ ...(screenListeners ?? {}),
}}
>
{children}
@@ -49,11 +52,9 @@ function OnyxTabNavigator({id, selectedTab, children, ...rest}) {
);
}
-OnyxTabNavigator.defaultProps = defaultProps;
-OnyxTabNavigator.propTypes = propTypes;
OnyxTabNavigator.displayName = 'OnyxTabNavigator';
-export default withOnyx({
+export default withOnyx({
selectedTab: {
key: ({id}) => `${ONYXKEYS.COLLECTION.SELECTED_TAB}${id}`,
},
diff --git a/src/libs/Navigation/getStateFromPath.js b/src/libs/Navigation/getStateFromPath.ts
similarity index 57%
rename from src/libs/Navigation/getStateFromPath.js
rename to src/libs/Navigation/getStateFromPath.ts
index f2564c9d2512..3a53b02fc3c7 100644
--- a/src/libs/Navigation/getStateFromPath.js
+++ b/src/libs/Navigation/getStateFromPath.ts
@@ -1,11 +1,12 @@
-import {getStateFromPath as RNGetStateFromPath} from '@react-navigation/native';
+import {NavigationState, PartialState, getStateFromPath as RNGetStateFromPath} from '@react-navigation/native';
+import {Route} from '@src/ROUTES';
import linkingConfig from './linkingConfig';
/**
- * @param {String} path - The path to parse
- * @returns {Object | undefined} - It's possible that there is no navigation action for the given path
+ * @param path - The path to parse
+ * @returns - It's possible that there is no navigation action for the given path
*/
-function getStateFromPath(path) {
+function getStateFromPath(path: Route): PartialState {
const normalizedPath = !path.startsWith('/') ? `/${path}` : path;
const state = linkingConfig.getStateFromPath ? linkingConfig.getStateFromPath(normalizedPath, linkingConfig.config) : RNGetStateFromPath(normalizedPath, linkingConfig.config);
@@ -13,6 +14,7 @@ function getStateFromPath(path) {
if (!state) {
throw new Error('Failed to parse the path to a navigation state.');
}
+
return state;
}
diff --git a/src/libs/Navigation/getTopmostReportActionID.js b/src/libs/Navigation/getTopmostReportActionID.js
deleted file mode 100644
index a4480931cda0..000000000000
--- a/src/libs/Navigation/getTopmostReportActionID.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import lodashFindLast from 'lodash/findLast';
-import lodashGet from 'lodash/get';
-
-// This function is in a separate file than Navigation.js to avoid cyclic dependency.
-
-/**
- * Find the last visited report screen in the navigation state and get the linked reportActionID of it.
- *
- * @param {Object} state - The react-navigation state
- * @returns {String | undefined} - It's possible that there is no report screen
- */
-function getTopmostReportActionID(state) {
- if (!state) {
- return;
- }
- const topmostCentralPane = lodashFindLast(state.routes, (route) => route.name === 'CentralPaneNavigator');
-
- if (!topmostCentralPane) {
- return;
- }
-
- const directReportActionIDParam = lodashGet(topmostCentralPane, 'params.params.reportActionID');
-
- if (!topmostCentralPane.state && !directReportActionIDParam) {
- return;
- }
-
- if (directReportActionIDParam) {
- return directReportActionIDParam;
- }
-
- const topmostReport = lodashFindLast(topmostCentralPane.state.routes, (route) => route.name === 'Report');
- if (!topmostReport) {
- return;
- }
-
- const topmostReportActionID = lodashGet(topmostReport, 'params.reportActionID');
-
- return topmostReportActionID;
-}
-
-export default getTopmostReportActionID;
diff --git a/src/libs/Navigation/getTopmostReportActionID.ts b/src/libs/Navigation/getTopmostReportActionID.ts
new file mode 100644
index 000000000000..15ab1efef704
--- /dev/null
+++ b/src/libs/Navigation/getTopmostReportActionID.ts
@@ -0,0 +1,48 @@
+import {NavigationState, PartialState} from '@react-navigation/native';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import {RootStackParamList} from './types';
+
+// This function is in a separate file than Navigation.js to avoid cyclic dependency.
+
+/**
+ * Find the last visited report screen in the navigation state and get the linked reportActionID of it.
+ *
+ * @param state - The react-navigation state
+ * @returns - It's possible that there is no report screen
+ */
+function getTopmostReportActionID(state: NavigationState | NavigationState | PartialState): string | undefined {
+ if (!state) {
+ return;
+ }
+
+ const topmostCentralPane = state.routes.filter((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR).at(-1);
+ if (!topmostCentralPane) {
+ return;
+ }
+
+ const directReportParams = topmostCentralPane.params && 'params' in topmostCentralPane.params && topmostCentralPane?.params?.params;
+ const directReportActionIDParam = directReportParams && 'reportActionID' in directReportParams && directReportParams?.reportActionID;
+
+ if (!topmostCentralPane.state && !directReportActionIDParam) {
+ return;
+ }
+
+ if (directReportActionIDParam) {
+ return directReportActionIDParam;
+ }
+
+ const topmostReport = topmostCentralPane.state?.routes.filter((route) => route.name === SCREENS.REPORT).at(-1);
+ if (!topmostReport) {
+ return;
+ }
+
+ const topmostReportActionID = topmostReport.params && 'reportActionID' in topmostReport.params && topmostReport.params?.reportActionID;
+ if (typeof topmostReportActionID !== 'string') {
+ return;
+ }
+
+ return topmostReportActionID;
+}
+
+export default getTopmostReportActionID;
diff --git a/src/libs/Navigation/getTopmostReportId.js b/src/libs/Navigation/getTopmostReportId.js
deleted file mode 100644
index 8ca9c39baf6a..000000000000
--- a/src/libs/Navigation/getTopmostReportId.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import lodashFindLast from 'lodash/findLast';
-import lodashGet from 'lodash/get';
-
-// This function is in a separate file than Navigation.js to avoid cyclic dependency.
-
-/**
- * Find the last visited report screen in the navigation state and get the id of it.
- *
- * @param {Object} state - The react-navigation state
- * @returns {String | undefined} - It's possible that there is no report screen
- */
-function getTopmostReportId(state) {
- if (!state) {
- return;
- }
- const topmostCentralPane = lodashFindLast(state.routes, (route) => route.name === 'CentralPaneNavigator');
-
- if (!topmostCentralPane) {
- return;
- }
-
- const directReportIdParam = lodashGet(topmostCentralPane, 'params.params.reportID');
-
- if (!topmostCentralPane.state && !directReportIdParam) {
- return;
- }
-
- if (directReportIdParam) {
- return directReportIdParam;
- }
-
- const topmostReport = lodashFindLast(topmostCentralPane.state.routes, (route) => route.name === 'Report');
- if (!topmostReport) {
- return;
- }
-
- const topmostReportId = lodashGet(topmostReport, 'params.reportID');
-
- return topmostReportId;
-}
-
-export default getTopmostReportId;
diff --git a/src/libs/Navigation/getTopmostReportId.ts b/src/libs/Navigation/getTopmostReportId.ts
new file mode 100644
index 000000000000..3342761e7ccf
--- /dev/null
+++ b/src/libs/Navigation/getTopmostReportId.ts
@@ -0,0 +1,48 @@
+import {NavigationState, PartialState} from '@react-navigation/native';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import {RootStackParamList} from './types';
+
+// This function is in a separate file than Navigation.js to avoid cyclic dependency.
+
+/**
+ * Find the last visited report screen in the navigation state and get the id of it.
+ *
+ * @param state - The react-navigation state
+ * @returns - It's possible that there is no report screen
+ */
+function getTopmostReportId(state: NavigationState | NavigationState | PartialState): string | undefined {
+ if (!state) {
+ return;
+ }
+
+ const topmostCentralPane = state.routes.filter((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR).at(-1);
+ if (!topmostCentralPane) {
+ return;
+ }
+
+ const directReportParams = topmostCentralPane.params && 'params' in topmostCentralPane.params && topmostCentralPane?.params?.params;
+ const directReportIdParam = directReportParams && 'reportID' in directReportParams && directReportParams?.reportID;
+
+ if (!topmostCentralPane.state && !directReportIdParam) {
+ return;
+ }
+
+ if (directReportIdParam) {
+ return directReportIdParam;
+ }
+
+ const topmostReport = topmostCentralPane.state?.routes.filter((route) => route.name === SCREENS.REPORT).at(-1);
+ if (!topmostReport) {
+ return;
+ }
+
+ const topmostReportId = topmostReport.params && 'reportID' in topmostReport.params && topmostReport.params?.reportID;
+ if (typeof topmostReportId !== 'string') {
+ return;
+ }
+
+ return topmostReportId;
+}
+
+export default getTopmostReportId;
diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.ts
similarity index 60%
rename from src/libs/Navigation/linkTo.js
rename to src/libs/Navigation/linkTo.ts
index ca87a0d7b79c..1a4aa2d0cfb7 100644
--- a/src/libs/Navigation/linkTo.js
+++ b/src/libs/Navigation/linkTo.ts
@@ -1,39 +1,53 @@
import {getActionFromState} from '@react-navigation/core';
-import _ from 'lodash';
+import {NavigationAction, NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native';
+import {Writable} from 'type-fest';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
+import {Route} from '@src/ROUTES';
import getStateFromPath from './getStateFromPath';
import getTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
+import {NavigationRoot, RootStackParamList, StackNavigationAction} from './types';
+
+type ActionPayloadParams = {
+ screen?: string;
+ params?: unknown;
+ path?: string;
+};
+
+type ActionPayload = {
+ params?: ActionPayloadParams;
+};
/**
* Motivation for this function is described in NAVIGATION.md
*
- * @param {Object} action action generated by getActionFromState
- * @param {Object} state The root state
- * @returns {Object} minimalAction minimal action is the action that we should dispatch
+ * @param action action generated by getActionFromState
+ * @param state The root state
+ * @returns minimalAction minimal action is the action that we should dispatch
*/
-function getMinimalAction(action, state) {
- let currentAction = action;
- let currentState = state;
- let currentTargetKey = null;
+function getMinimalAction(action: NavigationAction, state: NavigationState): Writable {
+ let currentAction: NavigationAction = action;
+ let currentState: NavigationState | PartialState | undefined = state;
+ let currentTargetKey: string | undefined;
- while (currentState.routes[currentState.index].name === currentAction.payload.name) {
- if (!currentState.routes[currentState.index].state) {
+ while (currentAction.payload && 'name' in currentAction.payload && currentState?.routes[currentState.index ?? -1].name === currentAction.payload.name) {
+ if (!currentState?.routes[currentState.index ?? -1].state) {
break;
}
- currentState = currentState.routes[currentState.index].state;
+ currentState = currentState?.routes[currentState.index ?? -1].state;
+ currentTargetKey = currentState?.key;
- currentTargetKey = currentState.key;
+ const payload = currentAction.payload as ActionPayload;
// Creating new smaller action
currentAction = {
type: currentAction.type,
payload: {
- name: currentAction.payload.params.screen,
- params: currentAction.payload.params.params,
- path: currentAction.payload.params.path,
+ name: payload?.params?.screen,
+ params: payload?.params?.params,
+ path: payload?.params?.path,
},
target: currentTargetKey,
};
@@ -41,13 +55,13 @@ function getMinimalAction(action, state) {
return currentAction;
}
-export default function linkTo(navigation, path, type, isActiveRoute) {
- if (navigation === undefined) {
+export default function linkTo(navigation: NavigationContainerRef | null, path: Route, type?: string, isActiveRoute?: boolean) {
+ if (!navigation) {
throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?");
}
- let root = navigation;
- let current;
+ let root: NavigationRoot = navigation;
+ let current: NavigationRoot | undefined;
// Traverse up to get the root navigation
// eslint-disable-next-line no-cond-assign
@@ -55,18 +69,18 @@ export default function linkTo(navigation, path, type, isActiveRoute) {
root = current;
}
+ const rootState = root.getState();
const state = getStateFromPath(path);
-
- const action = getActionFromState(state, linkingConfig.config);
+ const action: StackNavigationAction = getActionFromState(state, linkingConfig.config);
// If action type is different than NAVIGATE we can't change it to the PUSH safely
- if (action.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) {
+ if (action?.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) {
// In case if type is 'FORCED_UP' we replace current screen with the provided. This means the current screen no longer exists in the stack
if (type === CONST.NAVIGATION.TYPE.FORCED_UP) {
action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE;
// If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack
- } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(root.getState()) !== getTopmostReportId(state)) {
+ } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(rootState) !== getTopmostReportId(state)) {
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
// If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow
@@ -75,12 +89,12 @@ export default function linkTo(navigation, path, type, isActiveRoute) {
action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE;
// If this action is navigating to the RightModalNavigator and the last route on the root navigator is not RightModalNavigator then push
- } else if (action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && _.last(root.getState().routes).name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ } else if (action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && rootState.routes.at(-1)?.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
}
}
- if (action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ if (action && 'payload' in action && action.payload && 'name' in action.payload && action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
const minimalAction = getMinimalAction(action, navigation.getRootState());
if (minimalAction) {
// There are situations where a route already exists on the current navigation stack
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.ts
similarity index 97%
rename from src/libs/Navigation/linkingConfig.js
rename to src/libs/Navigation/linkingConfig.ts
index e0ac35c957a3..bb06bf7e4528 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.ts
@@ -1,21 +1,24 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import {LinkingOptions} from '@react-navigation/native';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
+import {RootStackParamList} from './types';
-export default {
+const linkingConfig: LinkingOptions = {
prefixes: ['new-expensify://', 'https://www.expensify.cash', 'https://staging.expensify.cash', 'https://dev.new.expensify.com', CONST.NEW_EXPENSIFY_URL, CONST.STAGING_NEW_EXPENSIFY_URL],
config: {
initialRouteName: SCREENS.HOME,
screens: {
// Main Routes
- ValidateLogin: ROUTES.VALIDATE_LOGIN,
- UnlinkLogin: ROUTES.UNLINK_LOGIN,
+ [SCREENS.VALIDATE_LOGIN]: ROUTES.VALIDATE_LOGIN,
+ [SCREENS.UNLINK_LOGIN]: ROUTES.UNLINK_LOGIN,
[SCREENS.TRANSITION_BETWEEN_APPS]: ROUTES.TRANSITION_BETWEEN_APPS,
[SCREENS.CONCIERGE]: ROUTES.CONCIERGE,
- AppleSignInDesktop: ROUTES.APPLE_SIGN_IN,
- GoogleSignInDesktop: ROUTES.GOOGLE_SIGN_IN,
- SAMLSignIn: ROUTES.SAML_SIGN_IN,
+ [SCREENS.SIGN_IN_WITH_APPLE_DESKTOP]: ROUTES.APPLE_SIGN_IN,
+ [SCREENS.SIGN_IN_WITH_GOOGLE_DESKTOP]: ROUTES.GOOGLE_SIGN_IN,
+ [SCREENS.SAML_SIGN_IN]: ROUTES.SAML_SIGN_IN,
[SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT,
[SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route,
@@ -192,7 +195,7 @@ export default {
exact: true,
},
Settings_TwoFactorAuth: {
- path: ROUTES.SETTINGS_2FA,
+ path: ROUTES.SETTINGS_2FA.route,
exact: true,
},
Settings_Share_Code: {
@@ -448,3 +451,5 @@ export default {
},
},
};
+
+export default linkingConfig;
diff --git a/src/libs/Navigation/navigationRef.js b/src/libs/Navigation/navigationRef.js
deleted file mode 100644
index 00c98d178f7e..000000000000
--- a/src/libs/Navigation/navigationRef.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import {createNavigationContainerRef} from '@react-navigation/native';
-
-const navigationRef = createNavigationContainerRef();
-export default navigationRef;
diff --git a/src/libs/Navigation/navigationRef.ts b/src/libs/Navigation/navigationRef.ts
new file mode 100644
index 000000000000..032d9f9f3d9a
--- /dev/null
+++ b/src/libs/Navigation/navigationRef.ts
@@ -0,0 +1,6 @@
+import {createNavigationContainerRef} from '@react-navigation/native';
+import {NavigationRef} from './types';
+
+const navigationRef: NavigationRef = createNavigationContainerRef();
+
+export default navigationRef;
diff --git a/src/libs/Navigation/shouldPreventDeeplinkPrompt/index.js b/src/libs/Navigation/shouldPreventDeeplinkPrompt.ts
similarity index 57%
rename from src/libs/Navigation/shouldPreventDeeplinkPrompt/index.js
rename to src/libs/Navigation/shouldPreventDeeplinkPrompt.ts
index 23f46cb9808f..2b19da1f5224 100644
--- a/src/libs/Navigation/shouldPreventDeeplinkPrompt/index.js
+++ b/src/libs/Navigation/shouldPreventDeeplinkPrompt.ts
@@ -2,12 +2,9 @@ import CONST from '@src/CONST';
/**
* Determines if the deeplink prompt should be shown on the current screen
- * @param {String} screenName
- * @param {Boolean} isAuthenticated
- * @returns {Boolean}
*/
-export default function shouldPreventDeeplinkPrompt(screenName) {
+export default function shouldPreventDeeplinkPrompt(screenName: string): boolean {
// We don't want to show the deeplink prompt on screens where a user is in the
// authentication process, so we are blocking the prompt on the following screens (Denylist)
- return CONST.DEEPLINK_PROMPT_DENYLIST.includes(screenName);
+ return CONST.DEEPLINK_PROMPT_DENYLIST.some((name) => name === screenName);
}
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
new file mode 100644
index 000000000000..41df21d8e237
--- /dev/null
+++ b/src/libs/Navigation/types.ts
@@ -0,0 +1,401 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import {CommonActions, NavigationContainerRefWithCurrent, NavigationHelpers, NavigationState, NavigatorScreenParams, PartialRoute, Route} from '@react-navigation/native';
+import {ValueOf} from 'type-fest';
+import CONST from '@src/CONST';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+
+type NavigationRef = NavigationContainerRefWithCurrent;
+
+type NavigationRoot = NavigationHelpers;
+
+type GoBackAction = Extract;
+type ResetAction = Extract;
+type SetParamsAction = Extract;
+
+type ActionNavigate = {
+ type: ValueOf;
+ payload: {
+ name?: string;
+ key?: string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ params?: any;
+ path?: string;
+ merge?: boolean;
+ };
+ source?: string;
+ target?: string;
+};
+
+type StackNavigationAction = GoBackAction | ResetAction | SetParamsAction | ActionNavigate | undefined;
+
+type NavigationStateRoute = NavigationState['routes'][number];
+type NavigationPartialRoute = PartialRoute>;
+type StateOrRoute = NavigationState | NavigationStateRoute | NavigationPartialRoute;
+
+type CentralPaneNavigatorParamList = {
+ [SCREENS.REPORT]: {
+ reportActionID: string;
+ reportID: string;
+ };
+};
+
+type SettingsNavigatorParamList = {
+ [SCREENS.SETTINGS.ROOT]: undefined;
+ Settings_Share_Code: undefined;
+ [SCREENS.SETTINGS.WORKSPACES]: undefined;
+ Settings_Profile: undefined;
+ Settings_Pronouns: undefined;
+ Settings_Display_Name: undefined;
+ Settings_Timezone: undefined;
+ Settings_Timezone_Select: undefined;
+ Settings_PersonalDetails_Initial: undefined;
+ Settings_PersonalDetails_LegalName: undefined;
+ Settings_PersonalDetails_DateOfBirth: undefined;
+ Settings_PersonalDetails_Address: undefined;
+ Settings_PersonalDetails_Address_Country: undefined;
+ Settings_ContactMethods: undefined;
+ Settings_ContactMethodDetails: undefined;
+ Settings_NewContactMethod: undefined;
+ [SCREENS.SETTINGS.PREFERENCES]: undefined;
+ Settings_Preferences_PriorityMode: undefined;
+ Settings_Preferences_Language: undefined;
+ Settings_Preferences_Theme: undefined;
+ Settings_Close: undefined;
+ [SCREENS.SETTINGS.SECURITY]: undefined;
+ Settings_About: undefined;
+ Settings_App_Download_Links: undefined;
+ Settings_Lounge_Access: undefined;
+ Settings_Wallet: undefined;
+ Settings_Wallet_Cards_Digital_Details_Update_Address: undefined;
+ Settings_Wallet_DomainCard: undefined;
+ Settings_Wallet_ReportVirtualCardFraud: undefined;
+ Settings_Wallet_Card_Activate: undefined;
+ [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.NAME]: undefined;
+ [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.PHONE]: undefined;
+ [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.ADDRESS]: undefined;
+ [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.CONFIRM]: undefined;
+ Settings_Wallet_Transfer_Balance: undefined;
+ Settings_Wallet_Choose_Transfer_Account: undefined;
+ Settings_Wallet_EnablePayments: undefined;
+ Settings_Add_Debit_Card: undefined;
+ Settings_Add_Bank_Account: undefined;
+ [SCREENS.SETTINGS.STATUS]: undefined;
+ Settings_Status_Set: undefined;
+ Workspace_Initial: undefined;
+ Workspace_Settings: undefined;
+ Workspace_Settings_Currency: undefined;
+ Workspace_Card: {
+ policyID: string;
+ };
+ Workspace_Reimburse: {
+ policyID: string;
+ };
+ Workspace_RateAndUnit: undefined;
+ Workspace_Bills: {
+ policyID: string;
+ };
+ Workspace_Invoices: {
+ policyID: string;
+ };
+ Workspace_Travel: {
+ policyID: string;
+ };
+ Workspace_Members: {
+ policyID: string;
+ };
+ Workspace_Invite: {
+ policyID: string;
+ };
+ Workspace_Invite_Message: {
+ policyID: string;
+ };
+ ReimbursementAccount: {
+ stepToOpen: string;
+ policyID: string;
+ };
+ GetAssistance: {
+ taskID: string;
+ };
+ Settings_TwoFactorAuth: undefined;
+ Settings_ReportCardLostOrDamaged: undefined;
+ KeyboardShortcuts: undefined;
+};
+
+type NewChatNavigatorParamList = {
+ NewChat_Root: undefined;
+};
+
+type SearchNavigatorParamList = {
+ Search_Root: undefined;
+};
+
+type DetailsNavigatorParamList = {
+ Details_Root: {
+ login: string;
+ reportID: string;
+ };
+};
+
+type ProfileNavigatorParamList = {
+ Profile_Root: {
+ accountID: string;
+ reportID: string;
+ };
+};
+
+type ReportDetailsNavigatorParamList = {
+ Report_Details_Root: undefined;
+ Report_Details_Share_Code: {
+ reportID: string;
+ };
+};
+
+type ReportSettingsNavigatorParamList = {
+ Report_Settings_Root: undefined;
+ Report_Settings_Room_Name: undefined;
+ Report_Settings_Notification_Preferences: undefined;
+ Report_Settings_Write_Capability: undefined;
+};
+
+type ReportWelcomeMessageNavigatorParamList = {
+ Report_WelcomeMessage_Root: {reportID: string};
+};
+
+type ParticipantsNavigatorParamList = {
+ ReportParticipants_Root: {reportID: string};
+};
+
+type RoomMembersNavigatorParamList = {
+ RoomMembers_Root: undefined;
+};
+
+type RoomInviteNavigatorParamList = {
+ RoomInvite_Root: undefined;
+};
+
+type MoneyRequestNavigatorParamList = {
+ Money_Request: undefined;
+ Money_Request_Amount: undefined;
+ Money_Request_Participants: {
+ iouType: string;
+ reportID: string;
+ };
+ Money_Request_Confirmation: {
+ iouType: string;
+ reportID: string;
+ };
+ Money_Request_Currency: {
+ iouType: string;
+ reportID: string;
+ currency: string;
+ backTo: string;
+ };
+ Money_Request_Date: {
+ iouType: string;
+ reportID: string;
+ field: string;
+ threadReportID: string;
+ };
+ Money_Request_Description: {
+ iouType: string;
+ reportID: string;
+ field: string;
+ threadReportID: string;
+ };
+ Money_Request_Category: {
+ iouType: string;
+ reportID: string;
+ };
+ Money_Request_Tag: {
+ iouType: string;
+ reportID: string;
+ };
+ Money_Request_Merchant: {
+ iouType: string;
+ reportID: string;
+ field: string;
+ threadReportID: string;
+ };
+ IOU_Send_Enable_Payments: undefined;
+ IOU_Send_Add_Bank_Account: undefined;
+ IOU_Send_Add_Debit_Card: undefined;
+ Money_Request_Waypoint: {
+ iouType: string;
+ transactionID: string;
+ waypointIndex: string;
+ threadReportID: number;
+ };
+ Money_Request_Edit_Waypoint: {
+ iouType: string;
+ transactionID: string;
+ waypointIndex: string;
+ threadReportID: number;
+ };
+ Money_Request_Distance: {
+ iouType: ValueOf;
+ reportID: string;
+ };
+ Money_Request_Receipt: {
+ iouType: string;
+ reportID: string;
+ };
+};
+
+type NewTaskNavigatorParamList = {
+ NewTask_Root: undefined;
+ NewTask_TaskAssigneeSelector: undefined;
+ NewTask_TaskShareDestinationSelector: undefined;
+ NewTask_Details: undefined;
+ NewTask_Title: undefined;
+ NewTask_Description: undefined;
+};
+
+type TeachersUniteNavigatorParamList = {
+ [SCREENS.SAVE_THE_WORLD.ROOT]: undefined;
+ I_Know_A_Teacher: undefined;
+ Intro_School_Principal: undefined;
+ I_Am_A_Teacher: undefined;
+};
+
+type TaskDetailsNavigatorParamList = {
+ Task_Title: undefined;
+ Task_Description: undefined;
+ Task_Assignee: {
+ reportID: string;
+ };
+};
+
+type EnablePaymentsNavigatorParamList = {
+ EnablePayments_Root: undefined;
+};
+
+type SplitDetailsNavigatorParamList = {
+ SplitDetails_Root: {
+ reportActionID: string;
+ };
+ SplitDetails_Edit_Request: undefined;
+ SplitDetails_Edit_Currency: undefined;
+};
+
+type AddPersonalBankAccountNavigatorParamList = {
+ AddPersonalBankAccount_Root: undefined;
+};
+
+type WalletStatementNavigatorParamList = {
+ WalletStatement_Root: undefined;
+};
+
+type FlagCommentNavigatorParamList = {
+ FlagComment_Root: {
+ reportID: string;
+ reportActionID: string;
+ };
+};
+
+type EditRequestNavigatorParamList = {
+ EditRequest_Root: {
+ field: string;
+ threadReportID: string;
+ };
+ EditRequest_Currency: undefined;
+};
+
+type SignInNavigatorParamList = {
+ SignIn_Root: undefined;
+};
+
+type ReferralDetailsNavigatorParamList = {
+ Referral_Details: undefined;
+};
+
+type PrivateNotesNavigatorParamList = {
+ PrivateNotes_View: {
+ reportID: string;
+ accountID: string;
+ };
+ PrivateNotes_List: {
+ reportID: string;
+ accountID: string;
+ };
+ PrivateNotes_Edit: {
+ reportID: string;
+ accountID: string;
+ };
+};
+
+type RightModalNavigatorParamList = {
+ Settings: NavigatorScreenParams;
+ NewChat: NavigatorScreenParams;
+ Search: NavigatorScreenParams;
+ Details: NavigatorScreenParams;
+ Profile: NavigatorScreenParams;
+ Report_Details: NavigatorScreenParams;
+ Report_Settings: NavigatorScreenParams;
+ Report_WelcomeMessage: NavigatorScreenParams;
+ Participants: NavigatorScreenParams;
+ RoomMembers: NavigatorScreenParams;
+ RoomInvite: NavigatorScreenParams;
+ MoneyRequest: NavigatorScreenParams;
+ NewTask: NavigatorScreenParams;
+ TeachersUnite: NavigatorScreenParams;
+ Task_Details: NavigatorScreenParams;
+ EnablePayments: NavigatorScreenParams;
+ SplitDetails: NavigatorScreenParams;
+ AddPersonalBankAccount: NavigatorScreenParams;
+ Wallet_Statement: NavigatorScreenParams;
+ Flag_Comment: NavigatorScreenParams;
+ EditRequest: NavigatorScreenParams;
+ SignIn: NavigatorScreenParams;
+ Referral: NavigatorScreenParams;
+ Private_Notes: NavigatorScreenParams;
+};
+
+type PublicScreensParamList = {
+ [SCREENS.HOME]: undefined;
+ [SCREENS.TRANSITION_BETWEEN_APPS]: {
+ shouldForceLogin: string;
+ email: string;
+ shortLivedAuthToken: string;
+ exitTo: string;
+ };
+ [SCREENS.VALIDATE_LOGIN]: {
+ accountID: string;
+ validateCode: string;
+ };
+ [SCREENS.UNLINK_LOGIN]: {
+ accountID: string;
+ validateCode: string;
+ };
+ [SCREENS.SIGN_IN_WITH_APPLE_DESKTOP]: undefined;
+ [SCREENS.SIGN_IN_WITH_GOOGLE_DESKTOP]: undefined;
+ [SCREENS.SAML_SIGN_IN]: undefined;
+};
+
+type AuthScreensParamList = {
+ [SCREENS.HOME]: undefined;
+ [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: NavigatorScreenParams;
+ [SCREENS.VALIDATE_LOGIN]: {
+ accountID: string;
+ validateCode: string;
+ };
+ [SCREENS.TRANSITION_BETWEEN_APPS]: {
+ shouldForceLogin: string;
+ email: string;
+ shortLivedAuthToken: string;
+ exitTo: string;
+ };
+ [SCREENS.CONCIERGE]: undefined;
+ [SCREENS.REPORT_ATTACHMENTS]: {
+ reportID: string;
+ source: string;
+ };
+ [SCREENS.NOT_FOUND]: undefined;
+ [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams;
+ [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined;
+ [CONST.DEMO_PAGES.MONEY2020]: undefined;
+};
+
+type RootStackParamList = PublicScreensParamList & AuthScreensParamList;
+
+export type {NavigationRef, StackNavigationAction, CentralPaneNavigatorParamList, RootStackParamList, StateOrRoute, NavigationStateRoute, NavigationRoot};
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 763c8d19f800..d03235a637c7 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -5,7 +5,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import {RecentWaypoint, ReportAction, Transaction} from '@src/types/onyx';
import {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import {EmptyObject} from '@src/types/utils/EmptyObject';
-import {isExpensifyCard} from './CardUtils';
+import {isCorporateCard, isExpensifyCard} from './CardUtils';
import DateUtils from './DateUtils';
import * as NumberUtils from './NumberUtils';
@@ -335,10 +335,11 @@ function isExpensifyCardTransaction(transaction: Transaction): boolean {
}
/**
- * Determine whether a transaction is made with a card.
+ * Determine whether a transaction is made with a card (Expensify or Company Card).
*/
function isCardTransaction(transaction: Transaction): boolean {
- return (transaction?.cardID ?? 0) > 0;
+ const cardID = transaction?.cardID ?? 0;
+ return isCorporateCard(cardID);
}
/**
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index 7c49006c10a5..388020bc0d6d 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -200,7 +200,8 @@ function getAgeRequirementError(date: string, minimumAge: number, maximumAge: nu
* http/https/ftp URL scheme required.
*/
function isValidWebsite(url: string): boolean {
- return new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i').test(url);
+ const isLowerCase = url === url.toLowerCase();
+ return new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i').test(url) && isLowerCase;
}
function validateIdentity(identity: Record): Record {
@@ -296,6 +297,13 @@ function isValidLegalName(name: string): boolean {
return CONST.REGEX.ALPHABETIC_AND_LATIN_CHARS.test(name);
}
+/**
+ * Checks that the provided name doesn't contain special characters or numbers
+ */
+function isValidPersonName(value: string) {
+ return /^[^\d^!#$%*=<>;{}"]+$/.test(value);
+}
+
/**
* Checks if the provided string includes any of the provided reserved words
*/
@@ -406,4 +414,5 @@ export {
isValidAccountRoute,
isValidRecoveryCode,
prepareValues,
+ isValidPersonName,
};
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index accf84002bd3..4de8f1c1f171 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -13,7 +13,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as SessionUtils from '@libs/SessionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
+import ROUTES, {Route} from '@src/ROUTES';
import * as OnyxTypes from '@src/types/onyx';
import {SelectedTimezone} from '@src/types/onyx/PersonalDetails';
import type {OnyxData} from '@src/types/onyx/Request';
@@ -392,7 +392,7 @@ function setUpPoliciesAndNavigate(session: OnyxTypes.Session) {
const isLoggingInAsNewUser = !!session.email && SessionUtils.isLoggingInAsNewUser(currentUrl, session.email);
const url = new URL(currentUrl);
- const exitTo = url.searchParams.get('exitTo');
+ const exitTo = url.searchParams.get('exitTo') as Route | null;
// Approved Accountants and Guides can enter a flow where they make a workspace for other users,
// and those are passed as a search parameter when using transition links
diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js
index 9adcd3803766..68642bd8fdf1 100644
--- a/src/libs/actions/Card.js
+++ b/src/libs/actions/Card.js
@@ -93,7 +93,7 @@ function requestReplacementExpensifyCard(cardId, reason) {
/**
* Activates the physical Expensify card based on the last four digits of the card number
*
- * @param {Number} cardLastFourDigits
+ * @param {String} cardLastFourDigits
* @param {Number} cardID
*/
function activatePhysicalExpensifyCard(cardLastFourDigits, cardID) {
diff --git a/src/libs/actions/TwoFactorAuthActions.ts b/src/libs/actions/TwoFactorAuthActions.ts
index ccd4e2baf662..26e402096aa6 100644
--- a/src/libs/actions/TwoFactorAuthActions.ts
+++ b/src/libs/actions/TwoFactorAuthActions.ts
@@ -1,14 +1,14 @@
import Onyx from 'react-native-onyx';
import Navigation from '@libs/Navigation/Navigation';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
+import ROUTES, {Route} from '@src/ROUTES';
import {TwoFactorAuthStep} from '@src/types/onyx/Account';
/**
* Clear 2FA data if the flow is interrupted without finishing
*/
function clearTwoFactorAuthData() {
- Onyx.merge(ONYXKEYS.ACCOUNT, {recoveryCodes: '', twoFactorAuthSecretKey: '', twoFactorAuthStep: '', codesAreCopied: false});
+ Onyx.merge(ONYXKEYS.ACCOUNT, {recoveryCodes: null, twoFactorAuthSecretKey: null, twoFactorAuthStep: null, codesAreCopied: false});
}
function setTwoFactorAuthStep(twoFactorAuthStep: TwoFactorAuthStep) {
Onyx.merge(ONYXKEYS.ACCOUNT, {twoFactorAuthStep});
@@ -18,9 +18,10 @@ function setCodesAreCopied() {
Onyx.merge(ONYXKEYS.ACCOUNT, {codesAreCopied: true});
}
-function quitAndNavigateBackToSettings() {
+function quitAndNavigateBack(backTo?: Route) {
clearTwoFactorAuthData();
- Navigation.goBack(ROUTES.SETTINGS_SECURITY);
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ Navigation.goBack(backTo || ROUTES.SETTINGS_SECURITY);
}
-export {clearTwoFactorAuthData, setTwoFactorAuthStep, quitAndNavigateBackToSettings, setCodesAreCopied};
+export {clearTwoFactorAuthData, setTwoFactorAuthStep, quitAndNavigateBack, setCodesAreCopied};
diff --git a/src/libs/migrations/PersonalDetailsByAccountID.js b/src/libs/migrations/PersonalDetailsByAccountID.js
index 38c992a6a375..c08ec6fb2c43 100644
--- a/src/libs/migrations/PersonalDetailsByAccountID.js
+++ b/src/libs/migrations/PersonalDetailsByAccountID.js
@@ -257,6 +257,18 @@ export default function () {
delete newReport.participants;
}
+ if (lodashHas(newReport, ['ownerEmail'])) {
+ reportWasModified = true;
+ Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing ownerEmail from report ${newReport.reportID}`);
+ delete newReport.ownerEmail;
+ }
+
+ if (lodashHas(newReport, ['managerEmail'])) {
+ reportWasModified = true;
+ Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing managerEmail from report ${newReport.reportID}`);
+ delete newReport.managerEmail;
+ }
+
if (reportWasModified) {
onyxData[onyxKey] = newReport;
}
diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js
index 43e1847e554a..a4d75a7c73a0 100644
--- a/src/pages/AddPersonalBankAccountPage.js
+++ b/src/pages/AddPersonalBankAccountPage.js
@@ -5,7 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import AddPlaidBankAccount from '@components/AddPlaidBankAccount';
import ConfirmationPage from '@components/ConfirmationPage';
-import Form from '@components/Form';
+import FormProvider from '@components/Form/FormProvider';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import useLocalize from '@hooks/useLocalize';
@@ -113,7 +113,7 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}) {
onButtonPress={() => exitFlow(true)}
/>
) : (
-
-
+
)}
);
diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js
index 569a6f9aa109..97c0f55f27c6 100644
--- a/src/pages/EnablePayments/IdologyQuestions.js
+++ b/src/pages/EnablePayments/IdologyQuestions.js
@@ -1,14 +1,17 @@
import PropTypes from 'prop-types';
-import React, {useState} from 'react';
+import React, {useRef, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import SingleChoiceQuestion from '@components/SingleChoiceQuestion';
+import FixedFooter from '@components/FixedFooter';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import FormScrollView from '@components/FormScrollView';
+import OfflineIndicator from '@components/OfflineIndicator';
+import RadioButtons from '@components/RadioButtons';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
+import * as ErrorUtils from '@libs/ErrorUtils';
import useThemeStyles from '@styles/useThemeStyles';
import * as BankAccounts from '@userActions/BankAccounts';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -48,13 +51,15 @@ const defaultProps = {
walletAdditionalDetails: {},
};
-function IdologyQuestions({questions, idNumber}) {
+function IdologyQuestions({questions, walletAdditionalDetails, idNumber}) {
const styles = useThemeStyles();
+ const formRef = useRef();
const {translate} = useLocalize();
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [shouldHideSkipAnswer, setShouldHideSkipAnswer] = useState(false);
const [userAnswers, setUserAnswers] = useState([]);
+ const [error, setError] = useState('');
const currentQuestion = questions[currentQuestionIndex] || {};
const possibleAnswers = _.filter(
@@ -69,6 +74,7 @@ function IdologyQuestions({questions, idNumber}) {
};
}),
);
+ const errorMessage = ErrorUtils.getLatestErrorMessage(walletAdditionalDetails) || error;
/**
* Put question answer in the state.
@@ -80,6 +86,7 @@ function IdologyQuestions({questions, idNumber}) {
tempAnswers[currentQuestionIndex] = {question: currentQuestion.type, answer};
setUserAnswers(tempAnswers);
+ setError('');
};
/**
@@ -87,37 +94,30 @@ function IdologyQuestions({questions, idNumber}) {
*/
const submitAnswers = () => {
if (!userAnswers[currentQuestionIndex]) {
- return;
- }
- // Get the number of questions that were skipped by the user.
- const skippedQuestionsCount = _.filter(userAnswers, (answer) => answer.answer === SKIP_QUESTION_TEXT).length;
-
- // We have enough answers, let's call expectID KBA to verify them
- if (userAnswers.length - skippedQuestionsCount >= questions.length - MAX_SKIP) {
- const tempAnswers = _.map(userAnswers, _.clone);
-
- // Auto skip any remaining questions
- if (tempAnswers.length < questions.length) {
- for (let i = tempAnswers.length; i < questions.length; i++) {
- tempAnswers[i] = {question: questions[i].type, answer: SKIP_QUESTION_TEXT};
- }
- }
-
- BankAccounts.answerQuestionsForWallet(tempAnswers, idNumber);
- setUserAnswers(tempAnswers);
+ setError(translate('additionalDetailsStep.selectAnswer'));
} else {
- // Else, show next question
- setCurrentQuestionIndex(currentQuestionIndex + 1);
- setShouldHideSkipAnswer(skippedQuestionsCount >= MAX_SKIP);
- }
- };
+ // Get the number of questions that were skipped by the user.
+ const skippedQuestionsCount = _.filter(userAnswers, (answer) => answer.answer === SKIP_QUESTION_TEXT).length;
+
+ // We have enough answers, let's call expectID KBA to verify them
+ if (userAnswers.length - skippedQuestionsCount >= questions.length - MAX_SKIP) {
+ const tempAnswers = _.map(userAnswers, _.clone);
+
+ // Auto skip any remaining questions
+ if (tempAnswers.length < questions.length) {
+ for (let i = tempAnswers.length; i < questions.length; i++) {
+ tempAnswers[i] = {question: questions[i].type, answer: SKIP_QUESTION_TEXT};
+ }
+ }
- const validate = (values) => {
- const errors = {};
- if (!values.answer) {
- errors.answer = translate('additionalDetailsStep.selectAnswer');
+ BankAccounts.answerQuestionsForWallet(tempAnswers, idNumber);
+ setUserAnswers(tempAnswers);
+ } else {
+ // Else, show next question
+ setCurrentQuestionIndex(currentQuestionIndex + 1);
+ setShouldHideSkipAnswer(skippedQuestionsCount >= MAX_SKIP);
+ }
}
- return errors;
};
return (
@@ -131,23 +131,33 @@ function IdologyQuestions({questions, idNumber}) {
{translate('additionalDetailsStep.helpLink')}
-
-
+
+ {currentQuestion.prompt}
+
+
+
+
+ {
+ formRef.current.scrollTo({y: 0, animated: true});
+ }}
+ message={errorMessage}
+ isLoading={walletAdditionalDetails.isLoading}
+ buttonText={translate('common.saveAndContinue')}
+ containerStyles={[styles.mh0, styles.mv0, styles.mb0]}
/>
-
+
+
);
}
diff --git a/src/pages/FlagCommentPage.js b/src/pages/FlagCommentPage.js
index 721bc6742c4b..3695896ea473 100644
--- a/src/pages/FlagCommentPage.js
+++ b/src/pages/FlagCommentPage.js
@@ -19,6 +19,7 @@ import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import reportActionPropTypes from './home/report/reportActionPropTypes';
import withReportAndReportActionOrNotFound from './home/report/withReportAndReportActionOrNotFound';
import reportPropTypes from './reportPropTypes';
@@ -161,7 +162,14 @@ function FlagCommentPage(props) {
>
{({safeAreaPaddingBottomStyle}) => (
-
+ {
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.report.reportID));
+ }}
+ />
{
- Link.openOldDotLink(secureYourAccountUrl);
+ Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID)));
},
icon: Expensicons.Shield,
shouldShowRightIcon: true,
iconRight: Expensicons.NewWindow,
wrapperStyle: [styles.cardMenuItem],
- link: () => Link.buildOldDotURL(secureYourAccountUrl),
},
]}
>
- {props.translate('validationStep.enable2FAText')}
+ {translate('validationStep.enable2FAText')}
);
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
index 284c48c1609a..4620ab1ddcae 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
@@ -520,6 +520,7 @@ class ReimbursementAccountPage extends React.Component {
);
}
diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js
index 69de88f12327..9c1e8345604f 100644
--- a/src/pages/ReimbursementAccount/RequestorStep.js
+++ b/src/pages/ReimbursementAccount/RequestorStep.js
@@ -52,6 +52,14 @@ const validate = (values) => {
}
}
+ if (values.firstName && !ValidationUtils.isValidPersonName(values.firstName)) {
+ errors.firstName = 'bankAccount.error.firstName';
+ }
+
+ if (values.lastName && !ValidationUtils.isValidPersonName(values.lastName)) {
+ errors.lastName = 'bankAccount.error.lastName';
+ }
+
if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) {
errors.ssnLast4 = 'bankAccount.error.ssnLast4';
}
diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js
index 17a66e84e055..b64f8e098a8a 100644
--- a/src/pages/ReimbursementAccount/ValidationStep.js
+++ b/src/pages/ReimbursementAccount/ValidationStep.js
@@ -43,6 +43,9 @@ const propTypes = {
/** If user has two-factor authentication enabled */
requiresTwoFactorAuth: PropTypes.bool,
}),
+
+ /** policyID of the workspace where user is setting up bank account */
+ policyID: PropTypes.string.isRequired,
};
const defaultProps = {
@@ -73,7 +76,7 @@ const filterInput = (amount) => {
return value;
};
-function ValidationStep({reimbursementAccount, translate, onBackButtonPress, account}) {
+function ValidationStep({reimbursementAccount, translate, onBackButtonPress, account, policyID}) {
const styles = useThemeStyles();
/**
* @param {Object} values - form input values passed by the Form component
@@ -180,7 +183,7 @@ function ValidationStep({reimbursementAccount, translate, onBackButtonPress, acc
{!requiresTwoFactorAuth && (
-
+
)}
@@ -211,7 +214,7 @@ function ValidationStep({reimbursementAccount, translate, onBackButtonPress, acc
/>
{reimbursementAccount.shouldShowResetModal && }
- {!requiresTwoFactorAuth && }
+ {!requiresTwoFactorAuth && }
)}
diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js
index 3976f0e07bd5..6d1a1e4db077 100644
--- a/src/pages/ReportDetailsPage.js
+++ b/src/pages/ReportDetailsPage.js
@@ -167,12 +167,8 @@ function ReportDetailsPage(props) {
{
- const topMostReportID = Navigation.getTopmostReportId();
- if (topMostReportID) {
- Navigation.goBack(ROUTES.HOME);
- return;
- }
Navigation.goBack();
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.report.reportID));
}}
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index 7b315ff6c819..5b57419c8530 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -19,14 +19,12 @@ import TaskHeaderActionButton from '@components/TaskHeaderActionButton';
import Text from '@components/Text';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import Tooltip from '@components/Tooltip';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
-import compose from '@libs/compose';
+import useLocalize from '@hooks/useLocalize';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import {getGroupChatName} from '@libs/GroupChatUtils';
import * as HeaderUtils from '@libs/HeaderUtils';
import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector';
import * as OptionsListUtils from '@libs/OptionsListUtils';
-import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import reportPropTypes from '@pages/reportPropTypes';
@@ -60,8 +58,14 @@ const propTypes = {
accountID: PropTypes.number,
}),
- ...windowDimensionsPropTypes,
- ...withLocalizePropTypes,
+ /** The current policy of the report */
+ policy: PropTypes.shape({
+ /** The policy name */
+ name: PropTypes.string,
+
+ /** The URL for the policy avatar */
+ avatar: PropTypes.string,
+ }),
};
const defaultProps = {
@@ -72,9 +76,12 @@ const defaultProps = {
session: {
accountID: 0,
},
+ policy: {},
};
function HeaderView(props) {
+ const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
+ const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
const participants = lodashGet(props.report, 'participantAccountIDs', []);
@@ -97,10 +104,9 @@ function HeaderView(props) {
const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(props.report.reportID);
const isEmptyChat = !props.report.lastMessageText && !props.report.lastMessageTranslationKey && !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey;
const isUserCreatedPolicyRoom = ReportUtils.isUserCreatedPolicyRoom(props.report);
- const policy = useMemo(() => props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`], [props.policies, props.report.policyID]);
- const canLeaveRoom = ReportUtils.canLeaveRoom(props.report, !_.isEmpty(policy));
+ const isPolicyMember = useMemo(() => !_.isEmpty(props.policy), [props.policy]);
+ const canLeaveRoom = ReportUtils.canLeaveRoom(props.report, isPolicyMember);
const isArchivedRoom = ReportUtils.isArchivedRoom(props.report);
- const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(props.report.policyID, props.policies), [props.report.policyID, props.policies]);
// We hide the button when we are chatting with an automated Expensify account since it's not possible to contact
// these users via alternative means. It is possible to request a call with Concierge so we leave the option for them.
@@ -112,7 +118,7 @@ function HeaderView(props) {
if (ReportUtils.isCompletedTaskReport(props.report) && canModifyTask) {
threeDotMenuItems.push({
icon: Expensicons.Checkmark,
- text: props.translate('task.markAsIncomplete'),
+ text: translate('task.markAsIncomplete'),
onSelected: Session.checkIfActionIsAllowed(() => Task.reopenTask(props.report)),
});
}
@@ -121,7 +127,7 @@ function HeaderView(props) {
if (props.report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED && props.report.statusNum !== CONST.REPORT.STATUS.CLOSED && canModifyTask) {
threeDotMenuItems.push({
icon: Expensicons.Trashcan,
- text: props.translate('common.cancel'),
+ text: translate('common.cancel'),
onSelected: Session.checkIfActionIsAllowed(() => Task.cancelTask(props.report.reportID, props.report.reportName, props.report.stateNum, props.report.statusNum)),
});
}
@@ -131,7 +137,7 @@ function HeaderView(props) {
if (props.report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) {
threeDotMenuItems.push({
icon: Expensicons.ChatBubbles,
- text: props.translate('common.join'),
+ text: translate('common.join'),
onSelected: Session.checkIfActionIsAllowed(() =>
Report.updateNotificationPreference(props.report.reportID, props.report.notificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, false),
),
@@ -140,7 +146,7 @@ function HeaderView(props) {
const isWorkspaceMemberLeavingWorkspaceRoom = lodashGet(props.report, 'visibility', '') === CONST.REPORT.VISIBILITY.RESTRICTED && isPolicyMember;
threeDotMenuItems.push({
icon: Expensicons.ChatBubbles,
- text: props.translate('common.leave'),
+ text: translate('common.leave'),
onSelected: Session.checkIfActionIsAllowed(() => Report.leaveRoom(props.report.reportID, isWorkspaceMemberLeavingWorkspaceRoom)),
});
}
@@ -151,7 +157,7 @@ function HeaderView(props) {
if (isConcierge && props.guideCalendarLink) {
threeDotMenuItems.push({
icon: Expensicons.Phone,
- text: props.translate('videoChatButtonAndMenu.tooltip'),
+ text: translate('videoChatButtonAndMenu.tooltip'),
onSelected: Session.checkIfActionIsAllowed(() => {
Link.openExternalLink(props.guideCalendarLink);
}),
@@ -159,14 +165,14 @@ function HeaderView(props) {
} else if (!isAutomatedExpensifyAccount && !isTaskReport && !isArchivedRoom) {
threeDotMenuItems.push({
icon: ZoomIcon,
- text: props.translate('videoChatButtonAndMenu.zoom'),
+ text: translate('videoChatButtonAndMenu.zoom'),
onSelected: Session.checkIfActionIsAllowed(() => {
Link.openExternalLink(CONST.NEW_ZOOM_MEETING_URL);
}),
});
threeDotMenuItems.push({
icon: GoogleMeetIcon,
- text: props.translate('videoChatButtonAndMenu.googleMeet'),
+ text: translate('videoChatButtonAndMenu.googleMeet'),
onSelected: Session.checkIfActionIsAllowed(() => {
Link.openExternalLink(CONST.NEW_GOOGLE_MEET_MEETING_URL);
}),
@@ -179,7 +185,7 @@ function HeaderView(props) {
const defaultSubscriptSize = ReportUtils.isExpenseRequest(props.report) ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT;
const icons = ReportUtils.getIcons(reportHeaderData, props.personalDetails);
const brickRoadIndicator = ReportUtils.hasReportNameError(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '';
- const shouldShowBorderBottom = !isTaskReport || !props.isSmallScreenWidth;
+ const shouldShowBorderBottom = !isTaskReport || !isSmallScreenWidth;
const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(props.report);
const isLoading = !props.report || !title;
@@ -189,21 +195,21 @@ function HeaderView(props) {
style={[styles.appContentHeader, shouldShowBorderBottom && styles.borderBottom]}
dataSet={{dragArea: true}}
>
-
+
{isLoading ? (
) : (
<>
- {props.isSmallScreenWidth && (
+ {isSmallScreenWidth && (
@@ -267,10 +273,10 @@ function HeaderView(props) {
)}
- {isTaskReport && !props.isSmallScreenWidth && ReportUtils.isOpenTaskReport(props.report) && }
+ {isTaskReport && !isSmallScreenWidth && ReportUtils.isOpenTaskReport(props.report) && }
{shouldShowThreeDotsButton && (
@@ -288,25 +294,22 @@ HeaderView.displayName = 'HeaderView';
HeaderView.defaultProps = defaultProps;
export default memo(
- compose(
- withWindowDimensions,
- withLocalize,
- withOnyx({
- guideCalendarLink: {
- key: ONYXKEYS.ACCOUNT,
- selector: (account) => (account && account.guideCalendarLink) || null,
- initialValue: null,
- },
- parentReport: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || report.reportID}`,
- selector: reportWithoutHasDraftSelector,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- },
- }),
- )(HeaderView),
+ withOnyx({
+ guideCalendarLink: {
+ key: ONYXKEYS.ACCOUNT,
+ selector: (account) => (account && account.guideCalendarLink) || null,
+ initialValue: null,
+ },
+ parentReport: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || report.reportID}`,
+ selector: reportWithoutHasDraftSelector,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ policy: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
+ selector: (policy) => _.pick(policy, ['name', 'avatar', 'pendingAction']),
+ },
+ })(HeaderView),
);
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 17d49bd0f486..828a793a8565 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -544,7 +544,7 @@ function ReportActionItem(props) {
{content};
+ return {content};
};
if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
@@ -586,7 +586,6 @@ function ReportActionItem(props) {
${props.translate('parentReportAction.deletedTask')}`} />
diff --git a/src/pages/home/report/ReportActionItemGrouped.js b/src/pages/home/report/ReportActionItemGrouped.js
index 2d55ecbbe037..8c8c51bda3c2 100644
--- a/src/pages/home/report/ReportActionItemGrouped.js
+++ b/src/pages/home/report/ReportActionItemGrouped.js
@@ -1,25 +1,26 @@
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
-import styles from '@styles/styles';
+import stylePropTypes from '@styles/stylePropTypes';
+import useThemeStyles from '@styles/useThemeStyles';
const propTypes = {
/** Children view component for this action item */
children: PropTypes.node.isRequired,
/** Styles for the outermost View */
- // eslint-disable-next-line react/forbid-prop-types
- wrapperStyles: PropTypes.arrayOf(PropTypes.object),
+ wrapperStyle: stylePropTypes,
};
const defaultProps = {
- wrapperStyles: [styles.chatItem],
+ wrapperStyle: undefined,
};
-function ReportActionItemGrouped(props) {
+function ReportActionItemGrouped({wrapperStyle, children}) {
+ const styles = useThemeStyles();
return (
-
- {props.children}
+
+ {children}
);
}
diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js
index e69531e2cc53..9c641d879de3 100644
--- a/src/pages/home/report/ReportActionItemSingle.js
+++ b/src/pages/home/report/ReportActionItemSingle.js
@@ -19,9 +19,10 @@ import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import * as UserUtils from '@libs/UserUtils';
import reportPropTypes from '@pages/reportPropTypes';
-import styles from '@styles/styles';
+import stylePropTypes from '@styles/stylePropTypes';
import * as StyleUtils from '@styles/StyleUtils';
import themeColors from '@styles/themes/default';
+import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import ReportActionItemDate from './ReportActionItemDate';
@@ -33,8 +34,7 @@ const propTypes = {
action: PropTypes.shape(reportActionPropTypes).isRequired,
/** Styles for the outermost View */
- // eslint-disable-next-line react/forbid-prop-types
- wrapperStyles: PropTypes.arrayOf(PropTypes.object),
+ wrapperStyle: stylePropTypes,
/** Children view component for this action item */
children: PropTypes.node.isRequired,
@@ -61,7 +61,7 @@ const propTypes = {
};
const defaultProps = {
- wrapperStyles: [styles.chatItem],
+ wrapperStyle: undefined,
showHeader: true,
shouldShowSubscriptAvatar: false,
hasBeenFlagged: false,
@@ -79,6 +79,7 @@ const showWorkspaceDetails = (reportID) => {
};
function ReportActionItemSingle(props) {
+ const styles = useThemeStyles();
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const actorAccountID = props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport ? props.iouReport.managerID : props.action.actorAccountID;
let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID);
@@ -207,7 +208,7 @@ function ReportActionItemSingle(props) {
const statusTooltipText = formattedDate ? `${statusText} (${formattedDate})` : statusText;
return (
-
+
{
/**
* @param {Boolean} isPaymentItem whether the item being rendered is the payments menu item
- * @returns {Number} the user wallet balance
+ * @returns {String|undefined} the user's wallet balance
*/
- const getWalletBalance = (isPaymentItem) => isPaymentItem && CurrencyUtils.convertToDisplayString(props.userWallet.currentBalance);
+ const getWalletBalance = (isPaymentItem) => (isPaymentItem ? CurrencyUtils.convertToDisplayString(props.userWallet.currentBalance) : undefined);
return (
<>
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
index 5f91414368a0..3b8a1edf48c3 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
@@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
-import {Keyboard, ScrollView, View} from 'react-native';
+import {InteractionManager, Keyboard, ScrollView, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -264,6 +264,11 @@ class ContactMethodDetailsPage extends Component {
title={this.props.translate('contacts.removeContactMethod')}
onConfirm={this.confirmDeleteAndHideModal}
onCancel={() => this.toggleDeleteModal(false)}
+ onModalHide={() => {
+ InteractionManager.runAfterInteractions(() => {
+ this.validateCodeFormRef.current.focusLastSelected();
+ });
+ }}
prompt={this.props.translate('contacts.removeAreYouSure')}
confirmText={this.props.translate('common.yesContinue')}
cancelText={this.props.translate('common.cancel')}
diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
index d5b87fbaf03f..3bb999261d44 100644
--- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
+++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
@@ -89,6 +89,15 @@ function BaseValidateCodeForm(props) {
}
inputValidateCodeRef.current.focus();
},
+ focusLastSelected() {
+ if (!inputValidateCodeRef.current) {
+ return;
+ }
+ if (focusTimeoutRef.current) {
+ clearTimeout(focusTimeoutRef.current);
+ }
+ focusTimeoutRef.current = setTimeout(inputValidateCodeRef.current.focusLastSelected, CONST.ANIMATED_TRANSITION);
+ },
}));
useFocusEffect(
@@ -96,6 +105,9 @@ function BaseValidateCodeForm(props) {
if (!inputValidateCodeRef.current) {
return;
}
+ if (focusTimeoutRef.current) {
+ clearTimeout(focusTimeoutRef.current);
+ }
focusTimeoutRef.current = setTimeout(inputValidateCodeRef.current.focus, CONST.ANIMATED_TRANSITION);
return () => {
if (!focusTimeoutRef.current) {
diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js
index db2be7f5b681..bf1edd7b806d 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.js
+++ b/src/pages/settings/Security/SecuritySettingsPage.js
@@ -44,7 +44,7 @@ function SecuritySettingsPage(props) {
{
translationKey: 'twoFactorAuth.headerTitle',
icon: Expensicons.Shield,
- action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA)),
+ action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute())),
},
{
translationKey: 'closeAccountPage.closeAccount',
diff --git a/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper.js b/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper.js
index bd1068693785..69d662ba6e81 100644
--- a/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper.js
+++ b/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper.js
@@ -11,7 +11,7 @@ import StepWrapperPropTypes from './StepWrapperPropTypes';
function StepWrapper({
title = '',
stepCounter = null,
- onBackButtonPress = TwoFactorAuthActions.quitAndNavigateBackToSettings,
+ onBackButtonPress = () => TwoFactorAuthActions.quitAndNavigateBack(),
children = null,
shouldEnableKeyboardAvoidingView = true,
onEntryTransitionEnd,
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js
index bc85b01cd3da..86d218ec63ae 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js
@@ -24,7 +24,7 @@ import * as TwoFactorAuthActions from '@userActions/TwoFactorAuthActions';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-function CodesStep({account = defaultAccount}) {
+function CodesStep({account = defaultAccount, backTo}) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -50,6 +50,7 @@ function CodesStep({account = defaultAccount}) {
text: translate('twoFactorAuth.stepCodes'),
total: 3,
}}
+ onBackButtonPress={() => TwoFactorAuthActions.quitAndNavigateBack(backTo)}
>
TwoFactorAuthActions.quitAndNavigateBack()}
/>
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js
index c1cb9dc460ea..de36888f30b8 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js
@@ -1,13 +1,24 @@
+import PropTypes from 'prop-types';
import React from 'react';
import ConfirmationPage from '@components/ConfirmationPage';
import LottieAnimations from '@components/LottieAnimations';
import useLocalize from '@hooks/useLocalize';
+import Navigation from '@navigation/Navigation';
import StepWrapper from '@pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper';
import useTwoFactorAuthContext from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth';
import * as TwoFactorAuthActions from '@userActions/TwoFactorAuthActions';
import CONST from '@src/CONST';
-function SuccessStep() {
+const propTypes = {
+ /** The route where user needs to be redirected after setting up 2FA */
+ backTo: PropTypes.string,
+};
+
+const defaultProps = {
+ backTo: '',
+};
+
+function SuccessStep({backTo}) {
const {setStep} = useTwoFactorAuthContext();
const {translate} = useLocalize();
@@ -29,10 +40,16 @@ function SuccessStep() {
onButtonPress={() => {
TwoFactorAuthActions.clearTwoFactorAuthData();
setStep(CONST.TWO_FACTOR_AUTH_STEPS.ENABLED);
+ if (backTo) {
+ Navigation.navigate(backTo);
+ }
}}
/>
);
}
+SuccessStep.propTypes = propTypes;
+SuccessStep.defaultProps = defaultProps;
+
export default SuccessStep;
diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js
index 31a33efa3996..720c2e02b3c0 100644
--- a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js
+++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js
@@ -1,3 +1,5 @@
+import {useRoute} from '@react-navigation/native';
+import lodashGet from 'lodash/get';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import useAnimatedStepContext from '@components/AnimatedStep/useAnimatedStepContext';
@@ -13,6 +15,8 @@ import TwoFactorAuthContext from './TwoFactorAuthContext';
import {defaultAccount, TwoFactorAuthPropTypes} from './TwoFactorAuthPropTypes';
function TwoFactorAuthSteps({account = defaultAccount}) {
+ const route = useRoute();
+ const backTo = lodashGet(route.params, 'backTo', '');
const [currentStep, setCurrentStep] = useState(CONST.TWO_FACTOR_AUTH_STEPS.CODES);
const {setAnimationDirection} = useAnimatedStepContext();
@@ -45,17 +49,17 @@ function TwoFactorAuthSteps({account = defaultAccount}) {
const renderStep = () => {
switch (currentStep) {
case CONST.TWO_FACTOR_AUTH_STEPS.CODES:
- return ;
+ return ;
case CONST.TWO_FACTOR_AUTH_STEPS.VERIFY:
return ;
case CONST.TWO_FACTOR_AUTH_STEPS.SUCCESS:
- return ;
+ return ;
case CONST.TWO_FACTOR_AUTH_STEPS.ENABLED:
return ;
case CONST.TWO_FACTOR_AUTH_STEPS.DISABLED:
return ;
default:
- return ;
+ return ;
}
};
diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js
index e20721b5db4a..3534ef5c064c 100644
--- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js
+++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js
@@ -123,7 +123,7 @@ function ActivatePhysicalCardPage({
return;
}
- CardSettings.activatePhysicalExpensifyCard(Number(lastFourDigits), cardID);
+ CardSettings.activatePhysicalExpensifyCard(lastFourDigits, cardID);
}, [lastFourDigits, cardID, translate]);
if (_.isEmpty(physicalCard)) {
diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js
index 030ca04b7074..4176393e92dd 100644
--- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js
+++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js
@@ -13,7 +13,7 @@ import FormUtils from '@libs/FormUtils';
import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils';
import Navigation from '@libs/Navigation/Navigation';
import assignedCardPropTypes from '@pages/settings/Wallet/assignedCardPropTypes';
-import styles from '@styles/styles';
+import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -113,7 +113,7 @@ const defaultProps = {
session: {},
loginList: {},
isConfirmation: false,
- renderContent: (onSubmit, submitButtonText, children = () => {}, onValidate = () => ({})) => (
+ renderContent: (onSubmit, submitButtonText, styles, children = () => {}, onValidate = () => ({})) => (