diff --git a/.eslintignore b/.eslintignore index 396bfd28c614..d3358a02fe4b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ **/node_modules/* **/dist/* +android/**/build/* .github/actions/**/index.js" docs/vendor/** diff --git a/.eslintrc.js b/.eslintrc.js index 281f8269804e..c0c95d3f5686 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -72,8 +72,8 @@ const restrictedImportPatterns = [ ]; module.exports = { - extends: ['expensify', 'plugin:storybook/recommended', 'plugin:react-hooks/recommended', 'plugin:react-native-a11y/basic', 'plugin:@dword-design/import-alias/recommended', 'prettier'], - plugins: ['react-hooks', 'react-native-a11y'], + extends: ['expensify', 'plugin:storybook/recommended', 'plugin:react-native-a11y/basic', 'plugin:@dword-design/import-alias/recommended', 'prettier'], + plugins: ['react-native-a11y'], parser: 'babel-eslint', ignorePatterns: ['!.*', 'src/vendor', '.github/actions/**/index.js', 'desktop/dist/*.js', 'dist/*.js', 'node_modules/.bin/**', 'node_modules/.cache/**', '.git/**'], env: { @@ -87,6 +87,7 @@ module.exports = { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], plugins: ['react'], rules: { + 'prefer-regex-literals': 'off', 'rulesdir/no-multiple-onyx-in-file': 'off', 'rulesdir/onyx-props-must-have-default': 'off', 'react-native-a11y/has-accessibility-hint': ['off'], @@ -112,6 +113,7 @@ module.exports = { '@styles': './src/styles', // This path is provide alias for files like `ONYXKEYS` and `CONST`. '@src': './src', + '@desktop': './desktop', }, }, ], diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 04de0f5b5deb..818441828bf0 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -198,7 +198,7 @@ jobs: id: pods-cache with: path: ios/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock') }} + key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} restore-keys: ${{ runner.os }}-pods-cache- - name: Compare Podfile.lock and Manifest.lock diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 25a14fb27e1b..9548c3a6e595 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -154,6 +154,7 @@ jobs: echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc - name: Setup Node + id: setup-node uses: ./.github/actions/composite/setupNode - name: Setup XCode @@ -170,7 +171,7 @@ jobs: id: pods-cache with: path: ios/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock') }} + key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} restore-keys: ${{ runner.os }}-pods-cache- - name: Compare Podfile.lock and Manifest.lock @@ -179,7 +180,7 @@ jobs: - name: Install cocoapods uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 - if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' + if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' with: timeout_minutes: 10 max_attempts: 5 diff --git a/.prettierrc.js b/.prettierrc.js index bcc67708cc95..5c9b25ec472a 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -6,7 +6,20 @@ module.exports = { arrowParens: 'always', printWidth: 190, singleAttributePerLine: true, - importOrder: ['@assets/(.*)$', '@components/(.*)$', '@hooks/(.*)$', '@libs/(.*)$', '@navigation/(.*)$', '@pages/(.*)$', '@styles/(.*)$', '@userActions/(.*)$', '@src/(.*)$', '^[./]'], + /** `importOrder` should be defined in an alphabetical order. */ + importOrder: [ + '@assets/(.*)$', + '@components/(.*)$', + '@desktop/(.*)$', + '@hooks/(.*)$', + '@libs/(.*)$', + '@navigation/(.*)$', + '@pages/(.*)$', + '@styles/(.*)$', + '@userActions/(.*)$', + '@src/(.*)$', + '^[./]', + ], importOrderSortSpecifiers: true, importOrderCaseInsensitive: true, }; diff --git a/__mocks__/react-native-localize.ts b/__mocks__/react-native-localize.ts index aa0322d6714c..0188b8afd23f 100644 --- a/__mocks__/react-native-localize.ts +++ b/__mocks__/react-native-localize.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-import-module-exports import mockRNLocalize from 'react-native-localize/mock'; module.exports = mockRNLocalize; diff --git a/__mocks__/react-native-webview.js b/__mocks__/react-native-webview.ts similarity index 55% rename from __mocks__/react-native-webview.js rename to __mocks__/react-native-webview.ts index 58875fd5288b..8266c7b1eda0 100644 --- a/__mocks__/react-native-webview.js +++ b/__mocks__/react-native-webview.ts @@ -1,6 +1,8 @@ +import type {View as RNView} from 'react-native'; + jest.mock('react-native-webview', () => { const {View} = require('react-native'); return { - WebView: () => View, + WebView: () => View as RNView, }; }); diff --git a/android/app/build.gradle b/android/app/build.gradle index cff6f004967e..fb798c33d1a9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001044400 - versionName "1.4.44-0" + versionCode 1001044412 + versionName "1.4.44-12" } flavorDimensions "default" diff --git a/assets/images/workspace-profile-light.png b/assets/images/workspace-profile-light.png new file mode 100644 index 000000000000..7e82c98656d2 Binary files /dev/null and b/assets/images/workspace-profile-light.png differ diff --git a/assets/images/workspace-profile.png b/assets/images/workspace-profile.png index 72112566e35f..df1f6f9fd645 100644 Binary files a/assets/images/workspace-profile.png and b/assets/images/workspace-profile.png differ diff --git a/babel.config.js b/babel.config.js index d3bcecdae8cb..2a09d086dc5c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -68,6 +68,7 @@ const metro = { // This path is provide alias for files like `ONYXKEYS` and `CONST`. '@src': './src', '@userActions': './src/libs/actions', + '@desktop': './desktop', }, }, ], diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 209a4c0c2753..170198987793 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -219,6 +219,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ // This path is provide alias for files like `ONYXKEYS` and `CONST`. '@src': path.resolve(__dirname, '../../src/'), '@userActions': path.resolve(__dirname, '../../src/libs/actions/'), + '@desktop': path.resolve(__dirname, '../../desktop'), }, // React Native libraries may have web-specific module implementations that appear with the extension `.web.js` diff --git a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md index 16e628acbeee..043cc4be1e26 100644 --- a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md @@ -81,7 +81,7 @@ Follow the below steps to run reconciliation on the Expensify Card settlements: - Entry ID: a unique number grouping card payments and transactions settled by those payments - Amount: the amount debited from the Business Bank Account for payments - Merchant: the business where a purchase was made - - Card: refers to the Expensify credit card number and cardholder's email address + - Card: refers to the Expensify Card number and cardholder's email address - Business Account: the business bank account connected to Expensify that the settlement is paid from - Transaction ID: a special ID that helps Expensify support locate transactions if there's an issue diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index e4e898423238..f62dd79f5ce5 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.44.0 + 1.4.44.12 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 661432acfd4c..ddbd21b2842a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.44.0 + 1.4.44.12 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index cdd031218474..4cffc1f6179a 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.44 CFBundleVersion - 1.4.44.0 + 1.4.44.12 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index dc8eb94eeb3f..12c0c99c0d9a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1432,7 +1432,7 @@ PODS: - React-Core - RNReactNativeHapticFeedback (2.2.0): - React-Core - - RNReanimated (3.6.1): + - RNReanimated (3.7.1): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1986,7 +1986,7 @@ SPEC CHECKSUMS: rnmapbox-maps: fcf7f1cbdc8bd7569c267d07284e8a5c7bee06ed RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 - RNReanimated: 57f436e7aa3d277fbfed05e003230b43428157c0 + RNReanimated: beb07f7f900543928467da8107c175d1e57a1049 RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a diff --git a/jest/setup.ts b/jest/setup.ts index 4a23a85edb83..11b0d77ed7ac 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -31,7 +31,12 @@ jest.spyOn(console, 'debug').mockImplementation((...params) => { // This mock is required for mocking file systems when running tests jest.mock('react-native-fs', () => ({ - unlink: jest.fn(() => new Promise((res) => res())), + unlink: jest.fn( + () => + new Promise((res) => { + res(); + }), + ), CachesDirectoryPath: jest.fn(), })); diff --git a/package-lock.json b/package-lock.json index 3f7bb61b64e0..2aa676e920ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.44-0", + "version": "1.4.44-12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.44-0", + "version": "1.4.44-12", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -38,7 +38,6 @@ "@react-native-picker/picker": "2.5.1", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.8", - "@react-navigation/native-stack": "^6.9.17", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.1.11", @@ -53,7 +52,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -98,7 +97,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.6", + "react-native-onyx": "2.0.7", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -107,7 +106,7 @@ "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.6.1", + "react-native-reanimated": "^3.7.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", @@ -207,13 +206,12 @@ "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.43", + "eslint-config-expensify": "^2.0.44", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", "eslint-plugin-jsdoc": "^46.2.6", "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.5.13", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", @@ -451,6 +449,35 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/eslint-parser": { + "version": "7.23.10", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.10.tgz", + "integrity": "sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==", + "dev": true, + "peer": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", @@ -7325,20 +7352,71 @@ "license": "MIT" }, "node_modules/@lwc/eslint-plugin-lwc": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@lwc/eslint-plugin-lwc/-/eslint-plugin-lwc-0.11.0.tgz", - "integrity": "sha512-wJOD4XWOH91GaZfypMSKfEeMXqMfvKdsb2gSJ/9FEwJVlziKg1aagtRYJh2ln3DyEZV33tBC/p/dWzIeiwa1tg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@lwc/eslint-plugin-lwc/-/eslint-plugin-lwc-1.7.2.tgz", + "integrity": "sha512-fvdW/yvkNfqgt2Cc4EJCRYE55QJVNXdDaVTHRk5i1kkKP2Xj3GG0nAsYwXYqApEeRpUTpUZljPlO29/SWRXJoA==", "dev": true, - "license": "MIT", "dependencies": { - "minimatch": "^3.0.4" + "globals": "^13.24.0", + "minimatch": "^9.0.3" }, "engines": { "node": ">=10.0.0" }, "peerDependencies": { - "babel-eslint": "^10", - "eslint": "^6 || ^7" + "@babel/eslint-parser": "^7", + "eslint": "^7 || ^8" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@malept/cross-spawn-promise": { @@ -7690,6 +7768,16 @@ "uuid": "8.3.2" } }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "peer": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10257,17 +10345,6 @@ "react": "*" } }, - "node_modules/@react-navigation/elements": { - "version": "1.3.21", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.21.tgz", - "integrity": "sha512-eyS2C6McNR8ihUoYfc166O1D8VYVh9KIl0UQPI8/ZJVsStlfSTgeEEh+WXge6+7SFPnZ4ewzEJdSAHH+jzcEfg==", - "peerDependencies": { - "@react-navigation/native": "^6.0.0", - "react": "*", - "react-native": "*", - "react-native-safe-area-context": ">= 3.0.0" - } - }, "node_modules/@react-navigation/material-top-tabs": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-6.6.3.tgz", @@ -10299,22 +10376,6 @@ "react-native": "*" } }, - "node_modules/@react-navigation/native-stack": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.9.17.tgz", - "integrity": "sha512-X8p8aS7JptQq7uZZNFEvfEcPf6tlK4PyVwYDdryRbG98B4bh2wFQYMThxvqa+FGEN7USEuHdv2mF0GhFKfX0ew==", - "dependencies": { - "@react-navigation/elements": "^1.3.21", - "warn-once": "^0.1.0" - }, - "peerDependencies": { - "@react-navigation/native": "^6.0.0", - "react": "*", - "react-native": "*", - "react-native-safe-area-context": ">= 3.0.0", - "react-native-screens": ">= 3.0.0" - } - }, "node_modules/@react-navigation/routers": { "version": "6.1.9", "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz", @@ -10341,6 +10402,17 @@ "react-native-screens": ">= 3.0.0" } }, + "node_modules/@react-navigation/stack/node_modules/@react-navigation/elements": { + "version": "1.3.17", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.17.tgz", + "integrity": "sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA==", + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0" + } + }, "node_modules/@react-ng/bounds-observer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@react-ng/bounds-observer/-/bounds-observer-0.2.1.tgz", @@ -22873,24 +22945,6 @@ "node": ">=10" } }, - "node_modules/aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha512-majUxHgLehQTeSA+hClx+DY09OVUqG3GtezWkF1krgLGNdlDu9l9V8DaqNMWbq4Eddc8wsyDA0hpDUtnYxQEXw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, - "node_modules/aria-query/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -23351,13 +23405,6 @@ "node": ">=4" } }, - "node_modules/axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -25641,13 +25688,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true, - "license": "MIT" - }, "node_modules/charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", @@ -25988,16 +26028,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 10" - } - }, "node_modules/clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -29507,6 +29537,27 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, + "engines": { + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" + } + }, "node_modules/eslint-config-airbnb-base": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", @@ -29551,579 +29602,24 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.43", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.43.tgz", - "integrity": "sha512-kLd6NyYbyb3mCB6VH6vu49/RllwNo0rdXcLUUGB7JGny+2N19jOmBJ4/GLKsbpFzvEZEghXfn7BITPRkxVJcgg==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.44.tgz", + "integrity": "sha512-fwa7lcQk7llYgqcWA1TX4kcSigYqSVkKGk+anODwYlYSbVbXwzzkQsncsaiWVTM7+eJdk46GmWPeiMAWOGWPvw==", "dev": true, "dependencies": { - "@lwc/eslint-plugin-lwc": "^0.11.0", + "@lwc/eslint-plugin-lwc": "^1.7.2", "babel-eslint": "^10.1.0", - "eslint": "6.8.0", - "eslint-config-airbnb": "18.0.1", - "eslint-config-airbnb-base": "14.0.0", + "eslint": "^7.32.0", + "eslint-config-airbnb": "19.0.4", + "eslint-config-airbnb-base": "15.0.0", "eslint-plugin-es": "^4.1.0", "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.18.0", - "eslint-plugin-rulesdir": "^0.2.0", - "lodash": "^4.17.21", - "underscore": "^1.13.1" - } - }, - "node_modules/eslint-config-expensify/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint-config-expensify/node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/eslint-config-expensify/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/eslint-config-expensify/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-config-expensify/node_modules/eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-config-airbnb": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", - "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-config-airbnb-base": "^14.0.0", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2", "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^1.7.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-config-airbnb-base": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", - "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "confusing-browser-globals": "^1.0.7", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.0.tgz", - "integrity": "sha512-p+PGoGeV4SaZRDsXqdj9OWcOrOpZn8gXoGPcIQTzo2IDMbAKhNDnME9myZWqO3Ic4R3YmwAZ1lDjWl2R2hMUVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.1", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.3", - "object.entries": "^1.1.1", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "resolve": "^1.14.2" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react-hooks": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", - "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=7" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true, - "license": "ISC" - }, - "node_modules/eslint-config-expensify/node_modules/globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-config-expensify/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint-config-expensify/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-config-expensify/node_modules/jsx-ast-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", - "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.1", - "object.assign": "^4.1.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.5.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-config-expensify/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "eslint-plugin-react": "^7.18.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-rulesdir": "^0.2.2", + "lodash": "^4.17.21", + "underscore": "^1.13.6" } }, "node_modules/eslint-config-prettier": { @@ -30584,9 +30080,10 @@ } }, "node_modules/eslint-plugin-rulesdir": { - "version": "0.2.1", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.2.tgz", + "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -31261,8 +30758,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", - "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", + "integrity": "sha512-nAe0fPbfRn/VYHe6mCp/APmMbda/NiHE3aZq7q0kWhPmz1LVTukeaREmZ7SN8auyLOy9/mS0RIQLeV0AR8vsrA==", "license": "MIT", "dependencies": { "classnames": "2.4.0", @@ -31748,47 +31245,6 @@ "node": ">=0.10.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -32071,32 +31527,6 @@ "dev": true, "license": "ISC" }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -34671,107 +34101,6 @@ "css-in-js-utils": "^2.0.0" } }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -41682,13 +41011,6 @@ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" - }, "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -45168,9 +44490,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.6.tgz", - "integrity": "sha512-qsCxvNKc+mq/Y74v6Twe7VZxqgfpjBm0997R8OEtCUJEtgAp0riCQ3GvuVVIWYALz3S+ADokEAEPzeFW2frtpw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.7.tgz", + "integrity": "sha512-UGMUTSFxYEzNn3wuCGzaf0t6D5XwcE+3J2pYj7wPlbskdcHVLijZZEwgSSDBF7hgNfCuZ+ImetskPNktnf9hkg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -45308,9 +44630,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz", - "integrity": "sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.7.1.tgz", + "integrity": "sha512-bapCxhnS58+GZynQmA/f5U8vRlmhXlI/WhYg0dqnNAGXHNIc+38ahRWcG8iK8e0R2v9M8Ky2ZWObEC6bmweofg==", "dependencies": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", @@ -47425,16 +46747,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", @@ -53240,19 +52552,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/write-file-atomic": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", @@ -53264,19 +52563,6 @@ "signal-exit": "^3.0.2" } }, - "node_modules/write/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ws": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", diff --git a/package.json b/package.json index d79437b1e65b..e054ea423388 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.44-0", + "version": "1.4.44-12", "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.", @@ -86,7 +86,6 @@ "@react-native-picker/picker": "2.5.1", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.8", - "@react-navigation/native-stack": "^6.9.17", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.1.11", @@ -101,7 +100,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -146,7 +145,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.6", + "react-native-onyx": "2.0.7", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -155,7 +154,7 @@ "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.6.1", + "react-native-reanimated": "^3.7.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", @@ -255,13 +254,12 @@ "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.43", + "eslint-config-expensify": "^2.0.44", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", "eslint-plugin-jsdoc": "^46.2.6", "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.5.13", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", diff --git a/patches/@react-navigation+native-stack+6.9.17.patch b/patches/@react-navigation+native-stack+6.9.17.patch deleted file mode 100644 index 933ca6ce792e..000000000000 --- a/patches/@react-navigation+native-stack+6.9.17.patch +++ /dev/null @@ -1,39 +0,0 @@ -diff --git a/node_modules/@react-navigation/native-stack/src/types.tsx b/node_modules/@react-navigation/native-stack/src/types.tsx -index 206fb0b..7a34a8e 100644 ---- a/node_modules/@react-navigation/native-stack/src/types.tsx -+++ b/node_modules/@react-navigation/native-stack/src/types.tsx -@@ -490,6 +490,14 @@ export type NativeStackNavigationOptions = { - * Only supported on iOS and Android. - */ - freezeOnBlur?: boolean; -+ // partial changes from https://github.com/react-navigation/react-navigation/commit/90cfbf23bcc5259f3262691a9eec6c5b906e5262 -+ // patch can be removed when new version of `native-stack` will be released -+ /** -+ * Whether the keyboard should hide when swiping to the previous screen. Defaults to `false`. -+ * -+ * Only supported on iOS -+ */ -+ keyboardHandlingEnabled?: boolean; - }; - - export type NativeStackNavigatorProps = DefaultNavigatorOptions< -diff --git a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx -index a005c43..03d8b50 100644 ---- a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx -+++ b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx -@@ -161,6 +161,7 @@ const SceneView = ({ - statusBarTranslucent, - statusBarColor, - freezeOnBlur, -+ keyboardHandlingEnabled, - } = options; - - let { -@@ -289,6 +290,7 @@ const SceneView = ({ - onNativeDismissCancelled={onNativeDismissCancelled} - // this prop is available since rn-screens 3.16 - freezeOnBlur={freezeOnBlur} -+ hideKeyboardOnSwipe={keyboardHandlingEnabled} - > - - diff --git a/patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch similarity index 100% rename from patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch rename to patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch diff --git a/patches/react-native-reanimated+3.6.1.patch b/patches/react-native-reanimated+3.7.1.patch similarity index 100% rename from patches/react-native-reanimated+3.6.1.patch rename to patches/react-native-reanimated+3.7.1.patch diff --git a/src/CONST.ts b/src/CONST.ts index 8abd4c087b16..cf0b54bceb42 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -178,6 +178,7 @@ const CONST = { DATE: { SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss', FNS_FORMAT_STRING: 'yyyy-MM-dd', + FNS_DATE_TIME_FORMAT_STRING: 'yyyy-MM-dd HH:mm:ss', LOCAL_TIME_FORMAT: 'h:mm a', YEAR_MONTH_FORMAT: 'yyyyMM', MONTH_FORMAT: 'MMMM', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f2d606bd62a6..d4a0b8a21d66 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -272,6 +272,9 @@ const ONYXKEYS = { /** Indicates whether we should store logs or not */ SHOULD_STORE_LOGS: 'shouldStoreLogs', + // Paths of PDF file that has been cached during one session + CACHED_PDF_PATHS: 'cachedPDFPaths', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -564,6 +567,7 @@ type OnyxValuesMapping = { [ONYXKEYS.PLAID_CURRENT_EVENT]: string; [ONYXKEYS.LOGS]: Record; [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; + [ONYXKEYS.CACHED_PDF_PATHS]: Record; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; @@ -582,4 +586,4 @@ type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: type AssertOnyxKeys = AssertTypesEqual; export default ONYXKEYS; -export type {OnyxValues, OnyxKey, OnyxCollectionKey, OnyxValue, OnyxValueKey, OnyxFormKey, OnyxFormValuesMapping, OnyxFormDraftKey}; +export type {OnyxValues, OnyxKey, OnyxCollectionKey, OnyxValue, OnyxValueKey, OnyxFormKey, OnyxFormValuesMapping, OnyxFormDraftKey, OnyxCollectionValuesMapping}; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a8786bda3ffb..22ebffd52eec 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -526,6 +526,11 @@ const ROUTES = { route: 'workspace/:policyID/categories', getRoute: (policyID: string) => `workspace/${policyID}/categories` as const, }, + WORKSPACE_CATEGORIES_SETTINGS: { + route: 'workspace/:policyID/categories/settings', + getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, + }, + // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 520895c89c98..ac75968e68b9 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -219,6 +219,7 @@ const SCREENS = { DESCRIPTION: 'Workspace_Profile_Description', SHARE: 'Workspace_Profile_Share', NAME: 'Workspace_Profile_Name', + CATEGORIES_SETTINGS: 'Categories_Settings', }, EDIT_REQUEST: { diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 8ad26e5a7c46..4f21f4aa1dc3 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -271,6 +271,7 @@ function AddressSearch( }; const renderHeaderComponent = () => ( + // eslint-disable-next-line react/jsx-no-useless-fragment <> {(predefinedPlaces?.length ?? 0) > 0 && ( <> diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index ab39e5379230..7f0178863fc9 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -89,7 +89,7 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { source?: AvatarSource; /** Optional callback to fire when we want to preview an image and approve it for use. */ - onConfirm?: ((file: Partial) => void) | null; + onConfirm?: ((file: FileObject) => void) | null; /** Whether the modal should be open by default */ defaultOpen?: boolean; @@ -264,7 +264,7 @@ function AttachmentModal({ } if (onConfirm) { - onConfirm(Object.assign(file ?? {}, {source: sourceState})); + onConfirm(Object.assign(file ?? {}, {source: sourceState} as FileObject)); } setIsModalOpen(false); @@ -318,7 +318,7 @@ function AttachmentModal({ const validateAndDisplayFileToUpload = useCallback( (data: FileObject) => { - if (!isDirectoryCheck(data)) { + if (!data || !isDirectoryCheck(data)) { return; } let fileObject = data; @@ -617,4 +617,4 @@ export default withOnyx({ }, })(memo(AttachmentModal)); -export type {Attachment}; +export type {Attachment, FileObject}; diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index d4d3d0696c59..0387ee087127 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -1,3 +1,4 @@ +import Str from 'expensify-common/lib/str'; import lodashCompact from 'lodash/compact'; import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef, useState} from 'react'; @@ -230,6 +231,28 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { setIsVisible(false); }; + /** + * @param {Object} fileData + * @returns {Promise} + */ + const validateAndCompleteAttachmentSelection = useCallback( + (fileData) => { + if (fileData.width === -1 || fileData.height === -1) { + showImageCorruptionAlert(); + return Promise.resolve(); + } + return getDataForUpload(fileData) + .then((result) => { + completeAttachmentSelection.current(result); + }) + .catch((error) => { + showGeneralAlert(error.message); + throw error; + }); + }, + [showGeneralAlert, showImageCorruptionAlert], + ); + /** * Handles the image/document picker result and * sends the selected attachment to the caller (parent component) @@ -244,24 +267,17 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { return Promise.resolve(); } const fileData = _.first(attachments); - RNImage.getSize(fileData.uri, (width, height) => { - fileData.width = width; - fileData.height = height; - if (fileData.width === -1 || fileData.height === -1) { - showImageCorruptionAlert(); - return Promise.resolve(); - } - return getDataForUpload(fileData) - .then((result) => { - completeAttachmentSelection.current(result); - }) - .catch((error) => { - showGeneralAlert(error.message); - throw error; - }); - }); + if (Str.isImage(fileData.fileName || fileData.name)) { + RNImage.getSize(fileData.fileCopyUri || fileData.uri, (width, height) => { + fileData.width = width; + fileData.height = height; + return validateAndCompleteAttachmentSelection(fileData); + }); + } else { + return validateAndCompleteAttachmentSelection(fileData); + } }, - [showGeneralAlert, showImageCorruptionAlert], + [validateAndCompleteAttachmentSelection], ); /** diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index edc8ab11fd27..b2c9fed64467 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -105,6 +105,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) { isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} transactionID={item.transactionID} + reportActionID={item.reportActionID} isHovered={isModalHovered} isFocused={isFocused} optionalVideoDuration={item.duration} diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index a4d3e1392095..ed618151e594 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -106,47 +106,52 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, Navigation.goBack(); }, []); - return ( - - {page == null ? ( + const containerStyles = [styles.flex1, styles.attachmentCarouselContainer]; + + if (page == null) { + return ( + + + ); + } + + return ( + + {page === -1 ? ( + ) : ( <> - {page === -1 ? ( - - ) : ( - <> - cycleThroughAttachments(-1)} - onForward={() => cycleThroughAttachments(1)} - autoHideArrow={autoHideArrows} - cancelAutoHideArrow={cancelAutoHideArrows} - /> - - updatePage(newPage)} - onClose={goBack} - ref={pagerRef} - /> - - )} + cycleThroughAttachments(-1)} + onForward={() => cycleThroughAttachments(1)} + autoHideArrow={autoHideArrows} + cancelAutoHideArrow={cancelAutoHideArrows} + /> + + updatePage(newPage)} + onClose={goBack} + ref={pagerRef} + /> )} ); } + AttachmentCarousel.propTypes = propTypes; AttachmentCarousel.defaultProps = defaultProps; AttachmentCarousel.displayName = 'AttachmentCarousel'; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index c871628f65e7..56425f64a51c 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -17,6 +17,7 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import compose from '@libs/compose'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -57,6 +58,9 @@ const propTypes = { // eslint-disable-next-line react/no-unused-prop-types transactionID: PropTypes.string, + /** The id of the report action related to the attachment */ + reportActionID: PropTypes.string, + isHovered: PropTypes.bool, optionalVideoDuration: PropTypes.number, @@ -71,6 +75,7 @@ const defaultProps = { isWorkspaceAvatar: false, maybeIcon: false, transactionID: '', + reportActionID: '', isHovered: false, optionalVideoDuration: 0, }; @@ -92,6 +97,7 @@ function AttachmentView({ maybeIcon, fallbackSource, transaction, + reportActionID, isHovered, optionalVideoDuration, }) { @@ -153,6 +159,15 @@ function AttachmentView({ if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; + const onPDFLoadComplete = (path) => { + if (path && (transaction.transactionID || reportActionID)) { + CachedPDFPaths.add(transaction.transactionID || reportActionID, path); + } + if (!loadComplete) { + setLoadComplete(true); + } + }; + // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -166,7 +181,7 @@ function AttachmentView({ encryptedSourceUrl={encryptedSourceUrl} onPress={onPress} onToggleKeyboard={onToggleKeyboard} - onLoadComplete={() => !loadComplete && setLoadComplete(true)} + onLoadComplete={onPDFLoadComplete} errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index fa8a6d71516f..4388ebb8f815 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -220,7 +220,7 @@ function AvatarWithImagePicker({ setError(null, {}); setIsMenuVisible(false); setImageData({ - uri: image.uri, + uri: image.uri ?? '', name: image.name, type: image.type, }); diff --git a/src/components/ButtonWithDropdownMenu.tsx b/src/components/ButtonWithDropdownMenu.tsx index 9466da601825..8aa3a5f0b9f0 100644 --- a/src/components/ButtonWithDropdownMenu.tsx +++ b/src/components/ButtonWithDropdownMenu.tsx @@ -10,33 +10,30 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PopoverMenu from './PopoverMenu'; -type PaymentType = DeepValueOf; - -type DropdownOption = { - value: PaymentType; +type DropdownOption = { + value: T; text: string; - icon: IconAsset; + icon?: IconAsset; iconWidth?: number; iconHeight?: number; iconDescription?: string; }; -type ButtonWithDropdownMenuProps = { +type ButtonWithDropdownMenuProps = { /** Text to display for the menu header */ menuHeaderText?: string; /** Callback to execute when the main button is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: PaymentType) => void; + onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: T) => void; /** Callback to execute when a dropdown option is selected */ - onOptionSelected?: (option: DropdownOption) => void; + onOptionSelected?: (option: DropdownOption) => void; /** Call the onPress function on main button when Enter key is pressed */ pressOnEnter?: boolean; @@ -55,19 +52,19 @@ type ButtonWithDropdownMenuProps = { /** Menu options to display */ /** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */ - options: DropdownOption[]; + options: Array>; /** The anchor alignment of the popover menu */ anchorAlignment?: AnchorAlignment; /* ref for the button */ - buttonRef: RefObject; + buttonRef?: RefObject; /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ enterKeyEventListenerPriority?: number; }; -function ButtonWithDropdownMenu({ +function ButtonWithDropdownMenu({ isLoading = false, isDisabled = false, pressOnEnter = false, @@ -83,7 +80,7 @@ function ButtonWithDropdownMenu({ options, onOptionSelected, enterKeyEventListenerPriority = 0, -}: ButtonWithDropdownMenuProps) { +}: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js index 2374fc9e5d0c..89312a7ca614 100644 --- a/src/components/CategoryPicker/index.js +++ b/src/components/CategoryPicker/index.js @@ -70,6 +70,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC onSelectRow={onSubmit} ListItem={RadioListItem} initiallyFocusedOptionKey={selectedOptionKey} + isRowMultilineSupported /> ); } diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 516de55c73ba..b6443f3ca385 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -75,7 +75,7 @@ function Composer( shouldContainScroll = false, ...props }: ComposerProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -278,6 +278,7 @@ function Composer( if (!onKeyPress || isEnterWhileComposition(e as unknown as KeyboardEvent)) { return; } + onKeyPress(e); }, [onKeyPress], diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index d8d88970ea78..6bc44aba69cd 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,11 +1,11 @@ -import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; +import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; type TextSelection = { start: number; end?: number; }; -type ComposerProps = { +type ComposerProps = TextInputProps & { /** identify id in the text input */ id?: string; @@ -31,7 +31,7 @@ type ComposerProps = { onNumberOfLinesChange?: (numberOfLines: number) => void; /** Callback method to handle pasting a file */ - onPasteFile?: (file?: File) => void; + onPasteFile?: (file: File) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types @@ -74,12 +74,6 @@ type ComposerProps = { /** Whether the sull composer is open */ isComposerFullSize?: boolean; - onKeyPress?: (event: NativeSyntheticEvent) => void; - - onFocus?: (event: NativeSyntheticEvent) => void; - - onBlur?: (event: NativeSyntheticEvent) => void; - /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; }; diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index aaa43e33744d..da8c0ed86a84 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -25,17 +25,29 @@ type ConfirmedRoutePropsOnyxProps = { type ConfirmedRouteProps = ConfirmedRoutePropsOnyxProps & { /** Transaction that stores the distance request data */ - transaction: Transaction; + transaction: Transaction | undefined; }; function ConfirmedRoute({mapboxAccessToken, transaction}: ConfirmedRouteProps) { const {isOffline} = useNetwork(); - const {route0: route} = transaction.routes ?? {}; - const waypoints = transaction.comment?.waypoints ?? {}; + const {route0: route} = transaction?.routes ?? {}; + const waypoints = transaction?.comment?.waypoints ?? {}; const coordinates = route?.geometry?.coordinates ?? []; const theme = useTheme(); const styles = useThemeStyles(); + const getMarkerComponent = useCallback( + (icon: IconAsset): ReactNode => ( + + ), + [theme], + ); + const getWaypointMarkers = useCallback( (waypointsData: WaypointCollection): WayPoint[] => { const numberOfWaypoints = Object.keys(waypointsData).length; @@ -60,19 +72,12 @@ function ConfirmedRoute({mapboxAccessToken, transaction}: ConfirmedRouteProps) { return { id: `${waypoint.lng},${waypoint.lat},${index}`, coordinate: [waypoint.lng, waypoint.lat] as const, - markerComponent: (): ReactNode => ( - - ), + markerComponent: (): ReactNode => getMarkerComponent(MarkerComponent), }; }) .filter((waypoint): waypoint is WayPoint => !!waypoint); }, - [theme], + [getMarkerComponent], ); const waypointMarkers = getWaypointMarkers(waypoints); @@ -82,26 +87,22 @@ function ConfirmedRoute({mapboxAccessToken, transaction}: ConfirmedRouteProps) { return MapboxToken.stop; }, []); - return ( - <> - {!isOffline && Boolean(mapboxAccessToken?.token) ? ( - } - style={[styles.mapView, styles.br4]} - waypoints={waypointMarkers} - styleURL={CONST.MAPBOX.STYLE_URL} - /> - ) : ( - - )} - + return !isOffline && Boolean(mapboxAccessToken?.token) ? ( + } + style={[styles.mapView, styles.br4]} + waypoints={waypointMarkers} + styleURL={CONST.MAPBOX.STYLE_URL} + /> + ) : ( + ); } diff --git a/src/components/CustomDevMenu/index.native.tsx b/src/components/CustomDevMenu/index.native.tsx index 54f1336b4fef..968f97b9e91f 100644 --- a/src/components/CustomDevMenu/index.native.tsx +++ b/src/components/CustomDevMenu/index.native.tsx @@ -8,6 +8,7 @@ const CustomDevMenu: CustomDevMenuElement = Object.assign( useEffect(() => { DevMenu.addItem('Open Test Preferences', toggleTestToolsModal); }, []); + // eslint-disable-next-line react/jsx-no-useless-fragment return <>; }, { diff --git a/src/components/CustomDevMenu/index.tsx b/src/components/CustomDevMenu/index.tsx index 4306d0cae090..6c33f2868b9d 100644 --- a/src/components/CustomDevMenu/index.tsx +++ b/src/components/CustomDevMenu/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type CustomDevMenuElement from './types'; +// eslint-disable-next-line react/jsx-no-useless-fragment const CustomDevMenu: CustomDevMenuElement = Object.assign(() => <>, {displayName: 'CustomDevMenu'}); export default CustomDevMenu; diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 4535acc734af..356fbd3726a3 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -1,8 +1,10 @@ import React, {useCallback, useContext, useEffect, useRef, useState} from 'react'; import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; +import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import {navigationRef} from '@libs/Navigation/Navigation'; import StatusBar from '@libs/StatusBar'; +import type {StatusBarStyle} from '@styles/index'; import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext'; import updateGlobalBackgroundColor from './updateGlobalBackgroundColor'; import updateStatusBarAppearance from './updateStatusBarAppearance'; @@ -16,7 +18,7 @@ type CustomStatusBarAndBackgroundProps = { function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBackgroundProps) { const {isRootStatusBarEnabled, setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const theme = useTheme(); - const [statusBarStyle, setStatusBarStyle] = useState(theme.statusBarStyle); + const [statusBarStyle, setStatusBarStyle] = useState(); const isDisabled = !isNested && !isRootStatusBarEnabled; @@ -34,6 +36,8 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack }; }, [isNested, setRootStatusBarEnabled]); + const didForceUpdateStatusBarRef = useRef(false); + const prevIsRootStatusBarEnabled = usePrevious(isRootStatusBarEnabled); // The prev and current status bar background color refs are initialized with the splash screen background color so the status bar color is changed from the splash screen color to the expected color atleast once on first render - https://github.com/Expensify/App/issues/34154 const prevStatusBarBackgroundColor = useRef(theme.splashBG); const statusBarBackgroundColor = useRef(theme.splashBG); @@ -57,11 +61,11 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack // Updates the status bar style and background color depending on the current route and theme // This callback is triggered everytime the route changes or the theme changes const updateStatusBarStyle = useCallback( - (listenerId?: number) => { + (listenerID?: number) => { // Check if this function is either called through the current navigation listener // react-navigation library has a bug internally, where it can't keep track of the listeners, therefore, sometimes when the useEffect would re-render and we run navigationRef.removeListener the listener isn't removed and we end up with two or more listeners. // https://github.com/Expensify/App/issues/34154#issuecomment-1898519399 - if (listenerId !== undefined && listenerId !== listenerCount.current) { + if (listenerID !== undefined && listenerID !== listenerCount.current) { return; } @@ -97,27 +101,39 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack } // Don't update the status bar style if it's the same as the current one, to prevent flashing. - if (newStatusBarStyle !== statusBarStyle) { + // Force update if the root status bar is back on active or it won't overwirte the nested status bar style + if ((!didForceUpdateStatusBarRef.current && !prevIsRootStatusBarEnabled && isRootStatusBarEnabled) || newStatusBarStyle !== statusBarStyle) { updateStatusBarAppearance({statusBarStyle: newStatusBarStyle}); setStatusBarStyle(newStatusBarStyle); + + if (!prevIsRootStatusBarEnabled && isRootStatusBarEnabled) { + didForceUpdateStatusBarRef.current = true; + } } }, - [statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], + [prevIsRootStatusBarEnabled, isRootStatusBarEnabled, statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], ); - // Add navigation state listeners to update the status bar every time the route changes - // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properly + useEffect(() => { + didForceUpdateStatusBarRef.current = false; + }, [isRootStatusBarEnabled]); + useEffect(() => { if (isDisabled) { return; } - const listenerId = ++listenerCount.current; - const listener = () => updateStatusBarStyle(listenerId); + // Update status bar when theme changes + updateStatusBarStyle(); + + // Add navigation state listeners to update the status bar every time the route changes + // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properly + const listenerID = ++listenerCount.current; + const listener = () => updateStatusBarStyle(listenerID); navigationRef.addListener('state', listener); return () => navigationRef.removeListener('state', listener); - }, [isDisabled, theme.appBG, updateStatusBarStyle]); + }, [isDisabled, updateStatusBarStyle]); // Update the global background and status bar style (on web) everytime the theme changes. // The background of the html element needs to be updated, otherwise you will see a big contrast when resizing the window or when the keyboard is open on iOS web. diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index 9846cfa04257..941d63c1bf94 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -75,8 +75,6 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) { let descriptionKey: TranslationPaths = 'distance.waypointDescription.stop'; if (index === 0) { descriptionKey = 'distance.waypointDescription.start'; - } else if (index === Object.keys(waypoints).length - 1) { - descriptionKey = 'distance.waypointDescription.finish'; } return ( diff --git a/src/components/DistanceRequest/DistanceRequestFooter.tsx b/src/components/DistanceRequest/DistanceRequestFooter.tsx index 357074478bd8..f86d5369d4ac 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.tsx +++ b/src/components/DistanceRequest/DistanceRequestFooter.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import type {ReactNode} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -46,6 +46,18 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig const numberOfFilledWaypoints = Object.values(waypoints ?? {}).filter((waypoint) => Object.keys(waypoint).length).length; const lastWaypointIndex = numberOfWaypoints - 1; + const getMarkerComponent = useCallback( + (icon: IconAsset): ReactNode => ( + + ), + [theme], + ); + const waypointMarkers = useMemo( () => Object.entries(waypoints ?? {}) @@ -67,18 +79,11 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig return { id: `${waypoint.lng},${waypoint.lat},${index}`, coordinate: [waypoint.lng, waypoint.lat] as const, - markerComponent: (): ReactNode => ( - - ), + markerComponent: (): ReactNode => getMarkerComponent(MarkerComponent), }; }) .filter((waypoint): waypoint is WayPoint => !!waypoint), - [waypoints, lastWaypointIndex, theme.icon], + [waypoints, lastWaypointIndex, getMarkerComponent], ); return ( diff --git a/src/components/DistanceRequest/DistanceRequestRenderItem.tsx b/src/components/DistanceRequest/DistanceRequestRenderItem.tsx index a1f3efbf0291..57e4fb0b530e 100644 --- a/src/components/DistanceRequest/DistanceRequestRenderItem.tsx +++ b/src/components/DistanceRequest/DistanceRequestRenderItem.tsx @@ -42,7 +42,7 @@ function DistanceRequestRenderItem({waypoints, item = '', onSecondaryInteraction descriptionKey += 'start'; waypointIcon = Expensicons.DotIndicatorUnfilled; } else if (index === lastWaypointIndex) { - descriptionKey += 'finish'; + descriptionKey += 'stop'; waypointIcon = Expensicons.Location; } else { descriptionKey += 'stop'; diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index bcfdba11b6fc..e138ca4d4194 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -54,11 +54,11 @@ const EmojiPicker = forwardRef((props, ref) => { * @param {Function} [onEmojiSelectedValue=() => {}] - Run a callback when Emoji selected. * @param {React.MutableRefObject} emojiPopoverAnchorValue - Element to which Popover is anchored * @param {Object} [anchorOrigin=DEFAULT_ANCHOR_ORIGIN] - Anchor origin for Popover - * @param {Function} [onWillShow=() => {}] - Run a callback when Popover will show + * @param {Function} [onWillShow] - Run a callback when Popover will show * @param {String} id - Unique id for EmojiPicker * @param {String} activeEmojiValue - Selected emoji to be highlighted */ - const showEmojiPicker = (onModalHideValue, onEmojiSelectedValue, emojiPopoverAnchorValue, anchorOrigin, onWillShow = () => {}, id, activeEmojiValue) => { + const showEmojiPicker = (onModalHideValue, onEmojiSelectedValue, emojiPopoverAnchorValue, anchorOrigin, onWillShow, id, activeEmojiValue) => { onModalHide.current = onModalHideValue; onEmojiSelected.current = onEmojiSelectedValue; activeEmoji.current = activeEmojiValue; @@ -72,7 +72,8 @@ const EmojiPicker = forwardRef((props, ref) => { const anchorOriginValue = anchorOrigin || DEFAULT_ANCHOR_ORIGIN; calculateAnchorPosition(emojiPopoverAnchor.current, anchorOriginValue).then((value) => { - onWillShow(); + // eslint-disable-next-line es/no-optional-chaining + onWillShow?.(); setIsEmojiPickerVisible(true); setEmojiPopoverAnchorPosition(value); setEmojiPopoverAnchorOrigin(anchorOriginValue); diff --git a/src/components/ExpensifyWordmark.tsx b/src/components/ExpensifyWordmark.tsx index 0e8f78686b07..3d340da84f8b 100644 --- a/src/components/ExpensifyWordmark.tsx +++ b/src/components/ExpensifyWordmark.tsx @@ -34,21 +34,19 @@ function ExpensifyWordmark({isSmallScreenWidth, style}: ExpensifyWordmarkProps) const LogoComponent = logoComponents[environment]; return ( - <> - - - - + + + ); } diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 0abb1dc4a873..7fa8b364fb0f 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -38,6 +38,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont const firstVisibleViewRef = React.useRef(null); const mutationObserverRef = React.useRef(null); const lastScrollOffsetRef = React.useRef(0); + const isListRenderedRef = React.useRef(false); const getScrollOffset = React.useCallback(() => { if (scrollRef.current == null) { @@ -137,6 +138,9 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); React.useEffect(() => { + if (!isListRenderedRef.current) { + return; + } requestAnimationFrame(() => { prepareForMaintainVisibleContentPosition(); setupMutationObserver(); @@ -186,6 +190,10 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont onScroll={onScrollInternal} scrollEventThrottle={1} ref={onRef} + onLayout={(e) => { + isListRenderedRef.current = true; + props.onLayout?.(e); + }} /> ); }); diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 4d55de008516..b5535a2fe6c1 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -2,12 +2,16 @@ import type {ComponentPropsWithoutRef, ComponentType, ForwardedRef} from 'react' import React, {forwardRef, useContext} from 'react'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RoomNameInput from '@components/RoomNameInput'; +import type RoomNameInputProps from '@components/RoomNameInput/types'; import TextInput from '@components/TextInput'; +import type {BaseTextInputProps} from '@components/TextInput/BaseTextInput/types'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import FormContext from './FormContext'; import type {InputComponentBaseProps, InputComponentValueProps, ValidInputs, ValueTypeKey} from './types'; -const textInputBasedComponents: ComponentType[] = [TextInput, RoomNameInput]; +type TextInputBasedComponents = [ComponentType, ComponentType]; + +const textInputBasedComponents: TextInputBasedComponents = [TextInput, RoomNameInput]; type ComputedComponentSpecificRegistrationParams = { shouldSubmitForm: boolean; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 37d0f730c9e9..ba147dfa81a2 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -8,6 +8,7 @@ import type CheckboxWithLabel from '@components/CheckboxWithLabel'; import type CountrySelector from '@components/CountrySelector'; import type Picker from '@components/Picker'; import type RadioButtons from '@components/RadioButtons'; +import type RoomNameInput from '@components/RoomNameInput'; import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import type StatePicker from '@components/StatePicker'; import type TextInput from '@components/TextInput'; @@ -22,7 +23,7 @@ import type {BaseForm} from '@src/types/form/Form'; * when adding new inputs or removing old ones. * * TODO: Add remaining inputs here once these components are migrated to Typescript: - * EmojiPickerButtonDropdown | RoomNameInput | ValuePicker + * EmojiPickerButtonDropdown */ type ValidInputs = | typeof TextInput @@ -35,6 +36,7 @@ type ValidInputs = | typeof AmountForm | typeof BusinessTypePicker | typeof StatePicker + | typeof RoomNameInput | typeof ValuePicker | typeof RadioButtons; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index fd2d80c4d79a..b07f366e3382 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -74,7 +74,7 @@ function ImageRenderer({tnode}: ImageRendererProps) { ); return imagePreviewModalDisabled ? ( - <>{thumbnailImageComponent} + thumbnailImageComponent ) : ( {({anchor, report, action, checkIfContextMenuActive}) => ( diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 7313bb4aa7bb..25b468181b87 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -132,4 +132,4 @@ Provider.displayName = 'withOnyx(LocaleContextProvider)'; export {Provider as LocaleContextProvider, LocaleContext}; -export type {LocaleContextProps}; +export type {LocaleContextProps, Locale}; diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 77447f13644c..bce241270d77 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -144,54 +144,50 @@ const MapView = forwardRef( } }; - return ( - <> - {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( - - setUserInteractedWithMap(true)} - pitchEnabled={pitchEnabled} - attributionPosition={{...styles.r2, ...styles.b2}} - scaleBarEnabled={false} - logoPosition={{...styles.l2, ...styles.b2}} - // eslint-disable-next-line react/jsx-props-no-spreading - {...responder.panHandlers} - > - - - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - - {directionCoordinates && } - - - ) : ( - + setUserInteractedWithMap(true)} + pitchEnabled={pitchEnabled} + attributionPosition={{...styles.r2, ...styles.b2}} + scaleBarEnabled={false} + logoPosition={{...styles.l2, ...styles.b2}} + // eslint-disable-next-line react/jsx-props-no-spreading + {...responder.panHandlers} + > + - )} - + + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + + {directionCoordinates && } + + + ) : ( + ); }, ); diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index 05be6d6409e8..70b781fcd5ea 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -177,50 +177,46 @@ const MapView = forwardRef( [mapRef], ); - return ( - <> - {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( - - setUserInteractedWithMap(true)} - ref={setRef} - mapLib={mapboxgl} - mapboxAccessToken={accessToken} - initialViewState={{ - longitude: currentPosition?.longitude, - latitude: currentPosition?.latitude, - zoom: initialState.zoom, - }} - style={StyleUtils.getTextColorStyle(theme.mapAttributionText)} - mapStyle={styleURL} - > - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - {directionCoordinates && } - - - ) : ( - - )} - + return !isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( + + setUserInteractedWithMap(true)} + ref={setRef} + mapLib={mapboxgl} + mapboxAccessToken={accessToken} + initialViewState={{ + longitude: currentPosition?.longitude, + latitude: currentPosition?.latitude, + zoom: initialState.zoom, + }} + style={StyleUtils.getTextColorStyle(theme.mapAttributionText)} + mapStyle={styleURL} + > + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + {directionCoordinates && } + + + ) : ( + ); }, ); diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 459131ecc434..23040a242807 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -1,4 +1,5 @@ import React, {useCallback} from 'react'; +import type {MeasureInWindowOnSuccessCallback} from 'react-native'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -18,7 +19,7 @@ type Mention = { alternateText: string; /** Email/phone number of the user */ - login: string; + login?: string; /** Array of icons of the user. We use the first element of this array */ icons: Icon[]; @@ -32,7 +33,7 @@ type MentionSuggestionsProps = { mentions: Mention[]; /** Fired when the user selects a mention */ - onSelect: () => void; + onSelect: (highlightedMentionIndex: number) => void; /** Mention prefix that follows the @ sign */ prefix: string; @@ -43,7 +44,7 @@ type MentionSuggestionsProps = { isMentionPickerLarge: boolean; /** Measures the parent container's position and dimensions. */ - measureParentContainer: () => void; + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; }; /** @@ -142,3 +143,5 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe MentionSuggestions.displayName = 'MentionSuggestions'; export default MentionSuggestions; + +export type {Mention}; diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js deleted file mode 100755 index df2781d3ea89..000000000000 --- a/src/components/MoneyRequestConfirmationList.js +++ /dev/null @@ -1,898 +0,0 @@ -import {useIsFocused} from '@react-navigation/native'; -import {format} from 'date-fns'; -import {isEmpty} from 'lodash'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import * as IOUUtils from '@libs/IOUUtils'; -import Log from '@libs/Log'; -import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReceiptUtils from '@libs/ReceiptUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import {policyPropTypes} from '@pages/workspace/withPolicy'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import categoryPropTypes from './categoryPropTypes'; -import ConfirmedRoute from './ConfirmedRoute'; -import FormHelpMessage from './FormHelpMessage'; -import Image from './Image'; -import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import optionPropTypes from './optionPropTypes'; -import OptionsSelector from './OptionsSelector'; -import ReceiptEmptyState from './ReceiptEmptyState'; -import SettlementButton from './SettlementButton'; -import ShowMoreButton from './ShowMoreButton'; -import Switch from './Switch'; -import tagPropTypes from './tagPropTypes'; -import Text from './Text'; -import transactionPropTypes from './transactionPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; - -const propTypes = { - /** Callback to inform parent modal of success */ - onConfirm: PropTypes.func, - - /** Callback to parent modal to send money */ - onSendMoney: PropTypes.func, - - /** Callback to inform a participant is selected */ - onSelectParticipant: PropTypes.func, - - /** Should we request a single or multiple participant selection from user */ - hasMultipleParticipants: PropTypes.bool.isRequired, - - /** IOU amount */ - iouAmount: PropTypes.number.isRequired, - - /** IOU comment */ - iouComment: PropTypes.string, - - /** IOU currency */ - iouCurrencyCode: PropTypes.string, - - /** IOU type */ - iouType: PropTypes.string, - - /** IOU date */ - iouCreated: PropTypes.string, - - /** IOU merchant */ - iouMerchant: PropTypes.string, - - /** IOU Category */ - iouCategory: PropTypes.string, - - /** IOU Tag */ - iouTag: PropTypes.string, - - /** IOU isBillable */ - iouIsBillable: PropTypes.bool, - - /** Callback to toggle the billable state */ - onToggleBillable: PropTypes.func, - - /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired, - - /** Payee of the money request with login */ - payeePersonalDetails: optionPropTypes, - - /** Can the participants be modified or not */ - canModifyParticipants: PropTypes.bool, - - /** Should the list be read only, and not editable? */ - isReadOnly: PropTypes.bool, - - /** Depending on expense report or personal IOU report, respective bank account route */ - bankAccountRoute: PropTypes.string, - - ...withCurrentUserPersonalDetailsPropTypes, - - /** Current user session */ - session: PropTypes.shape({ - email: PropTypes.string.isRequired, - }), - - /** The policyID of the request */ - policyID: PropTypes.string, - - /** The reportID of the request */ - reportID: PropTypes.string, - - /** File path of the receipt */ - receiptPath: PropTypes.string, - - /** File name of the receipt */ - receiptFilename: PropTypes.string, - - /** List styles for OptionsSelector */ - listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** ID of the transaction that represents the money request */ - transactionID: PropTypes.string, - - /** Transaction that represents the money request */ - transaction: transactionPropTypes, - - /** Unit and rate used for if the money request is a distance request */ - mileageRate: PropTypes.shape({ - /** Unit used to represent distance */ - unit: PropTypes.oneOf([CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]), - - /** Rate used to calculate the distance request amount */ - rate: PropTypes.number, - - /** The currency of the rate */ - currency: PropTypes.string, - }), - - /** Whether the money request is a distance request */ - isDistanceRequest: PropTypes.bool, - - /** Whether the money request is a scan request */ - isScanRequest: PropTypes.bool, - - /** Whether we're editing a split bill */ - isEditingSplitBill: PropTypes.bool, - - /** Whether we should show the amount, date, and merchant fields. */ - shouldShowSmartScanFields: PropTypes.bool, - - /** A flag for verifying that the current report is a sub-report of a workspace chat */ - isPolicyExpenseChat: PropTypes.bool, - - /* Onyx Props */ - /** Collection of categories attached to a policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), - - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, - - /* Onyx Props */ - /** The policy of the report */ - policy: policyPropTypes.policy, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, -}; - -const defaultProps = { - onConfirm: () => {}, - onSendMoney: () => {}, - onSelectParticipant: () => {}, - iouType: CONST.IOU.TYPE.REQUEST, - iouCategory: '', - iouTag: '', - iouIsBillable: false, - onToggleBillable: () => {}, - payeePersonalDetails: null, - canModifyParticipants: false, - isReadOnly: false, - bankAccountRoute: '', - session: { - email: null, - }, - policyID: '', - reportID: '', - ...withCurrentUserPersonalDetailsDefaultProps, - receiptPath: '', - receiptFilename: '', - listStyles: [], - policy: {}, - policyCategories: {}, - policyTags: {}, - transactionID: '', - transaction: {}, - mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'}, - isDistanceRequest: false, - isScanRequest: false, - shouldShowSmartScanFields: true, - isPolicyExpenseChat: false, - iou: iouDefaultProps, -}; - -function MoneyRequestConfirmationList(props) { - const theme = useTheme(); - const styles = useThemeStyles(); - // Destructure functions from props to pass it as a dependecy to useCallback/useMemo hooks. - // Prop functions pass props itself as a "this" value to the function which means they change every time props change. - const {onSendMoney, onConfirm, onSelectParticipant} = props; - const {translate, toLocaleDigit} = useLocalize(); - const transaction = props.transaction; - const {canUseViolations} = usePermissions(); - - const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST; - const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT; - const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND; - - const isSplitWithScan = isSplitBill && props.isScanRequest; - - const {unit, rate, currency} = props.mileageRate; - const distance = lodashGet(transaction, 'routes.route0.distance', 0); - const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0; - const taxRates = lodashGet(props.policy, 'taxRates', {}); - - // A flag for showing the categories field - const shouldShowCategories = props.isPolicyExpenseChat && (props.iouCategory || OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories))); - // A flag and a toggler for showing the rest of the form fields - const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - - // Do not hide fields in case of send money request - const shouldShowAllFields = props.isDistanceRequest || shouldExpandFields || !props.shouldShowSmartScanFields || isTypeSend || props.isEditingSplitBill; - - // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item - const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan; - const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !props.isDistanceRequest && !isSplitWithScan; - - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(props.policyTags), [props.policyTags]); - - // A flag for showing the tags field - const shouldShowTags = props.isPolicyExpenseChat && (props.iouTag || OptionsListUtils.hasEnabledTags(policyTagLists)); - - // A flag for showing tax fields - tax rate and tax amount - const shouldShowTax = props.isPolicyExpenseChat && lodashGet(props.policy, 'tax.trackingEnabled', props.policy.isTaxTrackingEnabled); - - // A flag for showing the billable field - const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true); - - const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithPendingRoute = props.isDistanceRequest && (!hasRoute || !rate); - const formattedAmount = isDistanceRequestWithPendingRoute - ? '' - : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, - props.isDistanceRequest ? currency : props.iouCurrencyCode, - ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); - - const defaultTaxKey = taxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; - const taxRateTitle = (props.transaction.taxRate && props.transaction.taxRate.text) || defaultTaxName; - - const isFocused = useIsFocused(); - const [formError, setFormError] = useState(''); - - const [didConfirm, setDidConfirm] = useState(false); - const [didConfirmSplit, setDidConfirmSplit] = useState(false); - - const shouldDisplayFieldError = useMemo(() => { - if (!props.isEditingSplitBill) { - return false; - } - - return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); - }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); - - const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const shouldDisplayMerchantError = props.isPolicyExpenseChat && !props.isScanRequest && isMerchantEmpty; - - useEffect(() => { - if (shouldDisplayFieldError && didConfirmSplit) { - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - if (shouldDisplayFieldError && props.hasSmartScanFailed) { - setFormError('iou.receiptScanningFailed'); - return; - } - // reset the form error whenever the screen gains or loses focus - setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, props.hasSmartScanFailed, didConfirmSplit]); - - useEffect(() => { - if (!shouldCalculateDistanceAmount) { - return; - } - - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate); - IOU.setMoneyRequestAmount(amount); - }, [shouldCalculateDistanceAmount, distance, rate, unit]); - - /** - * Returns the participants with amount - * @param {Array} participants - * @returns {Array} - */ - const getParticipantsWithAmount = useCallback( - (participantsList) => { - const iouAmount = IOUUtils.calculateAmount(participantsList.length, props.iouAmount, props.iouCurrencyCode); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( - participantsList, - props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(iouAmount, props.iouCurrencyCode) : '', - ); - }, - [props.iouAmount, props.iouCurrencyCode], - ); - - // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again - if (props.isEditingSplitBill && didConfirm) { - setDidConfirm(false); - } - - const splitOrRequestOptions = useMemo(() => { - let text; - if (isSplitBill && props.iouAmount === 0) { - text = translate('iou.split'); - } else if ((props.receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.request'); - if (props.iouAmount !== 0) { - text = translate('iou.requestAmount', {amount: formattedAmount}); - } - } else { - const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.requestAmount'; - text = translate(translationKey, {amount: formattedAmount}); - } - return [ - { - text: text[0].toUpperCase() + text.slice(1), - value: props.iouType, - }, - ]; - }, [isSplitBill, isTypeRequest, props.iouType, props.iouAmount, props.receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); - - const selectedParticipants = useMemo(() => _.filter(props.selectedParticipants, (participant) => participant.selected), [props.selectedParticipants]); - const payeePersonalDetails = useMemo(() => props.payeePersonalDetails || props.currentUserPersonalDetails, [props.payeePersonalDetails, props.currentUserPersonalDetails]); - const canModifyParticipants = !props.isReadOnly && props.canModifyParticipants && props.hasMultipleParticipants; - const shouldDisablePaidBySection = canModifyParticipants; - - const optionSelectorSections = useMemo(() => { - const sections = []; - const unselectedParticipants = _.filter(props.selectedParticipants, (participant) => !participant.selected); - if (props.hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants); - - if (!canModifyParticipants) { - formattedParticipantsList = _.map(formattedParticipantsList, (participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), - })); - } - - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, props.iouAmount, props.iouCurrencyCode, true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - payeePersonalDetails, - props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, props.iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - indexOffset: 0, - isDisabled: shouldDisablePaidBySection, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - indexOffset: 1, - }, - ); - } else { - const formattedSelectedParticipants = _.map(props.selectedParticipants, (participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), - })); - sections.push({ - title: translate('common.to'), - data: formattedSelectedParticipants, - shouldShow: true, - indexOffset: 0, - }); - } - return sections; - }, [ - props.selectedParticipants, - props.hasMultipleParticipants, - props.iouAmount, - props.iouCurrencyCode, - getParticipantsWithAmount, - selectedParticipants, - payeePersonalDetails, - translate, - shouldDisablePaidBySection, - canModifyParticipants, - ]); - - const selectedOptions = useMemo(() => { - if (!props.hasMultipleParticipants) { - return []; - } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, props.hasMultipleParticipants, payeePersonalDetails]); - - useEffect(() => { - if (!props.isDistanceRequest) { - return; - } - - /* - Set pending waypoints based on the route status. We should handle this dynamically to cover cases such as: - When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page. - In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending. - */ - IOU.setMoneyRequestPendingFields(props.transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); - - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); - IOU.setMoneyRequestMerchant(props.transactionID, distanceMerchant, false); - }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]); - - /** - * @param {Object} option - */ - const selectParticipant = useCallback( - (option) => { - // Return early if selected option is currently logged in user. - if (option.accountID === props.session.accountID) { - return; - } - onSelectParticipant(option); - }, - [props.session.accountID, onSelectParticipant], - ); - - /** - * Navigate to report details or profile of selected user - * @param {Object} option - */ - const navigateToReportOrUserDetail = (option) => { - if (option.accountID) { - const activeRoute = Navigation.getActiveRouteWithoutParams(); - - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); - } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); - } - }; - - /** - * @param {String} paymentMethod - */ - const confirm = useCallback( - (paymentMethod) => { - if (_.isEmpty(selectedParticipants)) { - return; - } - if (props.iouCategory && props.iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { - setFormError('iou.error.invalidCategoryLength'); - return; - } - if (props.iouType === CONST.IOU.TYPE.SEND) { - if (!paymentMethod) { - return; - } - - setDidConfirm(true); - - Log.info(`[IOU] Sending money via: ${paymentMethod}`); - onSendMoney(paymentMethod); - } else { - // validate the amount for distance requests - const decimals = CurrencyUtils.getCurrencyDecimals(props.iouCurrencyCode); - if (props.isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(props.iouAmount), decimals)) { - setFormError('common.error.invalidAmount'); - return; - } - - if (props.isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { - setDidConfirmSplit(true); - return; - } - - setDidConfirm(true); - onConfirm(selectedParticipants); - } - }, - [ - selectedParticipants, - onSendMoney, - onConfirm, - props.isEditingSplitBill, - props.iouType, - props.isDistanceRequest, - props.iouCategory, - isDistanceRequestWithPendingRoute, - props.iouCurrencyCode, - props.iouAmount, - transaction, - ], - ); - - const footerContent = useMemo(() => { - if (props.isReadOnly) { - return; - } - - const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0 || shouldDisplayMerchantError; - - const button = shouldShowSettlementButton ? ( - - ) : ( - confirm(value)} - options={splitOrRequestOptions} - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} - enterKeyEventListenerPriority={1} - /> - ); - - return ( - <> - {!_.isEmpty(formError) && ( - - )} - {button} - - ); - }, [ - props.isReadOnly, - props.iouType, - props.bankAccountRoute, - props.iouCurrencyCode, - props.policyID, - selectedParticipants.length, - shouldDisplayMerchantError, - confirm, - splitOrRequestOptions, - formError, - styles.ph1, - styles.mb2, - ]); - - const {image: receiptImage, thumbnail: receiptThumbnail} = - props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, props.receiptPath, props.receiptFilename) : {}; - return ( - - {props.isDistanceRequest && ( - - - - )} - {receiptImage || receiptThumbnail ? ( - - ) : ( - // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(props.policy) && - !props.isDistanceRequest && - props.iouType === CONST.IOU.TYPE.REQUEST && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( - CONST.IOU.ACTION.CREATE, - props.iouType, - transaction.transactionID, - props.reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } - /> - ) - )} - {props.shouldShowSmartScanFields && ( - { - if (props.isDistanceRequest) { - return; - } - if (props.isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.AMOUNT)); - return; - } - Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(props.iouType, props.reportID)); - }} - style={[styles.moneyRequestMenuItem, styles.mt2]} - titleStyle={styles.moneyRequestConfirmationAmount} - disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} - error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} - /> - )} - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( - CONST.IOU.ACTION.EDIT, - props.iouType, - transaction.transactionID, - props.reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!props.isReadOnly} - numberOfLinesTitle={2} - /> - {!shouldShowAllFields && ( - - )} - {shouldShowAllFields && ( - <> - {shouldShowDate && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( - CONST.IOU.ACTION.EDIT, - props.iouType, - transaction.transactionID, - props.reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - disabled={didConfirm} - interactive={!props.isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} - error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} - /> - )} - {props.isDistanceRequest && ( - Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} - disabled={didConfirm || !isTypeRequest} - interactive={!props.isReadOnly} - /> - )} - {shouldShowMerchant && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( - CONST.IOU.ACTION.EDIT, - props.iouType, - transaction.transactionID, - props.reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - disabled={didConfirm} - interactive={!props.isReadOnly} - brickRoadIndicator={ - props.isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '' - } - error={ - shouldDisplayMerchantError || (props.isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction)) - ? translate('common.error.enterMerchant') - : '' - } - /> - )} - {shouldShowCategories && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute( - CONST.IOU.ACTION.EDIT, - props.iouType, - props.transaction.transactionID, - props.reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!props.isReadOnly} - rightLabel={canUseViolations && Boolean(props.policy.requiresCategory) ? translate('common.required') : ''} - /> - )} - {shouldShowTags && - _.map(policyTagLists, ({name}, index) => ( - { - if (props.isEditingSplitBill) { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( - CONST.IOU.ACTION.EDIT, - CONST.IOU.TYPE.SPLIT, - index, - props.transaction.transactionID, - props.reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ); - return; - } - Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(props.iouType, props.reportID)); - }} - style={[styles.moneyRequestMenuItem]} - disabled={didConfirm} - interactive={!props.isReadOnly} - rightLabel={canUseViolations && Boolean(props.policy.requiresTag) ? translate('common.required') : ''} - /> - ))} - - {shouldShowTax && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!props.isReadOnly} - /> - )} - - {shouldShowTax && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!props.isReadOnly} - /> - )} - - {shouldShowBillable && ( - - {translate('common.billable')} - - - )} - - )} - - ); -} - -MoneyRequestConfirmationList.propTypes = propTypes; -MoneyRequestConfirmationList.defaultProps = defaultProps; -MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; - -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - policyCategories: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - }, - policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - }, - mileageRate: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - selector: DistanceRequestUtils.getDefaultMileageRate, - }, - splitTransactionDraft: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, - }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - iou: { - key: ONYXKEYS.IOU, - }, - }), -)(MoneyRequestConfirmationList); diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx new file mode 100755 index 000000000000..773e98b6462e --- /dev/null +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -0,0 +1,904 @@ +import {useIsFocused} from '@react-navigation/native'; +import {format} from 'date-fns'; +import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; +import * as IOUUtils from '@libs/IOUUtils'; +import Log from '@libs/Log'; +import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReceiptUtils from '@libs/ReceiptUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {MileageRate} from '@src/types/onyx/Policy'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; +import ConfirmedRoute from './ConfirmedRoute'; +import FormHelpMessage from './FormHelpMessage'; +import Image from './Image'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import OptionsSelector from './OptionsSelector'; +import ReceiptEmptyState from './ReceiptEmptyState'; +import SettlementButton from './SettlementButton'; +import ShowMoreButton from './ShowMoreButton'; +import Switch from './Switch'; +import Text from './Text'; +import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; + +type DropdownOption = { + text: string; + value: DeepValueOf; +}; + +type Option = Partial; + +type CategorySection = { + title: string | undefined; + shouldShow: boolean; + indexOffset: number; + data: Option[]; +}; + +type MoneyRequestConfirmationListOnyxProps = { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + iou: OnyxEntry; + + /** Unit and rate used for if the money request is a distance request */ + mileageRate: OnyxEntry; + + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; + + /** Collection of tags attached to a policy */ + policyTags: OnyxEntry; + + /** The policy of root parent report */ + policy: OnyxEntry; + + /** The session of the logged in user */ + session: OnyxEntry; +}; + +type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & + WithCurrentUserPersonalDetailsProps & { + /** Callback to inform parent modal of success */ + onConfirm?: (selectedParticipants: Participant[]) => void; + + /** Callback to parent modal to send money */ + onSendMoney?: (paymentMethod: PaymentMethodType) => void; + + /** Callback to inform a participant is selected */ + onSelectParticipant?: (option: Participant) => void; + + /** Should we request a single or multiple participant selection from user */ + hasMultipleParticipants: boolean; + + /** IOU amount */ + iouAmount: number; + + /** IOU comment */ + iouComment?: string; + + /** IOU currency */ + iouCurrencyCode?: string; + + /** IOU type */ + iouType?: ValueOf; + + /** IOU date */ + iouCreated?: string; + + /** IOU merchant */ + iouMerchant?: string; + + /** IOU Category */ + iouCategory?: string; + + /** IOU Tag */ + iouTag?: string; + + /** IOU isBillable */ + iouIsBillable?: boolean; + + /** Callback to toggle the billable state */ + onToggleBillable?: () => void; + + /** Selected participants from MoneyRequestModal with login / accountID */ + selectedParticipants: Participant[]; + + /** Payee of the money request with login */ + payeePersonalDetails?: OnyxEntry; + + /** Can the participants be modified or not */ + canModifyParticipants?: boolean; + + /** Should the list be read only, and not editable? */ + isReadOnly?: boolean; + + /** Depending on expense report or personal IOU report, respective bank account route */ + bankAccountRoute?: Route; + + /** The policyID of the request */ + policyID?: string; + + /** The reportID of the request */ + reportID?: string; + + /** File path of the receipt */ + receiptPath?: string; + + /** File name of the receipt */ + receiptFilename?: string; + + /** List styles for OptionsSelector */ + listStyles?: StyleProp; + + /** ID of the transaction that represents the money request */ + transactionID?: string; + + /** Whether the money request is a distance request */ + isDistanceRequest?: boolean; + + /** Whether the money request is a scan request */ + isScanRequest?: boolean; + + /** Whether we're editing a split bill */ + isEditingSplitBill?: boolean; + + /** Whether we should show the amount, date, and merchant fields. */ + shouldShowSmartScanFields?: boolean; + + /** A flag for verifying that the current report is a sub-report of a workspace chat */ + isPolicyExpenseChat?: boolean; + + /** Whether there is smartscan failed */ + hasSmartScanFailed?: boolean; + + /** ID of the report action */ + reportActionID?: string; + + /** Transaction object */ + transaction?: OnyxTypes.Transaction; + }; + +function MoneyRequestConfirmationList({ + onConfirm = () => {}, + onSendMoney = () => {}, + onSelectParticipant = () => {}, + iouType = CONST.IOU.TYPE.REQUEST, + iouCategory = '', + iouTag = '', + iouIsBillable = false, + onToggleBillable = () => {}, + payeePersonalDetails, + canModifyParticipants = false, + isReadOnly = false, + bankAccountRoute, + policyID, + reportID, + receiptPath, + receiptFilename, + transactionID, + mileageRate, + isDistanceRequest = false, + isScanRequest = false, + shouldShowSmartScanFields = true, + isPolicyExpenseChat = false, + transaction, + iouAmount, + policyTags, + policyCategories, + policy, + iouCurrencyCode, + isEditingSplitBill, + hasSmartScanFailed, + iouMerchant, + currentUserPersonalDetails, + hasMultipleParticipants, + selectedParticipants, + session, + iou, + reportActionID, + iouCreated, + listStyles, + iouComment, +}: MoneyRequestConfirmationListProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate, toLocaleDigit} = useLocalize(); + const {canUseViolations} = usePermissions(); + + const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; + const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + + const isSplitWithScan = isSplitBill && isScanRequest; + + const distance = transaction?.routes?.route0.distance ?? 0; + const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; + const taxRates = policy?.taxRates; + + // A flag for showing the categories field + const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + + // A flag and a toggler for showing the rest of the form fields + const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); + + // Do not hide fields in case of send money request + const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; + + // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item + const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan; + const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !isDistanceRequest && !isSplitWithScan; + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + // A flag for showing the tags field + const shouldShowTags = isPolicyExpenseChat && (iouTag || OptionsListUtils.hasEnabledTags(policyTagLists)); + + // A flag for showing tax fields - tax rate and tax amount + const shouldShowTax = isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled); + + // A flag for showing the billable field + const shouldShowBillable = !policy?.disabledFields?.defaultBillable ?? true; + + const hasRoute = TransactionUtils.hasRoute(transaction ?? null); + const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !mileageRate?.rate); + const formattedAmount = isDistanceRequestWithPendingRoute + ? '' + : CurrencyUtils.convertToDisplayString( + shouldCalculateDistanceAmount + ? DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate ?? 0) + : iouAmount, + isDistanceRequest ? mileageRate?.currency : iouCurrencyCode, + ); + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); + + const defaultTaxKey = taxRates?.defaultExternalID; + const defaultTaxName = (defaultTaxKey && `${taxRates?.taxes[defaultTaxKey].name} (${taxRates?.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) ?? ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const taxRateTitle = transaction?.taxRate?.text || defaultTaxName; + + const isFocused = useIsFocused(); + const [formError, setFormError] = useState(null); + + const [didConfirm, setDidConfirm] = useState(false); + const [didConfirmSplit, setDidConfirmSplit] = useState(false); + + const shouldDisplayFieldError = useMemo(() => { + if (!isEditingSplitBill) { + return false; + } + + return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction ?? null)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)); + }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); + + const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; + const shouldDisplayMerchantError = isPolicyExpenseChat && !isScanRequest && isMerchantEmpty; + + useEffect(() => { + if (shouldDisplayFieldError && didConfirmSplit) { + setFormError('iou.error.genericSmartscanFailureMessage'); + return; + } + if (shouldDisplayFieldError && hasSmartScanFailed) { + setFormError('iou.receiptScanningFailed'); + return; + } + // reset the form error whenever the screen gains or loses focus + setFormError(null); + }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]); + + useEffect(() => { + if (!shouldCalculateDistanceAmount) { + return; + } + + const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate ?? 0); + IOU.setMoneyRequestAmount(amount); + }, [shouldCalculateDistanceAmount, distance, mileageRate?.rate, mileageRate?.unit]); + + /** + * Returns the participants with amount + */ + const getParticipantsWithAmount = useCallback( + (participantsList: Participant[]) => { + const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); + return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( + participantsList, + calculatedIouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '', + ); + }, + [iouAmount, iouCurrencyCode], + ); + + // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again + if (isEditingSplitBill && didConfirm) { + setDidConfirm(false); + } + + const splitOrRequestOptions: DropdownOption[] = useMemo(() => { + let text; + if (isSplitBill && iouAmount === 0) { + text = translate('iou.split'); + } else if (!!(receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { + text = translate('iou.request'); + if (iouAmount !== 0) { + text = translate('iou.requestAmount', {amount: formattedAmount}); + } + } else { + const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.requestAmount'; + text = translate(translationKey, {amount: formattedAmount}); + } + return [ + { + text: text[0].toUpperCase() + text.slice(1), + value: iouType, + }, + ]; + }, [isSplitBill, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); + + const selectedParticipantsMemo = useMemo(() => selectedParticipants.filter((participant) => participant.selected), [selectedParticipants]); + const payeePersonalDetailsMemo = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); + const canModifyParticipantsValue = !isReadOnly && canModifyParticipants && hasMultipleParticipants; + + const optionSelectorSections: CategorySection[] = useMemo(() => { + const sections = []; + const unselectedParticipants = selectedParticipants.filter((participant) => !participant.selected); + if (hasMultipleParticipants) { + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipantsMemo); + let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; + + if (!canModifyParticipantsValue) { + formattedParticipantsList = formattedParticipantsList.map((participant) => ({ + ...participant, + isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), + })); + } + + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode ?? '', true); + const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( + payeePersonalDetailsMemo, + iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', + ); + + sections.push( + { + title: translate('moneyRequestConfirmationList.paidBy'), + data: [formattedPayeeOption], + shouldShow: true, + indexOffset: 0, + isDisabled: canModifyParticipantsValue, + }, + { + title: translate('moneyRequestConfirmationList.splitWith'), + data: formattedParticipantsList, + shouldShow: true, + indexOffset: 1, + }, + ); + } else { + const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ + ...participant, + isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), + })); + sections.push({ + title: translate('common.to'), + data: formattedSelectedParticipants, + shouldShow: true, + indexOffset: 0, + }); + } + return sections; + }, [ + selectedParticipants, + hasMultipleParticipants, + iouAmount, + iouCurrencyCode, + getParticipantsWithAmount, + payeePersonalDetailsMemo, + translate, + canModifyParticipantsValue, + selectedParticipantsMemo, + ]); + + const selectedOptions = useMemo(() => { + if (!hasMultipleParticipants) { + return []; + } + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode ?? '', true); + return [ + ...selectedParticipantsMemo, + OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetailsMemo, CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode)), + ]; + }, [hasMultipleParticipants, selectedParticipantsMemo, iouAmount, iouCurrencyCode, payeePersonalDetailsMemo]); + + useEffect(() => { + if (!isDistanceRequest) { + return; + } + /* + Set pending waypoints based on the route status. We should handle this dynamically to cover cases such as: + When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page. + In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending. + */ + IOU.setMoneyRequestPendingFields(transactionID ?? '', {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant( + hasRoute, + distance, + mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + mileageRate?.rate ?? 0, + mileageRate?.currency ?? 'USD', + translate, + toLocaleDigit, + ); + IOU.setMoneyRequestMerchant(transactionID ?? '', distanceMerchant, false); + }, [hasRoute, distance, mileageRate?.unit, mileageRate?.rate, mileageRate?.currency, translate, toLocaleDigit, isDistanceRequest, transactionID, isDistanceRequestWithPendingRoute]); + + const selectParticipant = useCallback( + (option: Participant) => { + // Return early if selected option is currently logged in user. + if (option.accountID === session?.accountID) { + return; + } + onSelectParticipant(option); + }, + [session?.accountID, onSelectParticipant], + ); + + /** + * Navigate to report details or profile of selected user + */ + const navigateToReportOrUserDetail = (option: Participant | OnyxTypes.Report) => { + if ('accountID' in option && option.accountID) { + const activeRoute = Navigation.getActiveRouteWithoutParams(); + + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); + } else if ('reportID' in option && option.reportID) { + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); + } + }; + + const confirm = useCallback( + (paymentMethod: PaymentMethodType | undefined) => { + if (selectedParticipantsMemo.length === 0) { + return; + } + if (iouCategory && iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { + setFormError('iou.error.invalidCategoryLength'); + return; + } + if (iouType === CONST.IOU.TYPE.SEND) { + if (!paymentMethod) { + return; + } + + setDidConfirm(true); + + Log.info(`[IOU] Sending money via: ${paymentMethod}`); + onSendMoney(paymentMethod); + } else { + // validate the amount for distance requests + const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); + if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { + setFormError('common.error.invalidAmount'); + return; + } + + if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) { + setDidConfirmSplit(true); + return; + } + + setDidConfirm(true); + onConfirm(selectedParticipantsMemo); + } + }, + [ + selectedParticipantsMemo, + iouCategory, + iouType, + onSendMoney, + iouCurrencyCode, + isDistanceRequest, + isDistanceRequestWithPendingRoute, + iouAmount, + isEditingSplitBill, + transaction, + onConfirm, + ], + ); + + const footerContent = useMemo(() => { + if (isReadOnly) { + return; + } + + const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; + const shouldDisableButton = selectedParticipantsMemo.length === 0 || shouldDisplayMerchantError; + + const button = shouldShowSettlementButton ? ( + + ) : ( + confirm(value as PaymentMethodType)} + options={splitOrRequestOptions} + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} + enterKeyEventListenerPriority={1} + /> + ); + + return ( + <> + {!!formError && ( + + )} + {button} + + ); + }, [ + isReadOnly, + iouType, + selectedParticipantsMemo.length, + shouldDisplayMerchantError, + confirm, + bankAccountRoute, + iouCurrencyCode, + policyID, + splitOrRequestOptions, + formError, + styles.ph1, + styles.mb2, + ]); + + const receiptData = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : null; + return ( + // @ts-expect-error TODO: Remove this once OptionsSelector (https://github.com/Expensify/App/issues/25125) is migrated to TypeScript. + + {isDistanceRequest && ( + + + + )} + {receiptData?.image ?? receiptData?.thumbnail ? ( + + ) : ( + // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") + PolicyUtils.isPaidGroupPolicy(policy) && + !isDistanceRequest && + iouType === CONST.IOU.TYPE.REQUEST && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( + CONST.IOU.ACTION.CREATE, + iouType, + transaction?.transactionID ?? '', + reportID ?? '', + Navigation.getActiveRouteWithoutParams(), + ), + ) + } + /> + ) + )} + {shouldShowSmartScanFields && ( + { + if (isDistanceRequest) { + return; + } + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID ?? '', reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(iouType, reportID)); + }} + style={[styles.moneyRequestMenuItem, styles.mt2]} + titleStyle={styles.moneyRequestConfirmationAmount} + disabled={didConfirm} + brickRoadIndicator={ + isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined + } + error={ + shouldDisplayMerchantError || (isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null)) + ? translate('common.error.enterMerchant') + : '' + } + /> + )} + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( + CONST.IOU.ACTION.EDIT, + iouType, + transaction?.transactionID ?? '', + reportID ?? '', + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + style={styles.moneyRequestMenuItem} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + numberOfLinesTitle={2} + /> + {!shouldShowAllFields && ( + + )} + {shouldShowAllFields && ( + <> + {shouldShowDate && ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( + CONST.IOU.ACTION.EDIT, + iouType, + transaction?.transactionID ?? '', + reportID ?? '', + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction ?? null) ? translate('common.error.enterDate') : ''} + /> + )} + {isDistanceRequest && ( + Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(iouType, reportID))} + disabled={didConfirm || !isTypeRequest} + interactive={!isReadOnly} + /> + )} + {shouldShowMerchant && ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( + CONST.IOU.ACTION.EDIT, + iouType, + transactionID ?? '', + reportID ?? '', + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={ + shouldDisplayMerchantError || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null)) + ? translate('common.error.enterMerchant') + : '' + } + /> + )} + {shouldShowCategories && ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute( + CONST.IOU.ACTION.EDIT, + iouType, + transaction?.transactionID ?? '', + reportID ?? '', + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + style={styles.moneyRequestMenuItem} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={canUseViolations && Boolean(policy?.requiresCategory) ? translate('common.required') : ''} + /> + )} + {shouldShowTags && + policyTagLists.map(({name}, index) => ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( + CONST.IOU.ACTION.EDIT, + CONST.IOU.TYPE.SPLIT, + index, + transaction?.transactionID ?? '', + reportID ?? '', + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + style={styles.moneyRequestMenuItem} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={canUseViolations && !!policy?.requiresTag ? translate('common.required') : ''} + /> + ))} + + {shouldShowTax && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction?.transactionID ?? '', reportID ?? '', Navigation.getActiveRouteWithoutParams()), + ) + } + disabled={didConfirm} + interactive={!isReadOnly} + /> + )} + + {shouldShowTax && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction?.transactionID ?? '', reportID ?? '', Navigation.getActiveRouteWithoutParams()), + ) + } + disabled={didConfirm} + interactive={!isReadOnly} + /> + )} + + {shouldShowBillable && ( + + {translate('common.billable')} + + + )} + + )} + + ); +} + +MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; + +export default withCurrentUserPersonalDetails( + withOnyx({ + policyCategories: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + }, + policyTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + }, + mileageRate: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: DistanceRequestUtils.getDefaultMileageRate, + }, + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, + iou: { + key: ONYXKEYS.IOU, + }, + session: { + key: ONYXKEYS.SESSION, + }, + })(MoneyRequestConfirmationList), +); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index f53e3130f660..fe8cc3506b3f 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -84,8 +84,10 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, let canDeleteRequest = canModifyRequest; if (ReportUtils.isPaidGroupPolicyExpenseReport(moneyRequestReport)) { - // If it's a paid policy expense report, only allow deleting the request if it's not submitted or the user is the policy admin - canDeleteRequest = canDeleteRequest && (ReportUtils.isDraftExpenseReport(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy)); + // If it's a paid policy expense report, only allow deleting the request if it's in draft state or instantly submitted state or the user is the policy admin + canDeleteRequest = + canDeleteRequest && + (ReportUtils.isDraftExpenseReport(moneyRequestReport) || ReportUtils.isExpenseReportWithInstantSubmittedState(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy)); } const changeMoneyRequestStatus = () => { diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 8eeaeaf87eff..92313d03ae2a 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -908,7 +908,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} - {shouldShowAllFields && <>{supplementaryFields}} + {shouldShowAllFields && supplementaryFields} ); } diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 1d1eea0d20ba..8b606bd4429d 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -5,6 +5,7 @@ import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ReportUtils from '@libs/ReportUtils'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -106,7 +107,8 @@ function MultipleAvatars({ let avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size, avatarSizeToStylesMap]); - const tooltipTexts = useMemo(() => (shouldShowTooltip ? icons.map((icon) => icon.name) : ['']), [shouldShowTooltip, icons]); + const tooltipTexts = useMemo(() => (shouldShowTooltip ? icons.map((icon) => ReportUtils.getUserDetailTooltipText(Number(icon.id), icon.name)) : ['']), [shouldShowTooltip, icons]); + const avatarSize = useMemo(() => { if (isFocusMode) { return CONST.AVATAR_SIZE.MID_SUBSCRIPT; @@ -173,145 +175,134 @@ function MultipleAvatars({ avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height)]); } - return ( - <> - {shouldStackHorizontally ? ( - avatarRows.map((avatars, rowIndex) => ( - ( + + {[...avatars].splice(0, maxAvatarsInRow).map((icon, index) => ( + - {[...avatars].splice(0, maxAvatarsInRow).map((icon, index) => ( - - - - - - ))} - {avatars.length > maxAvatarsInRow && ( - - - - {`+${avatars.length - maxAvatarsInRow}`} - - - - )} + + + + + ))} + {avatars.length > maxAvatarsInRow && ( + + + + {`+${avatars.length - maxAvatarsInRow}`} + + + + )} + + )) + ) : ( + + + + {/* View is necessary for tooltip to show for multiple avatars in LHN */} + + - )) - ) : ( - - + + + {icons.length === 2 ? ( - {/* View is necessary for tooltip to show for multiple avatars in LHN */} - - {icons.length === 2 ? ( - + + - - - - - ) : ( - - - - {`+${icons.length - 1}`} - - - - )} - - + {`+${icons.length - 1}`} + + + + )} - )} - + + ); } diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 93c744225237..7b45fd963fe7 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -262,32 +262,29 @@ function OptionRow({ /> )} - {showSelectedState && ( - <> - {shouldShowSelectedStateAsButton && !isSelected ? ( -