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') + 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 && (