diff --git a/.eslintrc.js b/.eslintrc.js index 5c23c7be0839..6194ccd39d3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,26 @@ const restrictedImportPaths = [ importNames: ['CSSProperties'], message: "Please use 'ViewStyle', 'TextStyle', 'ImageStyle' from 'react-native' instead.", }, + { + name: '@styles/index', + importNames: ['default', 'defaultStyles'], + message: 'Do not import styles directly. Please use the `useThemeStyles` hook or `withThemeStyles` HOC instead.', + }, + { + name: '@styles/utils', + importNames: ['default', 'DefaultStyleUtils'], + message: 'Do not import StyleUtils directly. Please use the `useStyleUtils` hook or `withStyleUtils` HOC instead.', + }, + { + name: '@styles/theme', + importNames: ['default', 'defaultTheme'], + + message: 'Do not import themes directly. Please use the `useTheme` hook or `withTheme` HOC instead.', + }, + { + name: '@styles/theme/illustrations', + message: 'Do not import theme illustrations directly. Please use the `useThemeIllustrations` hook instead.', + }, ]; const restrictedImportPatterns = [ @@ -31,6 +51,18 @@ const restrictedImportPatterns = [ group: ['**/assets/animations/**/*.json'], message: "Do not import animations directly. Please use the 'src/components/LottieAnimations' import instead.", }, + { + group: ['@styles/theme/themes/**'], + message: 'Do not import themes directly. Please use the `useTheme` hook or `withTheme` HOC instead.', + }, + { + group: ['@styles/utils/**', '!@styles/utils/FontUtils', '!@styles/utils/types'], + message: 'Do not import style util functions directly. Please use the `useStyleUtils` hook or `withStyleUtils` HOC instead.', + }, + { + group: ['@styles/theme/illustrations/themes/**'], + message: 'Do not import theme illustrations directly. Please use the `useThemeIllustrations` hook instead.', + }, ]; module.exports = { @@ -171,6 +203,21 @@ module.exports = { '@typescript-eslint/switch-exhaustiveness-check': 'error', '@typescript-eslint/consistent-type-definitions': ['error', 'type'], '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + prefer: 'type-imports', + fixStyle: 'separate-type-imports', + }, + ], + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/consistent-type-exports': [ + 'error', + { + fixMixedExportsWithInlineTypeSpecifier: false, + }, + ], + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'es/no-nullish-coalescing-operators': 'off', 'es/no-optional-chaining': 'off', 'valid-jsdoc': 'off', diff --git a/.github/actions/javascript/authorChecklist/categories/index.ts b/.github/actions/javascript/authorChecklist/categories/index.ts index 53e0ed2ab838..5ed63c647e74 100644 --- a/.github/actions/javascript/authorChecklist/categories/index.ts +++ b/.github/actions/javascript/authorChecklist/categories/index.ts @@ -1,4 +1,4 @@ -import Category from './Category'; +import type Category from './Category'; import newComponent from './newComponentCategory'; const categories: Category[] = [newComponent]; diff --git a/.github/actions/javascript/authorChecklist/categories/newComponentCategory.ts b/.github/actions/javascript/authorChecklist/categories/newComponentCategory.ts index 63e26c015a5a..405a687b5d3e 100644 --- a/.github/actions/javascript/authorChecklist/categories/newComponentCategory.ts +++ b/.github/actions/javascript/authorChecklist/categories/newComponentCategory.ts @@ -4,7 +4,7 @@ import traverse from '@babel/traverse'; import CONST from '../../../../libs/CONST'; import GithubUtils from '../../../../libs/GithubUtils'; import promiseSome from '../../../../libs/promiseSome'; -import Category from './Category'; +import type Category from './Category'; type SuperClassType = {superClass: {name?: string; object: {name: string}; property: {name: string}} | null; name: string}; diff --git a/.github/actions/javascript/validateReassureOutput/action.yml b/.github/actions/javascript/validateReassureOutput/action.yml index 4fd53e838fb5..b3b18c244a8f 100644 --- a/.github/actions/javascript/validateReassureOutput/action.yml +++ b/.github/actions/javascript/validateReassureOutput/action.yml @@ -7,9 +7,6 @@ inputs: COUNT_DEVIATION: description: Allowable deviation for the mean count in regression test results. required: true - REGRESSION_OUTPUT: - description: Refers to the results obtained from regression tests `.reassure/output.json`. - required: true runs: using: 'node20' main: './index.js' diff --git a/.github/actions/javascript/validateReassureOutput/index.js b/.github/actions/javascript/validateReassureOutput/index.js index 6cc59af1de48..e70c379697cd 100644 --- a/.github/actions/javascript/validateReassureOutput/index.js +++ b/.github/actions/javascript/validateReassureOutput/index.js @@ -8,9 +8,10 @@ /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const core = __nccwpck_require__(186); +const fs = __nccwpck_require__(147); const run = () => { - const regressionOutput = JSON.parse(core.getInput('REGRESSION_OUTPUT', {required: true})); + const regressionOutput = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')); const countDeviation = core.getInput('COUNT_DEVIATION', {required: true}); const durationDeviation = core.getInput('DURATION_DEVIATION_PERCENTAGE', {required: true}); diff --git a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js index da81d88c9885..214dc9e8b6d4 100644 --- a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js +++ b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js @@ -1,7 +1,8 @@ const core = require('@actions/core'); +const fs = require('fs'); const run = () => { - const regressionOutput = JSON.parse(core.getInput('REGRESSION_OUTPUT', {required: true})); + const regressionOutput = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')); const countDeviation = core.getInput('COUNT_DEVIATION', {required: true}); const durationDeviation = core.getInput('DURATION_DEVIATION_PERCENTAGE', {required: true}); diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 6aeecb3b4e05..813c341caaf6 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -10,7 +10,6 @@ on: env: SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} - DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer concurrency: group: ${{ github.workflow }}-${{ github.event_name }} @@ -174,6 +173,8 @@ jobs: name: Build and deploy iOS needs: validateActor if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} + env: + DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer runs-on: macos-13-xlarge steps: - name: Checkout diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index c49530c46faa..64b4536d9241 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -36,17 +36,9 @@ jobs: npm install --force npx reassure --branch - - name: Read output.json - id: reassure - uses: juliangruber/read-file-action@v1 - with: - path: .reassure/output.json - - name: Validate output.json id: validateReassureOutput uses: ./.github/actions/javascript/validateReassureOutput with: DURATION_DEVIATION_PERCENTAGE: 20 COUNT_DEVIATION: 0 - REGRESSION_OUTPUT: ${{ steps.reassure.outputs.content }} - diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 8b18b8aa5d53..25a14fb27e1b 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -10,9 +10,6 @@ on: types: [opened, synchronize, labeled] branches: ['*ci-test/**'] -env: - DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer - jobs: validateActor: runs-on: ubuntu-latest @@ -139,6 +136,7 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} env: PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }} + DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer runs-on: macos-13-xlarge steps: - name: Checkout diff --git a/android/app/build.gradle b/android/app/build.gradle index e93150b48fca..395e87664e99 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042101 - versionName "1.4.21-1" + versionCode 1001042300 + versionName "1.4.23-0" } flavorDimensions "default" diff --git a/assets/emojis/common.ts b/assets/emojis/common.ts index cbefb21cf2d6..b23383590c51 100644 --- a/assets/emojis/common.ts +++ b/assets/emojis/common.ts @@ -7,7 +7,7 @@ import TravelAndPlaces from '@assets/images/emojiCategoryIcons/plane.svg'; import AnimalsAndNature from '@assets/images/emojiCategoryIcons/plant.svg'; import Activities from '@assets/images/emojiCategoryIcons/soccer-ball.svg'; import FrequentlyUsed from '@assets/images/history.svg'; -import {HeaderEmoji, PickerEmojis} from './types'; +import type {HeaderEmoji, PickerEmojis} from './types'; const skinTones = [ { diff --git a/assets/emojis/en.ts b/assets/emojis/en.ts index 0a1ca7611117..28051e5ecd99 100644 --- a/assets/emojis/en.ts +++ b/assets/emojis/en.ts @@ -1,4 +1,4 @@ -import {EmojisList} from './types'; +import type {EmojisList} from './types'; /* eslint-disable @typescript-eslint/naming-convention */ const enEmojis: EmojisList = { diff --git a/assets/emojis/es.ts b/assets/emojis/es.ts index 46f825643859..0d23f887f556 100644 --- a/assets/emojis/es.ts +++ b/assets/emojis/es.ts @@ -1,4 +1,4 @@ -import {EmojisList} from './types'; +import type {EmojisList} from './types'; /* eslint-disable @typescript-eslint/naming-convention */ const esEmojis: EmojisList = { diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts index bc62adc4b252..02328001674e 100644 --- a/assets/emojis/index.ts +++ b/assets/emojis/index.ts @@ -1,10 +1,13 @@ +import type {Locale} from '@src/types/onyx'; import emojis from './common'; import enEmojis from './en'; import esEmojis from './es'; -import {Emoji} from './types'; +import type {Emoji, EmojisList} from './types'; type EmojiTable = Record; +type LocaleEmojis = Partial>; + const emojiNameTable = emojis.reduce((prev, cur) => { const newValue = prev; if (!('header' in cur) && cur.name) { @@ -26,10 +29,10 @@ const emojiCodeTableWithSkinTones = emojis.reduce((prev, cur) => { return newValue; }, {}); -const localeEmojis = { +const localeEmojis: LocaleEmojis = { en: enEmojis, es: esEmojis, -} as const; +}; export default emojis; export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis}; diff --git a/assets/emojis/types.ts b/assets/emojis/types.ts index c3a3b692f202..e659924a7fa4 100644 --- a/assets/emojis/types.ts +++ b/assets/emojis/types.ts @@ -1,9 +1,9 @@ -import IconAsset from '@src/types/utils/IconAsset'; +import type IconAsset from '@src/types/utils/IconAsset'; type Emoji = { code: string; name: string; - types?: string[]; + types?: readonly string[]; }; type HeaderEmoji = { @@ -12,8 +12,10 @@ type HeaderEmoji = { code: string; }; -type PickerEmojis = Array; +type PickerEmoji = Emoji | HeaderEmoji; + +type PickerEmojis = PickerEmoji[]; type EmojisList = Record; -export type {Emoji, HeaderEmoji, EmojisList, PickerEmojis}; +export type {Emoji, HeaderEmoji, EmojisList, PickerEmojis, PickerEmoji}; diff --git a/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg new file mode 100644 index 000000000000..829d3ee2e3fe --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__hourglass.svg b/assets/images/simple-illustrations/simple-illustration__hourglass.svg new file mode 100644 index 000000000000..539e1e45b795 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__hourglass.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__trashcan.svg b/assets/images/simple-illustrations/simple-illustration__trashcan.svg new file mode 100644 index 000000000000..4e66efa0a67e --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__trashcan.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md index a583941bf71d..b60c28147a45 100644 --- a/contributingGuides/TS_STYLE.md +++ b/contributingGuides/TS_STYLE.md @@ -26,6 +26,7 @@ - [1.19 Satisfies operator](#satisfies-operator) - [1.20 Hooks instead of HOCs](#hooks-instead-of-hocs) - [1.21 `compose` usage](#compose-usage) + - [1.22 Type imports](#type-imports) - [Exception to Rules](#exception-to-rules) - [Communication Items](#communication-items) - [Migration Guidelines](#migration-guidelines) @@ -383,7 +384,7 @@ type Foo = { -- [1.15](#file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. Do not use [`satisfies` operator](#satisfies-operator) for platform-specific implementations, always define shared types that complies with all variants. +- [1.15](#file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. Do not use [`satisfies` operator](#satisfies-operator) for platform-specific implementations, always define shared types that complies with all variants. > Why? To encourage consistent API across platform-specific implementations. If you're migrating module that doesn't have a default implement (i.e. `index.ts`, e.g. `getPlatform`), refer to [Migration Guidelines](#migration-guidelines) for further information. @@ -514,7 +515,7 @@ type Foo = { - [1.20](#hooks-instead-of-hocs) **Hooks instead of HOCs**: Replace HOCs usage with Hooks whenever possible. - + > Why? Hooks are easier to use (can be used inside the function component), and don't need nesting or `compose` when exporting the component. It also allows us to remove `compose` completely in some components since it has been bringing up some issues with TypeScript. Read the [`compose` usage](#compose-usage) section for further information about the TypeScript issues with `compose`. > Note: Because Onyx doesn't provide a hook yet, in a component that accesses Onyx data with `withOnyx` HOC, please make sure that you don't use other HOCs (if applicable) to avoid HOC nesting. @@ -571,7 +572,7 @@ type Foo = { - [1.21](#compose-usage) **`compose` usage**: Avoid the usage of `compose` function to compose HOCs in TypeScript files. Use nesting instead. - + > Why? `compose` function doesn't work well with TypeScript when dealing with several HOCs being used in a component, many times resulting in wrong types and errors. Instead, nesting can be used to allow a seamless use of multiple HOCs and result in a correct return type of the compoment. Also, you can use [hooks instead of HOCs](#hooks-instead-of-hocs) whenever possible to minimize or even remove the need of HOCs in the component. ```ts @@ -607,6 +608,38 @@ type Foo = { export default withCurrentUserPersonalDetails(ComponentWithReportOrNotFound); ``` + + +- [1.22](#type-imports) **Type imports/exports**: Always use the `type` keyword when importing/exporting types + + > Why? In order to improve code clarity and consistency and reduce bundle size after typescript transpilation, we enforce the all type imports/exports to contain the `type` keyword. This way, TypeScript can automatically remove those imports from the transpiled JavaScript bundle + + Imports: + ```ts + // BAD + import {SomeType} from './a' + import someVariable from './a' + + import {someVariable, SomeOtherType} from './b' + + // GOOD + import type {SomeType} from './a' + import someVariable from './a' + ``` + + Exports: + ```ts + // BAD + export {SomeType} + export someVariable + // or + export {someVariable, SomeOtherType} + + // GOOD + export type {SomeType} + export someVariable + ``` + ## Exception to Rules Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. The internal engineer assigned to the PR should be the one that approves each exception, however all discussion regarding granting exceptions should happen in the public channel instead of the GitHub PR page so that the TS migration team can access them easily. diff --git a/docs/articles/expensify-classic/account-settings/Account-Details.md b/docs/articles/expensify-classic/account-settings/Account-Details.md index bc4b94bf8a51..535e74eeb701 100644 --- a/docs/articles/expensify-classic/account-settings/Account-Details.md +++ b/docs/articles/expensify-classic/account-settings/Account-Details.md @@ -60,10 +60,12 @@ Is your Secondary Login (personal email) invalidated in your company account? If 4. Head to your personal email account and follow the prompts 5. You'll receive a link in the email to click that will unlink the two accounts -# FAQ +{% include faq-begin.md %} ## The profile picture on my account updated automatically. Why did this happen? Our focus is always on making your experience user-friendly and saving you valuable time. One of the ways we achieve this is by utilizing a public API to retrieve public data linked to your email address. This tool searches for public accounts or profiles associated with your email address, such as on LinkedIn. When it identifies one, it pulls in the uploaded profile picture and name to Expensify. While this automated process is generally accurate, there may be instances where it's not entirely correct. If this happens, we apologize for any inconvenience caused. The good news is that rectifying such situations is a straightforward process. You can quickly update your information manually by following the directions provided above, ensuring your data is accurate and up to date in no time. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/account-settings/Close-Account.md b/docs/articles/expensify-classic/account-settings/Close-Account.md index c25c22de9704..9b1e886fc94a 100644 --- a/docs/articles/expensify-classic/account-settings/Close-Account.md +++ b/docs/articles/expensify-classic/account-settings/Close-Account.md @@ -114,10 +114,12 @@ Here's how to do it: By following these steps, you can easily verify your email or phone number and close an unwanted Expensify account. -# FAQ +{% include faq-begin.md %} ## What should I do if I'm not directed to my account when clicking the validate option from my phone or email? It's possible your browser has blocked this, either because of some existing cache or extension. In this case, you should follow the Reset Password flow to reset the password and manually gain access with the new password, along with your email address. ## Why don't I see the Close Account option? It's possible your account is on a managed company domain. In this case, only the admins from that company can close it. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/account-settings/Copilot.md b/docs/articles/expensify-classic/account-settings/Copilot.md index 4fac402b7ced..31bc0eff60e6 100644 --- a/docs/articles/expensify-classic/account-settings/Copilot.md +++ b/docs/articles/expensify-classic/account-settings/Copilot.md @@ -59,7 +59,7 @@ To ensure a receipt is routed to the Expensify account in which you are a copilo 3. Send -# FAQ +{% include faq-begin.md %} ## Can a Copilot's Secondary Login be used to forward receipts? Yes! A Copilot can use any of the email addresses tied to their account to forward receipts into the account of the person they're assisting. @@ -67,3 +67,5 @@ Yes! A Copilot can use any of the email addresses tied to their account to forwa No, only the original account holder can add another Copilot to the account. ## Is there a restriction on the number of Copilots I can have or the number of users for whom I can act as a Copilot? There is no limit! You can have as many Copilots as you like, and you can be a Copilot for as many users as you need. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/account-settings/Merge-Accounts.md b/docs/articles/expensify-classic/account-settings/Merge-Accounts.md index abb218c74118..34bf422aa983 100644 --- a/docs/articles/expensify-classic/account-settings/Merge-Accounts.md +++ b/docs/articles/expensify-classic/account-settings/Merge-Accounts.md @@ -19,7 +19,7 @@ Merging two accounts together is fairly straightforward. Let’s go over how to 8. Paste the code into the required field If you have any questions about this process, feel free to reach out to Concierge for some assistance! -# FAQ +{% include faq-begin.md %} ## Can you merge accounts from the mobile app? No, accounts can only be merged from the full website at expensify.com. ## Can I administratively merge two accounts together? @@ -34,3 +34,5 @@ Yes! Please see below: - If you have two accounts with two different verified domains, you cannot merge them together. ## What happens to my “personal” Individual workspace when merging accounts? The old “personal” Individual workspace is deleted. If you plan to submit reports under a different workspace in the future, ensure that any reports on the Individual workspace in the old account are marked as Open before merging the accounts. You can typically do this by selecting “Undo Submit” on any submitted reports. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements.md index 7313c73ac6e6..5a5827149a4f 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements.md @@ -75,7 +75,7 @@ Examples of additional requested information: - An authorization letter - An independently certified documentation such as shareholder agreement from a lawyer, notary, or public accountant if an individual owns more than 25% of the company -# FAQ +{% include faq-begin.md %} ## How many people can send reimbursements internationally? @@ -103,3 +103,4 @@ This is the person who will process international reimbursements. The authorized You can leave this form section blank since the “User” is Expensify. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md index c41178b4aa7f..05149ebf868e 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md @@ -59,7 +59,7 @@ Expenses can be imported as either reimbursable or non-reimbursable. Select the *Remove a card*: If you need to remove a card, you can select the red trash can icon. Please remember this will remove all unreported and un-submitted transactions from your account that are tied to this card, so be careful! -# FAQ +{% include faq-begin.md %} *Is the bank/credit card import option right for me?* If you incur expenses using your personal or business card and need to get them accounted for in your company’s accounting, then you might want to import your bank/credit card. Please note, if you have a company-assigned corporate card, check with your company's Expensify admin on how to handle these cards. Often, admins will take care of card assignments, and you won't need to import them yourself. @@ -74,3 +74,5 @@ If you aren't able to see the expenses imported when you click “View expenses *How do I remove an imported spreadsheet?* If you need to remove an imported spreadsheet, you can select the red trash can icon. Please remember this will remove all unreported and unsubmitted transactions from your account that are tied to this import, so be careful! + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md index b59f68a65ce6..8c5ead911da4 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md @@ -36,7 +36,7 @@ You can complete this process either via the web app (on a computer), or via the If you are new to using Batch Payments in Australia, to reimburse your staff or process payroll, you may want to check out these bank-specific instructions for how to upload your .aba file: - ANZ Bank - [Import a file for payroll payments](https://www.anz.com.au/support/internet-banking/pay-transfer-business/payroll/import-file/) -- CommBank - [Importing and using
 Direct Entry (EFT) files](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf) +- CommBank - [Importing and using Direct Entry (EFT) files](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf) - Westpac - [Importing Payment Files](https://www.westpac.com.au/business-banking/online-banking/support-faqs/import-files/) - NAB - [Quick Reference Guide - Upload a payment file](https://www.nab.com.au/business/online-banking/nab-connect/help) - Bendigo Bank - [Bulk payments user guide](https://www.bendigobank.com.au/globalassets/documents/business/bulk-payments-user-guide.pdf) diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md index 2fbdac02e85c..4ae2c669561f 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md @@ -127,7 +127,7 @@ If you get a generic error message that indicates, "Something's gone wrong", ple 8. If you have another phone available, try to follow these steps on that device If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. -# FAQ +{% include faq-begin.md %} ## What is a Beneficial Owner? A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. @@ -157,3 +157,5 @@ It's a good idea to wait till the end of that second business day. If you still Make sure to reach out to your Account Manager or to Concierge once you have done so and our team will be able to re-trigger those 3 transactions! + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md index fc1e83701caf..fd50c245d568 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md @@ -93,9 +93,11 @@ Then, try to upload the revised spreadsheet again: 3. Check the row count again on the Output Preview to confirm it matches the spreadsheet 4. Click **Submit Spreadsheet** -# FAQ +{% include faq-begin.md %} ## Why can't I see my CSV transactions immediately after uploading them? Don't worry! You'll typically need to wait 1-2 minutes after clicking **I understand, I'll wait!** ## I'm trying to import a credit. Why isn't it uploading? Negative expenses shouldn't include a minus sign. Instead, they should just be wrapped in parentheses. For example, to indicate "-335.98," you'll want to make sure it's formatted as "(335.98)." + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md index a60c1ab7831a..f46c1a1442c2 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md @@ -99,7 +99,7 @@ To completely remove the card connection, unassign every card from the list and Note: If expenses are Processing and then rejected, they will also be deleted when they're returned to an Open state as the card they're linked to no longer exists. -# FAQ +{% include faq-begin.md %} ## My Commercial Card feed is set up. Why is a specific card not coming up when I try to assign it to an employee? Cards will appear in the drop-down when activated and have at least one posted transaction. If the card is activated and has been used for a while and you're still not seeing it, please reach out to your Account Manager or message concierge@expensify.com for further assistance. @@ -124,3 +124,5 @@ If your company uses a Commercial Card program that isn’t with one of our Appr - Stripe - Brex + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md index fa5879d85ea8..bc9801060223 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md @@ -84,8 +84,10 @@ Expensify eReceipts serve as digital substitutes for paper receipts in your purc To ensure seamless automatic importation, it's essential to maintain your transactions in US Dollars. Additionally, eReceipts can be directly imported from your bank account. Please be aware that CSV/OFX imported files of bank transactions do not support eReceipts. It's important to note that eReceipts are not generated for lodging expenses. Moreover, due to incomplete or inaccurate category information from certain banks, there may be instances of invalid eReceipts being generated for hotel purchases. If you choose to re-categorize expenses, a similar situation may arise. It's crucial to remember that our Expensify eReceipt Guarantee excludes coverage for hotel and motel expenses. -# FAQ +{% include faq-begin.md %} ## What plan/subscription is required in order to manage corporate cards? Group Policy (Collect or Control plan only) ## When do my company card transactions import to Expensify? Credit card transactions are imported to Expensify once they’re posted to the bank account. This usually takes 1-3 business days between the point of purchase and when the transactions populate in your account. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md index 59104ce36a41..9844622f8539 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md @@ -24,7 +24,7 @@ Importing your ANZ Visa into Expensify will allow your card transactions to flow 4. Once you’ve filled out and submitted your Internet Banking data authority form or ANZ Direct Online authority form, ANZ will set up the feed and send all the details directly to Expensify. 5. Then, we’ll add the card feed to your Expensify account and send you a message to let you know that it has been set up. We'll also include some webinar training resources to ensure you have all the information you need! -# FAQ +{% include faq-begin.md %} ## Are there discounts available for ANZ customers? As ANZ’s preferred receipt tracking and expense management partner, Expensify offers ANZ business customers a few special perks: @@ -44,3 +44,5 @@ After the free trial, you’ll get preferred pricing at 50% off the current rate ## Do I need to sign up for a specific period in order to receive the discount? There is no obligation to sign up for a certain period to receive the discount. After your free trial, the 50% discount for the first 12 months, will be applied automatically to your account. After the initial 12 months, the 15% discount will also be applied automatically. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md index 372edd8f14ec..c9720177a8fc 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md @@ -72,7 +72,7 @@ If you need to connect a separate card program from the same bank (that's access To fix this, you would need to contact your bank and request to combine all of your cards under a single set of login credentials. That way, you can connect all of your cards from that bank to Expensify using a single set of login credentials. -# FAQ +{% include faq-begin.md %} ## How can I connect and manage my company’s cards centrally if I’m not a domain admin? If you cannot access Domains, you must request Domain Admin access to an existing Domain Admin (usually the workspace owner). @@ -112,3 +112,5 @@ If you've answered "yes" to any of these questions, you'll need to update this i A Domain Admin can fix the connection by heading to **Settings > Domains > _Domain Name_ > Company Cards > Fix**. You will be prompted to enter the new credentials/updated information, and this should reestablish the connection. If you are still experiencing issues with the card connection, please search for company card troubleshooting or contact Expensify Support for help. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md index d6de2ca66ade..2cb684a2240b 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md @@ -51,7 +51,7 @@ If there are still unapproved expenses when you want to close your books for the - Match Approved Total to Company Card Liability account in your accounting system. - Unapproved Total becomes the Accrual amount (provided the first two amounts are correct). -# FAQ +{% include faq-begin.md %} ## Who can view and access the Reconciliation tab? @@ -67,3 +67,5 @@ If a cardholder reports expenses as missing, we first recommend using the Reconc If after updating, the expense still hasn’t appeared, you should reach out to Concierge with the missing expense specifics (merchant, date, amount and last four digits of the card number). Please note, only posted transactions will import. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md index a4ff7503f7bb..0bc5cb0ad955 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md @@ -56,7 +56,7 @@ You should be all set! The bank account will display as a deposit-only business 1. Navigate to **Settings > Account > Payments > Bank Accounts** 2. Click the **Delete** next to the bank account you want to remove -# FAQ +{% include faq-begin.md %} ## **What happens if my bank requires an additional security check before adding it to a third-party?** @@ -73,3 +73,5 @@ There are a few reasons a reimbursement may be unsuccessful. The first step is t - Your account wasn’t set up for Direct Deposit/ACH. You may want to contact your bank to confirm. If you aren’t sure, please reach out to Concierge and we can assist! + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md index 30a507a1f9df..09dd4de2867b 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md @@ -26,7 +26,7 @@ If at least 50% of your approved USD spend in a given month is on your company Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more. Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. ## Savings calculator To see how much money you can save (and even earn!) by using the Expensify Card, check out our [savings calculator](https://use.expensify.com/price-savings-calculator). Just enter a few details and see how much you’ll save! -# FAQ +{% include faq-begin.md %} ## What if we put less than 50% of our total spend on the Expensify Card? If you put less than 50% of your total USD spend on your Expensify Card, your bill gets discounted on a sliding scale based on the percentage of use. So if you don't use the Expensify Card at all, you'll be charged the full rate for each member based on your plan and subscription. Example: @@ -36,3 +36,5 @@ Example: You save 70% on the price per member on your bill for that month. Note: USD spend refers to approved USD transactions on the Expensify Card in any given month, compared to all approved USD spend on workspaces in that same month. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md index 4fd7ef71c2e7..49a369c3cb51 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md @@ -38,7 +38,7 @@ To take over billing for the entire domain, you must: 1. Go to **Settings > Domains > _Domain Name_ > Domain Admins** and enable Consolidated Domain Billing. Currently, Consolidated Domain Billing simply consolidates the amounts due for each Group Workspace Billing Owner (listed on the **Settings > Workspaces > Group** page). If you want to use the Annual Subscription across all Workspaces on the domain, you must also be the Billing Owner of all Group Workspaces. -# FAQ +{% include faq-begin.md %} ## Why can't I see the option to take over billing? There could be two reasons: 1. You may not have the role of Workspace Admin. If you can't click on the Workspace name (if it's not a blue hyperlink), you're not a Workspace Admin. Another Workspace Admin for that Workspace must change your role before you can proceed. @@ -47,3 +47,5 @@ There could be two reasons: There are two ways to resolve this: 1. Have your IT dept. gain access to the account so that you can make yourself an admin. Your IT department may need to recreate the ex-employee's email address. Once your IT department has access to the employee's Home page, you can request a magic link to be sent to that email address to gain access to the account. 1. Have another admin make you a Workspace admin. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md index f01bb963bacf..1e631a53b0b3 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md @@ -76,9 +76,11 @@ Note: Refunds apply to Collect or Control Group Workspaces with one month of bil Once you’ve successfully downgraded to a free Expensify account, your Workspace will be deleted and you will see a refund line item added to your Billing History. -# FAQ +{% include faq-begin.md %} ## Will I be charged for a monthly subscription even if I don't use SmartScans? Yes, the Monthly Subscription is prepaid and not based on activity, so you'll be charged regardless of usage. ## I'm on a group policy; do I need the monthly subscription too? Probably not. Group policy members already have unlimited SmartScans, so there's usually no need to buy the subscription. However, you can use it for personal use if you leave your company's Workspace. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md b/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md index 35f6a428e0af..2e829c0785d3 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md @@ -16,8 +16,10 @@ When a Domain Admin enables Consolidated Domain Billing, all Group workspaces ow If you don’t have multiple billing owners across your organization, or if you want to keep billing separate for any reason, then this feature isn’t necessary. If you have an Annual Subscription and enable Consolidated Domain Billing, the Consolidated Domain Billing feature will gather the amounts due for each Group workspace Billing Owner (listed under **Settings > Workspaces > Group**). To make full use of the Annual Subscription for all workspaces in your domain, you should also be the billing owner for all Group workspaces. -# FAQ +{% include faq-begin.md %} ## How do I take over the billing of a workspace with Consolidated Domain Billing enabled? You’ll have to toggle off Consolidated Domain Billing, take over ownership of the workspace, and then toggle it back on. ## Can I use Consolidated Domain Billing to cover the bill for some workspaces, but not others? No, this feature means that you’ll be paying the bill for all domain members who choose a subscription. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md index 4f660588d432..e6d8f2fedb73 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md @@ -31,7 +31,7 @@ To access these extra free weeks, all you need to do is complete the tasks on yo - Establish a connection between Expensify and your accounting system from the outset. By doing this early, you can start testing Expensify comprehensively from end to end. -# FAQ +{% include faq-begin.md %} ## What happens when my Free Trial ends? If you’ve already added a billing card to Expensify, you will automatically start your organization’s Expensify subscription after your Free Trial ends. At the beginning of the following month, we'll bill the card you have on file for your subscription, adjusting the charge to exclude the Free Trial period. If your Free Trial concludes without a billing card on file, you will see a notification on your Home page saying, 'Your Free Trial has expired.' @@ -42,3 +42,5 @@ If you continue without adding a billing card, you will be granted a five-day gr If you’d like to downgrade to an individual account after your Free Trial has ended, you will need to delete any Group Workspace that you have created. This action will remove the Workspaces, subscription, and any amount owed. You can do this in one of two ways from the Expensify web app: - Select the “Downgrade” option on the billing card task on your Home page. - Go to **Settings > Workspaces > [Workspace name]**, then click the gear button next to the Workspace and select Delete. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md index aa08340dd7a6..1d952cb15b1c 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md @@ -48,7 +48,7 @@ After purchasing the subscription from the App Store, remember to sync your app The subscription renewal date is the same as the purchase date. For instance, if you sign up for the subscription on September 7th, it will renew automatically on October 7th. You can cancel your subscription anytime during the month if you no longer need unlimited SmartScans. If you do cancel, keep in mind that your subscription (and your ability to SmartScan) will continue until the last day of the billing cycle. -# FAQ +{% include faq-begin.md %} ## Can I use an Individual Subscription while on a Collect or Control Plan? You can! If you want to track expenses separately from your organization’s Workspace, you can sign up for an Individual Subscription. However, only Submit and Track Workspace plans are available when on an Individual Subscription. Collect and Control Workspace plans require an annual or pay-per-use subscription. For more information, visit expensify.com/pricing. @@ -65,3 +65,5 @@ Your subscription is a pre-purchase for 30 days of unlimited SmartScanning. This ## How can I cancel my subscription from the iOS app? If you signed up for the Monthly Subscription via iOS and your iTunes account, you will need to log into iTunes and locate the subscription there in order to cancel it. The ability to cancel an Expensify subscription started via iOS is strictly limited to your iTunes account. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md index 2133e8c7da46..326ce7fe33ab 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md @@ -11,7 +11,7 @@ Pay-per-use is a billing option for people who prefer to use Expensify month to 1. Create a Group Workspace if you haven’t already by going to **Settings > Workspaces > Group > New Workspace** 2. Once you’ve created your Workspace, under the “Subscription” section on the Group Workspace page, select “Pay-per-use”. -# FAQ +{% include faq-begin.md %} ## What is considered an active user? An active user is anyone who chats, creates, modifies, submits, approves, reimburses, or exports a report in Expensify. This includes actions taken by a Copilot and Workspace automation (such as Scheduled Submit and automated reimbursement). If no one on your Group Workspace uses Expensify in a given month, you will not be billed for that month. @@ -26,4 +26,4 @@ If you expect to have an increased number of users for more than 3 out of 12 mon ## Will billing only be in USD currency? While USD is the default billing currency, we also have GBP, AUD, and NZD billing currencies. You can see the rates on our [pricing](https://www.expensify.com/pricing) page. - +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md b/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md index 33fbec003a91..92c92e4e3a44 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md @@ -15,6 +15,8 @@ Once your account is marked as tax-exempt, the corresponding state tax will no l If you need to remove your tax-exempt status, let your Account Manager know or contact Concierge. -# FAQ +{% include faq-begin.md %} ## What happens to my past Expensify bills that incorrectly had tax added to them? Expensify can provide a refund for the tax you were charged on your previous bills. Please let your Account Manager know or contact Concierge if this is the case. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md b/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md index 7f3d83af1e6e..a0bd2c442dbb 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md @@ -20,7 +20,7 @@ Every expense has an Attendees field and will list the expense creator’s name ![image of an expense with attendee tracking]({{site.url}}/assets/images/attendee-tracking.png){:width="100%"} -# FAQ +{% include faq-begin.md %} ## Can I turn off attendee tracking? Attendee tracking is a standard field on all expenses and cannot be turned off. @@ -49,3 +49,4 @@ There is no limit. ## How can I remove attendees from an expense? You can add or remove attendees from an expense as long as they are on a Draft report. Expenses on submitted reports cannot be edited, so you cannot remove attendees from these. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Currency.md b/docs/articles/expensify-classic/expense-and-report-features/Currency.md index eb6ca9bb2d40..77b5fbbb3ebc 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Currency.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Currency.md @@ -46,7 +46,7 @@ Then, set the default currency for that workspace to match the currency in which For example, if you have employees in the US, France, Japan, and India, you’d want to create four separate workspaces, add the employees to each, and then set the corresponding currency for each workspace. -# FAQ +{% include faq-begin.md %} ## I have expenses in several different currencies. How will this show up on a report? @@ -60,5 +60,4 @@ Expenses entered in a foreign currency are automatically converted to the defaul If you want to bypass the exchange rate conversion, you can manually enter an expense in your default currency instead. - - +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md b/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md index ae6a9ca77db1..295aa8d00cc9 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md @@ -45,11 +45,11 @@ In general, your expense rules will be applied in order, from **top to bottom**, 4. If you belong to a workspace that is tied to an accounting integration, the configuration settings for this connection may update your expense details upon export, even if the expense rules were successfully applied to the expense. -# FAQ +{% include faq-begin.md %} ## How can I use Expense Rules to vendor match when exporting to an accounting package? When exporting non-reimbursable expenses to your connected accounting package, the payee field will list "Credit Card Misc." if the merchant name on the expense in Expensify is not an exact match to a vendor in the accounting package. When an exact match is unavailable, "Credit Card Misc." prevents multiple variations of the same vendor (e.g., Starbucks and Starbucks #1234, as is often seen in credit card statements) from being created in your accounting package. For repeated expenses, the best practice is to use Expense Rules, which will automatically update the merchant name without having to do it manually each time. This only works for connections to QuickBooks Online, Desktop, and Xero. Vendor matching cannot be performed in this manner for NetSuite or Sage Intacct due to limitations in the API of the accounting package. - +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md b/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md index 795a895e81f0..9d19dbb4f9ba 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md @@ -26,7 +26,7 @@ Each report will show the total amount for all expenses in the upper right. Unde - **Time Expenses:** Employees or jobs are billed based on an hourly rate that you can set within Expensify. - **Distance Expenses:** These expenses are related to travel for work. -# FAQ +{% include faq-begin.md %} ## What’s the difference between a receipt, an expense, and a report attachment? @@ -40,3 +40,5 @@ In Expensify, a credit is displayed as an expense with a minus (ex. -$1.00) in f If a report includes a credit or a refund expense, it will offset the total amount on the report. For example, the report has two reimbursable expenses, $400 and $500. The total Reimbursable is $900. Conversely, a -$400 and $500 will be a total Reimbursable amount of $500 + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md b/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md index 229ca4ec1fe4..04183608e3d1 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md @@ -49,7 +49,7 @@ Report comments initially trigger a mobile app notification to report participan Comments can be formatted with bold, italics, or strikethrough using basic Markdown formatting. You can also add receipts and supporting documents to a report by clicking the paperclip icon on the right side of the comment field. -# FAQ +{% include faq-begin.md %} ## Why don’t some timestamps in Expensify match up with what’s shown in the report audit log? @@ -58,3 +58,5 @@ While the audit log is localized to your own timezone, some other features in Ex ## Is commenting on a report a billable action? Yes. If you comment on a report, you become a billable actor for the current month. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md b/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md index 5431355dd790..57a7f7de298c 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md +++ b/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md @@ -55,7 +55,7 @@ Select the expenses you want to export by checking the box to the left of each e Then, click **Export To** in the upper right corner of the page, and choose our default CSV format or create your own custom CSV template. -# FAQ +{% include faq-begin.md %} ## Can I use the filters and analytics features on the mobile app? The various features on the Expenses Page are only available while logged into your web account. @@ -71,3 +71,4 @@ We have more about company card expense reconciliation in this [support article] ## Can I edit multiple expenses at once? Yes! Select the expenses you want to edit and click **Edit Multiple**. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md b/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md index ff9e2105ffac..9c55cd9b4b8d 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md +++ b/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md @@ -31,7 +31,7 @@ To export a report to a CSV file, follow these steps on the Reports page: 2. Navigate to the upper right corner of the page and click the "Export to" button. 3. From the drop-down options that appear, select your preferred export format. -# FAQ +{% include faq-begin.md %} ## What does it mean if the integration icon for a report is grayed out? If the integration icon for a report appears grayed out, the report has yet to be fully exported. To address this, consider these options: @@ -41,3 +41,4 @@ To address this, consider these options: ## How can I see a specific expense on a report? To locate a specific expense within a report, click on the Report from the Reports page and then click on an expense to view the expense details. +{% include faq-end.md %} 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 1bfa5590efbc..16e628acbeee 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 @@ -148,7 +148,7 @@ Here are some reasons an Expensify Card transaction might be declined: 5. The merchant is located in a restricted country - Some countries may be off-limits for transactions. If a merchant or their headquarters (billing address) are physically located in one of these countries, Expensify Card purchases will be declined. This list may change at any time, so be sure to check back frequently: Belarus, Burundi, Cambodia, Central African Republic, Democratic Republic of the Congo, Cuba, Iran, Iraq, North Korea, Lebanon, Libya, Russia, Somalia, South Sudan, Syrian Arab Republic, Tanzania, Ukraine, Venezuela, Yemen, and Zimbabwe. -# FAQ +{% include faq-begin.md %} ## What happens when I reject an Expensify Card expense? Rejecting an Expensify Card expense from an Expensify report will simply allow it to be reported on a different report. @@ -170,3 +170,5 @@ If a transaction is pending and has a receipt attached (excluding eReceipts), a - Partial refunds: If a transaction is pending, a partial refund will reduce the amount of the transaction. - If a transaction is posted, a partial refund will create a negative transaction for the refund amount. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index b39119ffa4df..73b6c9106e4e 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -168,7 +168,7 @@ If Auto-Reconciliation is disabled for your company's Expensify Cards, a Domain 2. Each time a monthly settlement occurs, Expensify calculates the total purchase amount since the last settlement and creates a Journal Entry. This entry credits the settlement bank account (GL Account) and debits the Expensify Liability Account in Intacct. 3. As expenses are approved and exported to Intacct, Expensify credits the Liability Account and debits the appropriate expense categories. -# FAQ +{% include faq-begin.md %} ## What are the timeframes for auto-reconciliation in Expensify? We offer either daily or monthly auto-reconciliation: @@ -209,3 +209,5 @@ To address this, please follow these steps: 2. Go to the General Ledger (GL) account where your daily Expensify Card settlement withdrawals are recorded, and locate entries for the dates identified in Step 1. 3. Adjust each settlement entry so that it now posts to the Clearing Account. 4. Create a Journal Entry or Receive Money Transaction to clear the balance in the Liability Account using the funds currently held in the Clearing Account, which was set up in Step 2. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md index 3cb05cb136f6..f24ed57dc655 100644 --- a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md @@ -76,9 +76,11 @@ There was suspicious activity - If the spending looks suspicious, we may complete a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens. - The merchant is located in a restricted country -# FAQ +{% include faq-begin.md %} ## Can I use Smart Limits with a free Expensify account? If you're on the Free plan, you won't have the option to use Smart Limits. Your card limit will simply reset at the end of each calendar month. ## I still haven't received my Expensify Card. What should I do? For more information on why your card hasn't arrived, you can check out this resource on [Requesting a Card](https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the-Card#what-if-i-havent-received-my-card-after-multiple-weeks). + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md index 12dad0c7084d..caf540152063 100644 --- a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md +++ b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md @@ -46,7 +46,7 @@ To ensure the dispute process goes smoothly, please: - If you recognize the merchant but not the charge, and you've transacted with them before, contact the merchant directly, as it may be a non-fraudulent error. - Include supporting documentation like receipts or cancellation confirmations when submitting your dispute to enhance the likelihood of a favorable resolution. -# FAQ +{% include faq-begin.md %} ## **How am I protected from fraud using the Expensify Card?** Real-time push notifications alert you of every card charge upfront, helping identify potential issues immediately. Expensify also leverages sophisticated algorithms to detect and/or block unusual card activity. @@ -59,3 +59,4 @@ The dispute process can take a few days to a few months. It depends on the type ## **Can I cancel a dispute?** Contact Concierge if you've filed a dispute and want to cancel it. You might do this if you recognize a previously reported unauthorized charge or if the merchant has already resolved the issue. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md index ca0e7b4709b2..9940535e1fad 100644 --- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -38,7 +38,7 @@ If you need to cancel your Expensify Card and cannot access the website or mobil It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account. -# FAQ +{% include faq-begin.md %} ## What if I haven’t received my card after multiple weeks? @@ -47,3 +47,5 @@ Reach out to support, and we can locate a tracking number for the card. If the c ## I’m self-employed. Can I set up the Expensify Card as an individual? Yep! As long as you have a business bank account and have registered your company with the IRS, you are eligible to use the Expensify Card as an individual business owner. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index e0ef1f3f00fe..464f2129d800 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -46,7 +46,7 @@ If you have a validated domain, you can set a limit for multiple members by sett The Company Cards page will act as a hub to view all employees who have been issued a card and where you can view and edit the individual card limits. You’ll also be able to see anyone who has requested a card but doesn’t have one yet. -# FAQ +{% include faq-begin.md %} ## Are there foreign transaction fees? @@ -65,3 +65,5 @@ The Expensify Card is a free corporate card, and no fees are associated with it. As long as the verified bank account used to apply for the Expensify Card is a US bank account, your cardholders can be anywhere in the world. Otherwise, the Expensify Card is not available for customers using non-US banks. With that said, launching international support is a top priority for us. Let us know if you’re interested in contacting support, and we’ll reach out as soon as the Expensify Card is available outside the United States. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md b/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md index 750a1fc10e77..86dbfe5d0720 100644 --- a/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md +++ b/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md @@ -63,7 +63,7 @@ Using client IDs for Optimized Billing in Expensify: A unique identifier feature - Using client IDs for all Workspaces: It's beneficial to use client IDs for all Workspaces to ensure each one is easily recognizable. - Benefits of itemized billing receipts: Employing client IDs offers itemized billing by client, with each itemization detailing unique active users. -# FAQ +{% include faq-begin.md %} **Do I automatically get the special billing rate as an ExpensifyApproved! Partner?** - Yes, when you join the ExpensifyApproved! program, you will automatically get the special billing rate. To join the ExpensifyApproved! Program, you need to enroll in ExpensifyApproved! University. @@ -85,3 +85,5 @@ Using client IDs for Optimized Billing in Expensify: A unique identifier feature **Where can I see the Billing Receipts?** - All billing owners receive an emailed PDF of their monthly billing receipt, but a CSV version can also be downloaded from the platform. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md b/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md index 1b537839af77..e7a43c1d1d61 100644 --- a/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md @@ -29,7 +29,7 @@ You can include meal deductions or overnight lodging costs if your jurisdiction ### Step 6: Submit for Approval Finally, submit your Per Diem expense for approval, and you'll be on your way to getting reimbursed! -# FAQ +{% include faq-begin.md %} ## Can I edit my per diem expenses? Per Diems cannot be amended. To make changes, delete the expense and recreate it as needed. @@ -43,3 +43,5 @@ Reach out to your internal Admin team, as they've configured the rates in your p ## Can I add start and end times to per diems? Unfortunately, you cannot add start and end times to Per Diems in Expensify. By following these steps, you can efficiently create and manage your Per Diem expenses in Expensify, making the process of tracking and getting reimbursed hassle-free. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/Referral-Program.md b/docs/articles/expensify-classic/get-paid-back/Referral-Program.md index b4a2b4a7de74..4cc646c613a1 100644 --- a/docs/articles/expensify-classic/get-paid-back/Referral-Program.md +++ b/docs/articles/expensify-classic/get-paid-back/Referral-Program.md @@ -25,7 +25,7 @@ The best way to start is to submit any receipt to your manager (you'll get paid Referral rewards for the Spring/Summer 2023 campaign will be paid by direct deposit. -# FAQ +{% include faq-begin.md %} - **How will I know if I am the first person to refer a company to Expensify?** @@ -52,3 +52,5 @@ Please send a message to concierge@expensify.com with the billing owner of the c Expensify members who are opted-in for our newsletters will have received an email containing their unique referral link. On the mobile app, go to **Settings** > **Invite a Friend** > **Share Invite Link** to retrieve your referral link. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/Trips.md b/docs/articles/expensify-classic/get-paid-back/Trips.md index a65a8bfb8eec..ccfbe1592291 100644 --- a/docs/articles/expensify-classic/get-paid-back/Trips.md +++ b/docs/articles/expensify-classic/get-paid-back/Trips.md @@ -28,10 +28,12 @@ To view details about your past or upcoming trips, follow these steps within the 2. Navigate to the "Menu" option (top left ≡ icon) 3. Select **Trips** -# FAQ +{% include faq-begin.md %} ## How do I capture Trip receipts sent to my personal email address? If you received your receipt in an email that is not associated with your Expensify account, you can add this email as a [secondary login](https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#how-to-add-a-secondary-login) to directly forward the receipt into your account. ## How do I upload Trip receipts that were not sent to me by email? If your trip receipt was not sent to you by email, you can manually upload the receipt to your account. Check out this resource for more information on [manually uploading receipts](https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts#manually-upload). + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md b/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md index b5f5ec8be048..c89176bcc0e8 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md @@ -19,7 +19,7 @@ There may be multiple tax rates set up within your Workspace, so if the tax on y If the tax amount on your receipt is different to the calculated amount or the tax rate doesn’t show up, you can always manually type in the correct tax amount. -# FAQ +{% include faq-begin.md %} ## How do I set up multiple taxes (GST/PST/QST) on indirect connections? Expenses sometimes have more than one tax applied to them - for example in Canada, expenses can have both a Federal GST and a provincial PST or QST. @@ -37,3 +37,4 @@ Many tax authorities do not require the reporting of tax amounts by rate and the Alternatively, you can apply each specific tax rate by splitting the expense into the components that each rate will be applied to. To do this, click on **Split Expense** and apply the correct tax rate to each part. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md b/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md index a8444b98c951..a26146536e42 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md @@ -37,7 +37,7 @@ On the mobile app, merging is prompted when you see the message _"Potential dupl If the expenses exist on two different reports, you will be asked which report you'd like the newly created single expense to be reported onto. -# FAQ +{% include faq-begin.md %} ## Can you merge expenses across different reports? diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md b/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md index 29380dab5a5b..b0e3ee1b9ade 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md @@ -19,7 +19,7 @@ To SmartScan a receipt on your mobile app, tap the green camera button, point an ## Manually Upload To upload receipts on the web, simply navigate to the Expenses page and click on **New Expense**. Select **Scan Receipt** and choose the file you would like to upload, or drag-and-drop your image directly into the Expenses page, and that will start the SmartScanning process! -# FAQ +{% include faq-begin.md %} ## How do you SmartScan multiple receipts? You can utilize the Rapid Fire Mode to quickly SmartScan multiple receipts at once! @@ -34,3 +34,5 @@ Once that email address has been added as a Secondary Login, simply forward your You can crop and rotate a receipt image on the web app, and you can only edit one expense at a time. Navigate to your Expenses page and locate the expense whose receipt image you'd like to edit, then click the expense to open the Edit screen. If there is an image file associated with the receipt, you will see the Rotate and Crop buttons. Alternatively, you can also navigate to your Reports page, click on a report, and locate the individual expense. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md b/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md index ea808695e7cd..88ec2b730d1e 100644 --- a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md +++ b/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md @@ -147,7 +147,7 @@ As you go through each violation, click View to look at the expense in more deta Click Next to move on to the next item. Click Finish to complete the review process when you’re done. -# FAQ +{% include faq-begin.md %} ## Is there a difference between Expense Reports, Bills, and Invoices? @@ -164,3 +164,5 @@ If someone external to the business sends you an invoice for their services, you ## When should I submit my report? Your Company Admin can answer this one, and they may have configured the workspace’s [Scheduled Submit] setting to enforce a regular cadence for you. If not, you can still set this up under your [Individual workspace]. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/Invite-Members.md b/docs/articles/expensify-classic/getting-started/Invite-Members.md index 5b3c17c2e8fb..5a27f58cf2e8 100644 --- a/docs/articles/expensify-classic/getting-started/Invite-Members.md +++ b/docs/articles/expensify-classic/getting-started/Invite-Members.md @@ -51,7 +51,7 @@ Here's how it works: If a colleague signs up with a work email address that matc To enable this feature, go to **Settings > Workspace > Group > *Workspace Name* > People**. -# FAQ +{% include faq-begin.md %} ## Who can invite members to Expensify Any Workspace Admin can add members to a Group Workspace using any of the above methods. @@ -60,3 +60,5 @@ Under **Settings > Workspace > Group > *Workspace Name* > People > Invite** you ## How can I invite members via the API? If you would like to integrate an open API HR software, you can use our [Advanced Employee Updater API](https://integrations.expensify.com/Integration-Server/doc/employeeUpdater/) to invite members to your Workspace. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/Plan-Types.md b/docs/articles/expensify-classic/getting-started/Plan-Types.md index 90c632ffa5cc..4f8c52c2e1a1 100644 --- a/docs/articles/expensify-classic/getting-started/Plan-Types.md +++ b/docs/articles/expensify-classic/getting-started/Plan-Types.md @@ -20,7 +20,7 @@ The Track plan is tailored for solo Expensify users who don't require expense su ## Individual Submit Plan The Submit plan is designed for individuals who need to keep track of their expenses and share them with someone else, such as their boss, accountant, or even a housemate. It's specifically tailored for single users who want to both track and submit their expenses efficiently. -# FAQ +{% include faq-begin.md %} ## How can I change Individual plans? You have the flexibility to switch between a Track and Submit plan, or vice versa, at any time by navigating to **Settings > Workspaces > Individual > *Workspace Name* > Plan**. This allows you to adapt your expense management approach as needed. @@ -30,3 +30,5 @@ You can easily upgrade from a Collect to a Control plan at any time by going to ## How does pricing work if I have two types of Group Workspace plans? If you have a Control and Collect Workspace, you will be charged at the Control Workspace rate. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md index a8e1b0690b72..189ff671b213 100644 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md +++ b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md @@ -10,7 +10,7 @@ Start making more with us! We're thrilled to announce a new incentive for our US # How-to To benefit from this program, all you need to do is ensure that you are listed as a domain admin on your client's Expensify account. If you're not currently a domain admin, your client can follow the instructions outlined in [our help article](https://community.expensify.com/discussion/5749/how-to-add-and-remove-domain-admins#:~:text=Domain%20Admins%20have%20total%20control,a%20member%20of%20the%20domain.) to assign you this role. -# FAQ +{% include faq-begin.md %} - What if my firm is not permitted to accept revenue share from our clients?

We understand that different firms may have different policies. If your firm is unable to accept this revenue share, you can pass the revenue share back to your client to give them an additional 0.5% of cash back using your own internal payment tools.

- What if my firm does not wish to participate in the program?
diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md index 104cd49daf96..fb3cb5341f61 100644 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md +++ b/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md @@ -22,7 +22,7 @@ You can contact your Partner Manager by: - Signing in to new.expensify.com and searching for your Partner Manager - Replying to or clicking the chat link on any email you get from your Partner Manager -# FAQs +{% include faq-begin.md %} ## How do I know if my Partner Manager is online? You will be able to see if they are online via their status in new.expensify.com, which will either say “online” or have their working hours. @@ -32,4 +32,6 @@ If you’re unable to contact your Partner Manager (i.e., they're out of office ## Can I get on a call with my Partner Manager? Of course! You can ask your Partner Manager to schedule a call whenever you think one might be helpful. Partner Managers can discuss client onboarding strategies, firm wide training, and client setups. -We recommend continuing to work with Concierge for **general support questions**, as this team is always online and available to help immediately. \ No newline at end of file +We recommend continuing to work with Concierge for **general support questions**, as this team is always online and available to help immediately. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md b/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md index f4a6acdd8571..870edf959b32 100644 --- a/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md +++ b/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md @@ -91,7 +91,7 @@ Your Partner Manager should reach out to you once you've completed ExpensifyAppr - **Be Clear and Specific**: When asking questions or reporting issues, provide specific examples like affected users' email addresses or report IDs. This makes it easier for us to assist you effectively. - **Practice Kindness**: Remember that we're here to help. Please be polite, considerate, and patient as we work together to resolve any concerns you have. -# FAQ +{% include faq-begin.md %} ## Who gets an Account Manager? Members who have 10 or more active users, or clients of ExpensifyApproved! Accounts are automatically assigned a dedicated Account Manager. @@ -115,3 +115,5 @@ We recommend working with Concierge on general support questions, as this team i ## Who gets assigned a Setup Specialist? This feature is specifically for new members! Whenever you start a free trial, a product Setup Specialist will be assigned to guide you through configuring your Expensify account. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md index f6043aaea2eb..b89dca85df04 100644 --- a/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md @@ -20,7 +20,7 @@ Below is a breakdown of the available default templates. 3. Click the **Export to** in the top right corner 4. Select the export template you’d like to use -# FAQ +{% include faq-begin.md %} ## Why are my numbers exporting in a weird format? Do your numbers look something like this: 1.7976931348623157e+308? This means that your spreadsheet program is formatting long numbers in an exponential or scientific format. If that happens, you can correct it by changing the data to Plain Text or a Number in your spreadsheet program. ## Why are my leading zeros missing? @@ -28,3 +28,4 @@ Is the export showing “1” instead of “01”? This means that your spreadsh ## I want a report that is not in the default list, how can I build that? For a guide on building your own custom template check out Exports > Custom Exports in the Help pages! +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md index 6c71630015c5..ce07f4b56450 100644 --- a/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md @@ -35,7 +35,7 @@ The Insights dashboard allows you to monitor all aspects of company spend across 2. Build up a report using these [formulas](https://community.expensify.com/discussion/5795/deep-dive-expense-level-formula/p1?new=1) 3. If you need any help, click the **Support** button on the top left to contact your Account Manager -# FAQs +{% include faq-begin.md %} #### Can I share my custom export report? @@ -98,4 +98,6 @@ We’ve built a huge variety of custom reports for customers, so make sure to re - Unposted Travel Aging Report - Vendor Spend - … or anything you can imagine! -{% endraw %} \ No newline at end of file +{% endraw %} + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md index 7ba84cef6b94..9d752dec3eb9 100644 --- a/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md @@ -30,10 +30,12 @@ The PDF will include all expenses, any attached receipts, and all report notes. 3. Click on **Details** in the top right of the report 4. Click the **print icon** -# FAQ +{% include faq-begin.md %} ## Why isn’t my report exporting? Big reports with lots of expenses may cause the PDF download to fail due to images with large resolutions. In that case, try breaking the report into multiple smaller reports. Also, please note that a report must have at least one expense to be exported or saved as a PDF. ## Can I download multiple PDFs at once? No, you can’t download multiple reports as PDFs at the same time. If you’d like to export multiple reports, an alternative to consider is the CSV export option. ## The data exported to Excel is showing incorrectly. How can I fix this? When opening a CSV file export from Expensify in Excel, it’ll automatically register report IDs and transaction IDs as numbers and assign the number format to the report ID column. If a number is greater than a certain length, Excel will contract the number and display it in exponential form. To prevent this, the number needs to be imported as text, which can be done by opening Excel and clicking File > Import > select your CSV file > follow the prompts and on step 3 set the report ID/transactionID column to import as Text. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md b/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md index 65b276796c2a..47cbd2fdc1f3 100644 --- a/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md +++ b/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md @@ -70,7 +70,7 @@ You can set Custom Fields and Payroll Codes in bulk using a CSV upload in Expens If you have additional requirements for your ADP upload, for example, additional headings or datasets, reach out to your Expensify Account Manager who will assist you in customizing your ADP export. Expensify Account Managers are trained to accommodate your data requests and help you retrieve them from the system. -# FAQ +{% include faq-begin.md %} - Do I need to convert my employee list into new column headings so I can upload it to Expensify? @@ -79,3 +79,5 @@ Yes, you’ll need to convert your ADP employee data to the same headings as the - Can I add special fields/items to my ADP Payroll Custom Export Format? Yes! You can ask your Expensify Account Manager to help you prepare your ADP Payroll export so that it meets your specific requirements. Just reach out to them via the Chat option in Expensify and they’ll help you get set up. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md b/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md index f7a5127c9c0e..33a174325bf7 100644 --- a/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md +++ b/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md @@ -34,7 +34,7 @@ Expensify's direct integration with Gusto will automatically: 2. Click **Save** in the bottom right corner to sync employees into Expensify 3. If the connection is successful, you'll see a summary of how many employees were synced. If any employees were skipped, we'll tell you why. -# FAQ +{% include faq-begin.md %} ## Can I import different sets of employees into different Expensify workspaces? No - Gusto will add all employees to one Expensify workspace, so if you have more than one workspace, you'll need to choose when connecting. @@ -53,3 +53,5 @@ If your employees are set up in Expensify with their company emails, but with th To resolve this, you can ask each affected employee to merge their existing Expensify account with the new Expensify account by navigating to **Settings > Account > Account Details** and scrolling down to **Merge Accounts**. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md index 0856e2694340..6c7014827ea6 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md @@ -87,7 +87,7 @@ When exporting to Certinia PSA/SRP you may see up to three different currencies * Amount field on the Expense line: this currency is derived from the Expensify workspace default report currency. * Reimbursable Amount on the Expense line: this currency is derived from the currency of the resource with an email matching the report submitter. -# FAQ +{% include faq-begin.md %} ## What happens if the report can’t be exported to Certinia? * The preferred exporter will receive an email outlining the issue and any specific error messages * Any error messages preventing the export from taking place will be recorded in the report’s history @@ -148,3 +148,5 @@ Log into Certinia and go to Setup > Manage Users > Users and find the user whose * Enable Modify All Data and save Sync the connection within Expensify by going to **Settings** > **Workspaces** > **Groups** > _[Workspace Name]_ > **Connections** > **Sync Now** and then attempt to export the report again + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md index 852db0b7f7c0..09fad1b0ed1a 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md @@ -30,7 +30,7 @@ To export a report, click **Export To** in the top-left of a report and select y To export multiple reports, tick the checkbox next to the reports on the **Reports** page, then click **Export To** and select your accounting package from the dropdown menu. -# FAQ +{% include faq-begin.md %} ## Which accounting packages offer this indirect integration with Expensify? @@ -46,3 +46,5 @@ We support a pre-configured flat-file integration for the following accounting p If your accounting package isn’t listed, but it still accepts a flat-file import, select **Other** when completing the Accounting Software task on your Home page or head to **Settings** > **Workspaces** > **Group** > _Your desired workspace_ > **Export Formats**. This option allows you to create your own templates to export your expense and report data into a format compatible with your accounting system. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md index 8092ed9c6dd6..3ce0d07cb65d 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md @@ -558,7 +558,7 @@ Here's how you can send them to us: Send these two files to your Account Manager or Concierge so we can continue troubleshooting! -# FAQ +{% include faq-begin.md %} ## What type of Expensify plan is required for connecting to NetSuite? @@ -573,3 +573,5 @@ If a report is exported to NetSuite and then marked as paid in NetSuite, the rep ## If I enable Auto Sync, what happens to existing approved and reimbursed reports? If you previously had Auto Sync disabled but want to allow that feature to be used going forward, you can safely turn on Auto Sync without affecting existing reports. Auto Sync will only take effect for reports created after enabling that feature. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md index 958e423273ce..8fe31f3ec4f4 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md @@ -88,7 +88,7 @@ You can bring in Customers/Projects from QuickBooks into Expensify in two ways: ## Items Items can be imported from QuickBooks as categories alongside your expense accounts. -# FAQ +{% include faq-begin.md %} ## How do I sync my connection? 1: Ensure that both the Expensify Sync Manager and QuickBooks Desktop are running. 2: On the Expensify website, navigate to **Settings** > **Policies** > **Group** > _[Policy Name]_ > **Connections** > **QuickBooks Desktop**, and click **Sync now**. @@ -143,3 +143,5 @@ To resolve this error, follow these steps: Verify that the Sync Manager's status is **Connected**. 3. If the Sync Manager status is already **Connected**, click **Edit** and then *Save* to refresh the connection. Afterwards, try syncing your policy again. 4. If the error persists, double-check that the token you see in the Sync Manager matches the token in your connection settings. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md index 4075aaf18016..623e5f1dd997 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md @@ -302,7 +302,7 @@ Here are the QuickBooks Online fields that can be mapped as a report field withi - Customers/Projects - Locations -# FAQ +{% include faq-begin.md %} ## What happens if the report can't be exported to QuickBooks Online automatically? @@ -320,3 +320,5 @@ To ensure reports are reviewed before export, set up your Workspaces with the ap - If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in QuickBooks Online during the next sync. - If a report has been exported and marked as paid in QuickBooks Online, it will be automatically marked as reimbursed in Expensify during the next sync. - Reports that have yet to be exported to QuickBooks Online won't be automatically exported. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md index ac0a90ba6d37..560a65d0d722 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md @@ -550,7 +550,7 @@ When ACH reimbursement is enabled, the "Sync Reimbursed Reports" feature will ad Intacct requires that the target account for the Bill Payment be a Cash and Cash Equivalents account type. If you aren't seeing the account you want in that list, please first confirm that the category on the account is Cash and Cash Equivalents. -# FAQ +{% include faq-begin.md %} ## What if my report isn't automatically exported to Sage Intacct? There are a number of factors that can cause automatic export to fail. If this happens, the preferred exporter will receive an email and an Inbox task outlining the issue and any associated error messages. The same information will be populated in the comments section of the report. @@ -566,3 +566,5 @@ If your workspace has been connected to Intacct with Auto Sync disabled, you can If a report has been exported to Intacct and reimbursed via ACH in Expensify, we'll automatically mark it as paid in Intacct during the next sync. If a report has been exported to Intacct and marked as paid in Intacct, we'll automatically mark it as reimbursed in Expensify during the next sync. If a report has not been exported to Intacct, it will not be exported to Intacct automatically. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md index 98cc6f2bfdf6..9dd479e90cf1 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md @@ -236,7 +236,7 @@ If we can't find a match, we'll create a new customer record in Xero. And that's it! You've successfully set up and managed your invoice exports to Xero, making your tracking smooth and efficient. -# FAQ +{% include faq-begin.md %} ## Will receipt images be exported to Xero? @@ -258,3 +258,5 @@ It will be automatically marked as reimbursed in Expensify during the next sync. Reports that haven't been exported to Xero won't be sent automatically. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md b/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md index ac37a01b3e6b..7dcc8e5e9c29 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md @@ -52,7 +52,7 @@ You can automatically import receipts from many travel platforms into Expensify, - From your account settings, choose whether expenses should be sent to Expensify automatically or manually. - We recommend sending them automatically, so you can travel without even thinking about your expense reports. -# FAQ +{% include faq-begin.md %} **Q: What if I don’t have the option for Send to Expensify in Trainline?** @@ -69,3 +69,5 @@ A: Yes, you can set a weekly or monthly cadence for SpotHero expenses to be emai **Q: Do I need to select a specific profile before booking in Bolt Work and Grab?** A: Yes, ensure you have selected your work or business profile as the payment method before booking. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md b/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md index 237047fa270e..b1bf3c9745ff 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md @@ -20,7 +20,7 @@ Once you complete these steps, any flights you book through Navan will automatic If you booked your Navan flight using your Expensify Card, the Navan expense will automatically merge with the card expense. Learn more about the Expensify Card [here](https://use.expensify.com/company-credit-card). -# FAQ +{% include faq-begin.md %} ## How do I expense a prepaid hotel booking in Expensify using the Navan integration? Bookings that weren’t made in Navan directly (such as a prepaid hotel booking) won’t auto-import into Expensify. To import these trips into Expensify, follow these steps: @@ -45,3 +45,5 @@ Costs depend on your subscription plans with Expensify and Navan. Expensify does ## How do I disconnect the integration? To disconnect the integration, navigate to the integrations section in Navan, find Expensify, and select the option to disable the integration. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md index 33ffe7172603..65acc3630582 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md @@ -13,7 +13,7 @@ Removing a member from a workspace disables their ability to use the workspace. ![image of members table in a workspace]({{site.url}}/assets/images/ExpensifyHelp_RemovingMembers.png){:width="100%"} -# FAQ +{% include faq-begin.md %} ## Will reports from this member on this workspace still be available? Yes, as long as the reports have been submitted. You can navigate to the Reports page and enter the member's email in the search field to find them. However, Draft reports will be removed from the workspace, so these will no longer be visible to the Workspace Admin. @@ -34,3 +34,5 @@ If a member is a **preferred exporter, billing owner, report approver** or has * ## How do I remove a user completely from a company account? If you have a Control Workspace and have Domain Control enabled, you will need to remove them from the domain to delete members' accounts entirely and deactivate the Expensify Card. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md index 7c21b12a83e1..4727b1c4a38b 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md @@ -38,7 +38,7 @@ Your delegate's actions will be noted in the history and comments of each report The system records every action your vacation delegate takes on your behalf in the **Report History and Comments**. So, you can see when they approved an expense report for you. -# FAQs +{% include faq-begin.md %} ## Why can't my Vacation Delegate reimburse reports that they approve? @@ -50,5 +50,4 @@ If they do not have access to the reimbursement account used on your workspace, Don't worry, your delegate can also pick their own **Vacation Delegate**. This way, expense reports continue to get approved even if multiple people are away. - - +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/send-payments/Pay-Bills.md b/docs/articles/expensify-classic/send-payments/Pay-Bills.md index 8a5c7c5c7f88..81dcf3488462 100644 --- a/docs/articles/expensify-classic/send-payments/Pay-Bills.md +++ b/docs/articles/expensify-classic/send-payments/Pay-Bills.md @@ -70,7 +70,17 @@ To mark a Bill as paid outside of Expensify: **Fees:** None -# FAQ +# Deep Dive: How company bills and vendor invoices are processed in Expensify + +Here is how a vendor or supplier bill goes from received to paid in Expensify: + +1. When a vendor or supplier bill is received in Expensify via, the document is SmartScanned automatically and a Bill is created. The Bill is owned by the primary domain contact, who will see the Bill on the Reports page on their default group policy. +2. When the Bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow. Each time the Bill is approved, it is visible in the next approver's Inbox. +3. The final approver pays the Bill from their Expensify account on the web via one of the methods. +4. The Bill is coded with the relevant imported GL codes from a connected accounting software. After it has finished going through the approval workflow the Bill can be exported back to the accounting package connected to the policy. + + +{% include faq-begin.md %} ## What is my company's billing intake email? Your billing intake email is [yourdomain.com]@expensify.cash. Example, if your domain is `company.io` your billing email is `company.io@expensify.cash`. @@ -100,11 +110,4 @@ Payments are currently only supported for users paying in United States Dollars A Bill is a payable which represents an amount owed to a payee (usually a vendor or supplier), and is usually created from a vendor invoice. An Invoice is a receivable, and indicates an amount owed to you by someone else. -# Deep Dive: How company bills and vendor invoices are processed in Expensify - -Here is how a vendor or supplier bill goes from received to paid in Expensify: - -1. When a vendor or supplier bill is received in Expensify via, the document is SmartScanned automatically and a Bill is created. The Bill is owned by the primary domain contact, who will see the Bill on the Reports page on their default group policy. -2. When the Bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow. Each time the Bill is approved, it is visible in the next approver's Inbox. -3. The final approver pays the Bill from their Expensify account on the web via one of the methods. -4. The Bill is coded with the relevant imported GL codes from a connected accounting software. After it has finished going through the approval workflow the Bill can be exported back to the accounting package connected to the policy. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md b/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md index e55d99d70827..69b39bae2874 100644 --- a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md +++ b/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md @@ -19,6 +19,8 @@ To reimburse directly in Expensify, the following needs to be already configured If all of those settings are in place, to reimburse a report, you will click **Reimburse** on the report and then select **Via Direct Deposit (ACH)**. +![Reimbursing Reports Dropdown]({{site.url}}/assets/images/Reimbursing Reports Dropdown.png){:width="100%"} + ## Indirect or Manual Reimbursement If you don't have the option to utilize direct reimbursement, you can choose to mark a report as reimbursed by clicking the **Reimburse** button at the top of the report and then selecting **I’ll do it manually – just mark as reimbursed**. @@ -63,7 +65,7 @@ If either limit has been reached, you can expect funds deposited within your ban Rapid Reimbursement is not available for non-US-based reimbursement. If you are receiving a reimbursement to a non-US-based deposit account, you should expect to see the funds deposited in your bank account within four business days. -# FAQ +{% include faq-begin.md %} ## Who can reimburse reports? Only a workspace admin who has added a verified business bank account to their Expensify account can reimburse employees. @@ -73,3 +75,7 @@ Only a workspace admin who has added a verified business bank account to their E Instead of a bulk reimbursement option, you can set up automatic reimbursement. With this configured, reports below a certain threshold (defined by you) will be automatically reimbursed via ACH as soon as they're "final approved." To set your manual reimbursement threshold, head to **Settings > Workspace > Group > _[Workspace Name]_ > Reimbursement > Manual Reimbursement**. + +![Manual Reimbursement]({{site.url}}/assets/images/Reimbursing Manual.png){:width="100%"} + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md b/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md index 1a567dbe6fa3..cae289a0526a 100644 --- a/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md +++ b/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md @@ -42,7 +42,7 @@ Once you've set up your third party payment option, you can start using it to re 4. **Track Payment Status**: You can track the status of payments and view transaction details within your Expensify account. -# FAQ’s +{% include faq-begin.md %} ## Q: Are there any fees associated with using third party payment options in Expensify? @@ -57,3 +57,5 @@ A: Expensify allows you to link multiple payment providers if needed. You can se A: The reimbursement limit may depend on the policies and settings configured within your Expensify account and the limits imposed by your chosen payment provider. With Expensify's third party payment options, you can simplify your expense management and reimbursement processes. By following the steps outlined in this guide, you can set up and use third party payments efficiently. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md index 3c5bc0fe2421..30adac589dc0 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md @@ -42,7 +42,7 @@ Expensify’s Budgets feature allows you to: - **Per individual budget**: you can enter an amount if you want to set a budget per person - **Notification threshold** - this is the % in which you will be notified as the budgets are hit -# FAQ +{% include faq-begin.md %} ## Can I import budgets as a CSV? At this time, you cannot import budgets via CSV since we don’t import categories or tags from direct accounting integrations. @@ -54,3 +54,4 @@ Notifications are sent twice: ## How will I be notified when a budget is hit? A message will be sent in the #admins room of the Workspace. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md index 783bd50f17a3..0cd7ba790a9c 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md @@ -143,7 +143,7 @@ Category violations can happen for the following reasons: If Scheduled Submit is enabled on a workspace, expenses with category violations will not be auto-submitted unless the expense has a comment added. -# FAQ +{% include faq-begin.md %} ## The correct category list isn't showing when one of my employees is categorizing their expenses. Why is this happening? Its possible the employee is defaulted to their personal workspace so the expenses are not pulling the correct categories to choose from. Check to be sure the report is listed under the correct workspace by looking under the details section on top right of report. @@ -151,3 +151,4 @@ Its possible the employee is defaulted to their personal workspace so the expens ## Will the account numbers from our accounting system (QuickBooks Online, Sage Intacct, etc.) show in the Category list when employees are choosing what chart of accounts category to code their expense to? The GL account numbers will be visible in the workspace settings when connected to a Control-level workspace for workspace admins to see. We do not provide this information in an employee-facing capacity because most employees do not have access to that information within the accounting integration. If you wish to have this information available to your employees when they are categorizing their expenses, you can edit the account name in your accounting software to include the GL number — i.e. **Accounts Payable - 12345** +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md index cf2f0f59a4a0..6cafe3dccfaf 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md @@ -131,10 +131,11 @@ To enable SAML SSO in Expensify you will first need to claim and verify your dom - For disputing digital Expensify Card purchases, two-factor authentication must be enabled. - It might take up to 2 hours for domain-level enforcement to take effect, and users will be prompted to configure their individual 2FA settings on their next login to Expensify. -# FAQ +{% include faq-begin.md %} ## How many domains can I have? You can manage multiple domains by adding them through **Settings > Domains > New Domain**. However, to verify additional domains, you must be a Workspace Admin on a Control Workspace. Keep in mind that the Collect plan allows verification for just one domain. ## What’s the difference between claiming a domain and verifying a domain? Claiming a domain is limited to users with matching email domains, and allows Workspace Admins with a company email to manage bills, company cards, and reconciliation. Verifying a domain offers extra features and security. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md index 388bb5d5cbc9..ea701dc09d3e 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md @@ -106,7 +106,7 @@ If you enable tax but don’t select a tax rate or enter a tax reclaimable amoun Note: _Expensify won’t automatically track cumulative mileage. If you need to track cumulative mileage per employee, we recommend building a mileage report using our custom export formulas._ -# FAQs +{% include faq-begin.md %} ## Why do I see eReceipts for expenses greater than $75? @@ -116,3 +116,4 @@ An eReceipt is generated for Expensify card purchases of any amount in the follo Expensify does not update mileage rates to match the rate provided by the IRS. An admin of the workspace will need to update the rate or create a new rate in the workspace. This is because Expensify has customers worldwide, not just in the United States, and most companies want to communicate the change with employees and control the timing. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md index fcb1c8018613..87aef233aeb1 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md @@ -86,7 +86,7 @@ When you _Export to CSV_, Expensify also assigns a Rate ID to each existing rate Note: _This rate ID corresponds to the Destination+Subrate. You cannot overwrite Destinations, but you can overwrite the Subrate within a Destination by using this rate ID. Always use the “Clear Rate” option with a fresh upload when removing large numbers of rates rather than deleting them individually._ -# FAQs +{% include faq-begin.md %} ## How do I report on my team's Per Diem expenses? @@ -95,3 +95,4 @@ Great question! We’ve added a Per Diem export for users to export Per Diem exp ## What if I need help setting the exact rate amounts and currencies? Right now, Expensify can't help determine what these should be. They vary widely based on your country of origin, the state within that jurisdiction, your company workspace, and the time (usually year) you traveled. There's a demonstration spreadsheet [here](https://s3-us-west-1.amazonaws.com/concierge-responses-expensify-com/uploads%2F1596692482998-Germany+-+Per+Diem.csv), but it shouldn't be used for actual claims unless verified by your internal finance team or accountants. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md index a1916465fca8..ed2384d12006 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md @@ -32,7 +32,7 @@ A Workspace admin can enanble indirect reimbursement via **Settings > Workspaces **Additional features under Reimbursement > Indirect:** If you reimburse through a seperate system or through payroll, Expensify can collect and export employee bank account details for you. Just reach out to your Account Manager or concierge@expensify.com for us to add the Reimbursement Details Export format to the account. -# FAQ +{% include faq-begin.md %} ## How do I export employee bank account details once the Reimbursement Details Export format is added to my account? @@ -45,3 +45,4 @@ Bank account names can be updated via **Settings > Accounts > Payments** and cli ## What is the benefit of setting a default reimburser? The main benefit of being defined as the "reimburser" in the Workspace settings is that this user will receive notifications on their Home page alerting them when reports need to be reimbursed. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md b/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md index 758cb70067e1..e4b27b238e46 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md @@ -77,7 +77,7 @@ To enable SSO with Microsoft ADFS follow these steps: Assuming you’ve also set up Expensify SAML configuration with your metadata, SAML logins on Expensify.com should now work. For reference, ADFS’ default metadata path is: https://yourservicename.yourdomainname.com/FederationMetadata/2007-06/FederationMetadata.xml. -# FAQ +{% include faq-begin.md %} ## What should I do if I’m getting an error when trying to set up SSO? You can double check your configuration data for errors using samltool.com. If you’re still having issues, you can reach out to your Account Manager or contact Concierge for assistance. @@ -87,3 +87,4 @@ The entityID for Expensify is https://expensify.com. Remember not to copy and pa ## Can you have multiple domains with only one entityID? Yes. Please send a message to Concierge or your account manager and we will enable the ability to use the same entityID with multiple domains. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md index 2e6bd335ce4c..d802a183c8ba 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md @@ -78,7 +78,7 @@ Alternatively, if you update the tag details in your accounting integration, be # Deep Dive ## Make tags required You can require tags for any workspace expenses by enabling People must tag expenses on the Tags page by navigating to Settings > Workspace > Group > [Workspace Name] > Tags. -# FAQ +{% include faq-begin.md %} ## What are the different tag options? If you want your second tag to depend on the first one, use dependent tags. Include GL codes if needed, especially when using accounting integrations. @@ -91,4 +91,4 @@ Multi-level tagging is only available with the Control type policy. ## I can’t see "Do you want to use multiple level tags" feature on my company's expense workspace. Why is that? If you are connected to an accounting integration, you will not see this feature. You will need to add those tags in your integration first, then sync the connection. - +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md b/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md index de66315f2d79..307641c9c605 100644 --- a/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md +++ b/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md @@ -114,7 +114,7 @@ If you get a generic error message that indicates, "Something's gone wrong", ple 8. If you have another phone available, try to follow these steps on that device If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. -# FAQ +{% include faq-begin.md %} ## What is a Beneficial Owner? A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. @@ -140,3 +140,4 @@ It's a good idea to wait till the end of that second business day. If you still Make sure to reach out to your Account Manager or Concierge once that's all set, and our team will be able to re-trigger those three test transactions! +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md b/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md index 3b79072aa393..b036c5b087d2 100644 --- a/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md +++ b/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md @@ -60,7 +60,7 @@ Request an edit an expense or remove an expense before you pay, you can let your - Automatic submission is already set up, so your admin can pay you back immediately once you create an expense. - Your admin will get a notification when you send them a new expense, but you can also remind them to pay you by making a comment in the Report History section of your Processing report or chatting with them on new.expensify.com. -# FAQs +{% include faq-begin.md %} ## Do I need a business bank account to use the Free Plan? @@ -145,3 +145,5 @@ Depending on how quickly you report it to us, we may be able to help cancel a re ## As an admin, can I edit users’ expenses and delete them from reports? No. Only users can edit and delete expenses on the Free plan. Admin control of submitted expenses on a workspace is a feature of our paid plans. If you need something changed, let the user know by commenting in the Report History section of the report on www.expensify.com or by chatting with them in new.expensify.com. + +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md index 25ccdefad261..c7ae49e02292 100644 --- a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md +++ b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md @@ -136,7 +136,7 @@ You will receive a whisper from Concierge any time your content has been flagged *Note: Any message sent in public chat rooms are automatically reviewed by an automated system looking for offensive content and sent to our moderators for final decisions if it is found.* -# FAQs +{% include faq-begin.md %} ## What are the #announce and #admins rooms? @@ -162,3 +162,4 @@ The way your chats display in the left-hand menu is customizable. We offer two d - #focus mode will display only unread and pinned chats, and will sort them alphabetically. This setting is perfect for when you need to cut distractions and focus on a crucial project. You can find your display mode by clicking on your Profile > Preferences > Priority Mode. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/payments/Distance-Requests.md b/docs/articles/new-expensify/payments/Distance-Requests.md index 91b88409be8b..899cb48fd1f5 100644 --- a/docs/articles/new-expensify/payments/Distance-Requests.md +++ b/docs/articles/new-expensify/payments/Distance-Requests.md @@ -20,8 +20,9 @@ Expensify allows you to request reimbursement for mileage by creating a distance -# FAQs +{% include faq-begin.md %} ## Is there an easy way to reuse recent locations? Yes! We save your recently used locations and list them out on the page where you select the Start and Finish. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/payments/Referral-Program.md b/docs/articles/new-expensify/payments/Referral-Program.md index 6ffb923aeb76..a1b1043dff47 100644 --- a/docs/articles/new-expensify/payments/Referral-Program.md +++ b/docs/articles/new-expensify/payments/Referral-Program.md @@ -31,7 +31,7 @@ The sky's the limit for this referral program! Your referral can be anyone - a f For now, referral rewards will be paid via direct deposit into bank accounts that are connected to Expensify. -# FAQ +{% include faq-begin.md %} - **How will I know if I'm the first person to refer a company to Expensify?** @@ -54,3 +54,4 @@ Expensify reserves the right to modify the terms of the referral program at any - **Where can I find my referral link?** In New Expensify, go to **Settings** > **Share code** > **Get $250** to retrieve your invite link. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/payments/Request-Money.md b/docs/articles/new-expensify/payments/Request-Money.md index 43a72a075de7..9aac4787484c 100644 --- a/docs/articles/new-expensify/payments/Request-Money.md +++ b/docs/articles/new-expensify/payments/Request-Money.md @@ -31,6 +31,7 @@ These two features ensure you can live in the moment and settle up afterward. - Enter a reason for the split - The split is then shared equally between the attendees -# FAQs +{% include faq-begin.md %} ## Can I request money from more than one person at a time? If you need to request money for more than one person at a time, you’ll want to use the Split Bill feature. The Request Money option is for one-to-one payments between two people. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md b/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md index cf2f0f59a4a0..40d759479390 100644 --- a/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md +++ b/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md @@ -131,10 +131,12 @@ To enable SAML SSO in Expensify you will first need to claim and verify your dom - For disputing digital Expensify Card purchases, two-factor authentication must be enabled. - It might take up to 2 hours for domain-level enforcement to take effect, and users will be prompted to configure their individual 2FA settings on their next login to Expensify. -# FAQ +{% include faq-begin.md %} ## How many domains can I have? You can manage multiple domains by adding them through **Settings > Domains > New Domain**. However, to verify additional domains, you must be a Workspace Admin on a Control Workspace. Keep in mind that the Collect plan allows verification for just one domain. ## What’s the difference between claiming a domain and verifying a domain? Claiming a domain is limited to users with matching email domains, and allows Workspace Admins with a company email to manage bills, company cards, and reconciliation. Verifying a domain offers extra features and security. + +{% include faq-end.md %} diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f267261a49c0..ff0d5c910e6e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.21 + 1.4.23 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.21.1 + 1.4.23.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f95a3f871d4c..80cb37367088 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.21 + 1.4.23 CFBundleSignature ???? CFBundleVersion - 1.4.21.1 + 1.4.23.0 diff --git a/package-lock.json b/package-lock.json index c8b4b2ba2082..11a5ee7f2e56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.21-1", + "version": "1.4.23-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.21-1", + "version": "1.4.23-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -99,7 +99,7 @@ "react-native-pdf": "^6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -201,6 +201,7 @@ "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-expensify": "^2.0.43", "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", @@ -21202,8 +21203,7 @@ "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/keyv": { "version": "3.1.4", @@ -23403,15 +23403,15 @@ "license": "MIT" }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -23448,15 +23448,34 @@ "node": ">=0.10.0" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -23467,14 +23486,14 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -30582,12 +30601,14 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -30595,15 +30616,15 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-module-utils": { - "version": "2.7.4", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -30621,7 +30642,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -30719,23 +30739,28 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.26.0", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, - "license": "MIT", "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" @@ -30745,11 +30770,12 @@ } }, "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { @@ -30765,12 +30791,14 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/eslint-plugin-import/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, - "license": "MIT" + "bin": { + "semver": "bin/semver.js" + } }, "node_modules/eslint-plugin-jest": { "version": "24.7.0", @@ -33493,10 +33521,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "license": "MIT" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -34214,6 +34244,17 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hast-to-hyperscript": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", @@ -35900,11 +35941,11 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -44759,14 +44800,14 @@ } }, "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -44792,6 +44833,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "node_modules/object.hasown": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", @@ -44819,14 +44872,14 @@ } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -47631,8 +47684,8 @@ }, "node_modules/react-native-picker-select": { "version": "8.1.0", - "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", - "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "integrity": "sha512-NpXXyK+UuANYOysjUb9pCoq9SookRYPfpOcM4shxOD4+2Fkh7TYt2LBUpAdBicMHmtaR43RWXVQk9pMimOhg2w==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0" @@ -53277,12 +53330,13 @@ "license": "Apache-2.0" }, "node_modules/tsconfig-paths": { - "version": "3.14.1", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } @@ -53304,7 +53358,6 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -73074,15 +73127,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" } }, @@ -73101,27 +73154,40 @@ "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", "devOptional": true }, + "array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, "array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" } }, "array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" } }, @@ -78429,11 +78495,14 @@ "requires": {} }, "eslint-import-resolver-node": { - "version": "0.3.6", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "requires": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" }, "dependencies": { "debug": { @@ -78448,7 +78517,9 @@ } }, "eslint-module-utils": { - "version": "2.7.4", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "requires": { "debug": "^3.2.7" @@ -78520,29 +78591,37 @@ } }, "eslint-plugin-import": { - "version": "2.26.0", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" }, "dependencies": { "debug": { - "version": "2.6.9", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "doctrine": { @@ -78554,10 +78633,10 @@ "esutils": "^2.0.2" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -80424,9 +80503,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -80914,6 +80993,14 @@ "minimalistic-assert": "^1.0.1" } }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "requires": { + "function-bind": "^1.1.2" + } + }, "hast-to-hyperscript": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", @@ -82096,11 +82183,11 @@ } }, "is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "requires": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "is-data-descriptor": { @@ -88377,14 +88464,14 @@ } }, "object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "object.getownpropertydescriptors": { @@ -88397,6 +88484,18 @@ "es-abstract": "^1.20.1" } }, + "object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "object.hasown": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", @@ -88417,14 +88516,14 @@ } }, "object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "objectorarray": { @@ -90460,9 +90559,9 @@ "requires": {} }, "react-native-picker-select": { - "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", - "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==", - "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "integrity": "sha512-NpXXyK+UuANYOysjUb9pCoq9SookRYPfpOcM4shxOD4+2Fkh7TYt2LBUpAdBicMHmtaR43RWXVQk9pMimOhg2w==", + "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", "requires": { "lodash.isequal": "^4.5.0" } @@ -94475,11 +94574,13 @@ "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==" }, "tsconfig-paths": { - "version": "3.14.1", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "requires": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" }, diff --git a/package.json b/package.json index bd4b8ffacedf..d7bc53713f21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.21-1", + "version": "1.4.23-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -147,7 +147,7 @@ "react-native-pdf": "^6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -249,6 +249,7 @@ "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-expensify": "^2.0.43", "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", diff --git a/patches/react-native+0.72.4+005+fix-boost-dependency.patch b/patches/react-native+0.72.4+005+fix-boost-dependency.patch new file mode 100644 index 000000000000..477cf97b4a02 --- /dev/null +++ b/patches/react-native+0.72.4+005+fix-boost-dependency.patch @@ -0,0 +1,27 @@ +diff --git a/node_modules/react-native/ReactAndroid/build.gradle b/node_modules/react-native/ReactAndroid/build.gradle +index f44b6e4..818833b 100644 +--- a/node_modules/react-native/ReactAndroid/build.gradle ++++ b/node_modules/react-native/ReactAndroid/build.gradle +@@ -243,7 +243,8 @@ task createNativeDepsDirectories { + } + + task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) { +- src("https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION.replace("_", ".")}/source/boost_${BOOST_VERSION}.tar.gz") ++ def transformedVersion = BOOST_VERSION.replace("_", ".") ++ src("https://archives.boost.io/release/${transformedVersion}/source/boost_${BOOST_VERSION}.tar.gz") + onlyIfModified(true) + overwrite(false) + retries(5) +diff --git a/node_modules/react-native/third-party-podspecs/boost.podspec b/node_modules/react-native/third-party-podspecs/boost.podspec +index 3d9331c..bbbb738 100644 +--- a/node_modules/react-native/third-party-podspecs/boost.podspec ++++ b/node_modules/react-native/third-party-podspecs/boost.podspec +@@ -10,7 +10,7 @@ Pod::Spec.new do |spec| + spec.homepage = 'http://www.boost.org' + spec.summary = 'Boost provides free peer-reviewed portable C++ source libraries.' + spec.authors = 'Rene Rivera' +- spec.source = { :http => 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', ++ spec.source = { :http => 'https://archives.boost.io/release/1.76.0/source/boost_1_76_0.tar.bz2', + :sha256 => 'f0397ba6e982c4450f27bf32a2a83292aba035b827a5623a14636ea583318c41' } + + # Pinning to the same version as React.podspec. diff --git a/patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch new file mode 100644 index 000000000000..9a98cb7af85f --- /dev/null +++ b/patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native-reanimated/android/build.gradle b/node_modules/react-native-reanimated/android/build.gradle +index 3de90e5..42d9d1a 100644 +--- a/node_modules/react-native-reanimated/android/build.gradle ++++ b/node_modules/react-native-reanimated/android/build.gradle +@@ -567,7 +567,7 @@ if (REACT_NATIVE_MINOR_VERSION < 71) { + task downloadBoost(dependsOn: resolveBoost, type: Download) { + def transformedVersion = BOOST_VERSION.replace("_", ".") + def artifactLocalName = "boost_${BOOST_VERSION}.tar.gz" +- def srcUrl = "https://boostorg.jfrog.io/artifactory/main/release/${transformedVersion}/source/${artifactLocalName}" ++ def srcUrl = "https://archives.boost.io/release/${transformedVersion}/source/${artifactLocalName}" + if (REACT_NATIVE_MINOR_VERSION < 69) { + srcUrl = "https://github.com/react-native-community/boost-for-react-native/releases/download/v${transformedVersion}-0/${artifactLocalName}" + } diff --git a/patches/react-native-vision-camera+2.16.2+001+fix-boost-dependency.patch b/patches/react-native-vision-camera+2.16.2+001+fix-boost-dependency.patch new file mode 100644 index 000000000000..ef4fbf1d5084 --- /dev/null +++ b/patches/react-native-vision-camera+2.16.2+001+fix-boost-dependency.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native-vision-camera/android/build.gradle b/node_modules/react-native-vision-camera/android/build.gradle +index d308e15..2d87d8e 100644 +--- a/node_modules/react-native-vision-camera/android/build.gradle ++++ b/node_modules/react-native-vision-camera/android/build.gradle +@@ -347,7 +347,7 @@ if (ENABLE_FRAME_PROCESSORS) { + + task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) { + def transformedVersion = BOOST_VERSION.replace("_", ".") +- def srcUrl = "https://boostorg.jfrog.io/artifactory/main/release/${transformedVersion}/source/boost_${BOOST_VERSION}.tar.gz" ++ def srcUrl = "https://archives.boost.io/release/${transformedVersion}/source/boost_${BOOST_VERSION}.tar.gz" + if (REACT_NATIVE_VERSION < 69) { + srcUrl = "https://github.com/react-native-community/boost-for-react-native/releases/download/v${transformedVersion}-0/boost_${BOOST_VERSION}.tar.gz" + } diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.9+001+initial.patch index d88ef83d4bcd..91ba6bfd59c0 100644 --- a/patches/react-native-web+0.19.9+001+initial.patch +++ b/patches/react-native-web+0.19.9+001+initial.patch @@ -1,286 +1,648 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index c879838..288316c 100644 +index c879838..0c9dfcb 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -117,6 +117,14 @@ function findLastWhere(arr, predicate) { - * - */ - class VirtualizedList extends StateSafePureComponent { -+ pushOrUnshift(input, item) { -+ if (this.props.inverted) { -+ input.unshift(item); -+ } else { -+ input.push(item); +@@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[missing-local-annot] + + constructor(_props) { +- var _this$props$updateCel; ++ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; + super(_props); + this._getScrollMetrics = () => { + return this._scrollMetrics; +@@ -520,6 +520,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -569,7 +574,7 @@ class VirtualizedList extends StateSafePureComponent { + this._updateCellsToRender = () => { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { +- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); ++ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -589,7 +594,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable + }; + }; +@@ -621,12 +626,10 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._getFrameMetrics = (index, props) => { + var data = props.data, +- getItem = props.getItem, + getItemCount = props.getItemCount, + getItemLayout = props.getItemLayout; + invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); +- var item = getItem(data, index); +- var frame = this._frames[this._keyExtractor(item, index, props)]; ++ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -650,7 +653,7 @@ class VirtualizedList extends StateSafePureComponent { + + // The last cell we rendered may be at a new index. Bail if we don't know + // where it is. +- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { ++ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { + return []; + } + var first = focusedCellIndex; +@@ -690,9 +693,15 @@ class VirtualizedList extends StateSafePureComponent { + } + } + var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); ++ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; + this.state = { + cellsAroundViewport: initialRenderRegion, +- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) ++ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), ++ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -748,6 +757,26 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } ++ static _findItemIndexWithKey(props, key, hint) { ++ var itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ var curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (var ii = 0; ii < itemCount; ii++) { ++ var _curKey = VirtualizedList._getItemKey(props, ii); ++ if (_curKey === key) { ++ return ii; ++ } + } ++ return null; + } -+ - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params) { - var animated = params ? params.animated : true; -@@ -350,6 +358,7 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._defaultRenderScrollComponent = props => { - var onRefresh = props.onRefresh; -+ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return /*#__PURE__*/React.createElement(View, props); -@@ -367,13 +376,16 @@ class VirtualizedList extends StateSafePureComponent { - refreshing: props.refreshing, - onRefresh: onRefresh, - progressViewOffset: props.progressViewOffset -- }) : props.refreshControl -+ }) : props.refreshControl, -+ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] - })) - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return /*#__PURE__*/React.createElement(ScrollView, props); -+ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { -+ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] -+ })); ++ static _getItemKey(props, index) { ++ var item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } + static _createRenderMask(props, cellsAroundViewport, additionalRegions) { + var itemCount = props.getItemCount(props.data); + invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); +@@ -796,7 +825,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } +- _adjustCellsAroundViewport(props, cellsAroundViewport) { ++ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { + var data = props.data, + getItemCount = props.getItemCount; + var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); +@@ -819,17 +848,9 @@ class VirtualizedList extends StateSafePureComponent { + last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; } + newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); +@@ -902,16 +923,36 @@ class VirtualizedList extends StateSafePureComponent { + } + } + static getDerivedStateFromProps(newProps, prevState) { ++ var _newProps$maintainVis, _newProps$maintainVis2; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } +- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); ++ var maintainVisibleContentPositionAdjustment = null; ++ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; ++ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); ++ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { ++ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, ++ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment ++ } : prevState.cellsAroundViewport, newProps); + return { + cellsAroundViewport: constrainedCells, +- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) ++ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount }; - this._onCellLayout = (e, cellKey, index) => { -@@ -683,7 +695,7 @@ class VirtualizedList extends StateSafePureComponent { - onViewableItemsChanged = _this$props3.onViewableItemsChanged, - viewabilityConfig = _this$props3.viewabilityConfig; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged - }); -@@ -937,10 +949,10 @@ class VirtualizedList extends StateSafePureComponent { - var key = _this._keyExtractor(item, ii, _this.props); + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -934,7 +975,7 @@ class VirtualizedList extends StateSafePureComponent { + last = Math.min(end, last); + var _loop = function _loop() { + var item = getItem(data, ii); +- var key = _this._keyExtractor(item, ii, _this.props); ++ var key = VirtualizedList._keyExtractor(item, ii, _this.props); _this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ _this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); -- cells.push( /*#__PURE__*/React.createElement(CellRenderer, _extends({ -+ _this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ - CellRendererComponent: CellRendererComponent, - ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, - ListItemComponent: ListItemComponent, -@@ -1012,14 +1024,14 @@ class VirtualizedList extends StateSafePureComponent { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : - /*#__PURE__*/ - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListHeaderComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + stickyHeaderIndices.push(cells.length); +@@ -969,20 +1010,23 @@ class VirtualizedList extends StateSafePureComponent { + } + static _constrainToItemCount(cells, props) { + var itemCount = props.getItemCount(props.data); +- var last = Math.min(itemCount - 1, cells.last); ++ var lastPossibleCellIndex = itemCount - 1; ++ ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); ++ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last) + }; + } + _isNestedWithSameOrientation() { + var nestedContext = this.context; + return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); + } +- _keyExtractor(item, index, props +- // $FlowFixMe[missing-local-annot] +- ) { ++ static _keyExtractor(item, index, props) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -1022,7 +1066,12 @@ class VirtualizedList extends StateSafePureComponent { + cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + '-header', key: "$header" - }, /*#__PURE__*/React.createElement(View, { -@@ -1038,7 +1050,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListEmptyComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-empty', - key: "$empty" - }, /*#__PURE__*/React.cloneElement(_element2, { -@@ -1077,7 +1089,7 @@ class VirtualizedList extends StateSafePureComponent { - var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); - var lastMetrics = this.__getFrameMetricsApprox(last, this.props); - var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push( /*#__PURE__*/React.createElement(View, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { - key: "$spacer-" + section.first, - style: { - [spacerKey]: spacerSize -@@ -1100,7 +1112,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListFooterComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getFooterCellKey(), - key: "$footer" - }, /*#__PURE__*/React.createElement(View, { -@@ -1266,7 +1278,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } +- }, /*#__PURE__*/React.createElement(View, { ++ }, /*#__PURE__*/React.createElement(View ++ // We expect that header component will be a single native view so make it ++ // not collapsable to avoid this view being flattened and make this assumption ++ // no longer true. ++ , { ++ collapsable: false, + onLayout: this._onLayoutHeader, + style: [inversionStyle, this.props.ListHeaderComponentStyle] + }, +@@ -1124,7 +1173,11 @@ class VirtualizedList extends StateSafePureComponent { + // TODO: Android support + invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, + stickyHeaderIndices, +- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style ++ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, ++ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) ++ }) : undefined + }); + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { +@@ -1307,8 +1360,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached = _this$props8.onStartReached, + onStartReachedThreshold = _this$props8.onStartReachedThreshold, + onEndReached = _this$props8.onEndReached, +- onEndReachedThreshold = _this$props8.onEndReachedThreshold, +- initialScrollIndex = _this$props8.initialScrollIndex; ++ onEndReachedThreshold = _this$props8.onEndReachedThreshold; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + var _this$_scrollMetrics2 = this._scrollMetrics, + contentLength = _this$_scrollMetrics2.contentLength, + visibleLength = _this$_scrollMetrics2.visibleLength, +@@ -1348,16 +1405,10 @@ class VirtualizedList extends StateSafePureComponent { + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({ +- distanceFromStart +- }); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({ ++ distanceFromStart ++ }); + } + + // If the user scrolls away from the start or end and back again, +@@ -1412,6 +1463,11 @@ class VirtualizedList extends StateSafePureComponent { } - var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; -@@ -1452,6 +1464,12 @@ var styles = StyleSheet.create({ - left: 0, - borderColor: 'red', - borderWidth: 2 -+ }, -+ rowReverse: { -+ flexDirection: 'row-reverse' -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse' } - }); - export default VirtualizedList; -\ No newline at end of file + _updateViewableItems(props, cellsAroundViewport) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); + }); diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index c7d68bb..46b3fc9 100644 +index c7d68bb..43f9653 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -167,6 +167,14 @@ function findLastWhere( - class VirtualizedList extends StateSafePureComponent { - static contextType: typeof VirtualizedListContext = VirtualizedListContext; +@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { + type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, ++ // Used to track items added at the start of the list for maintainVisibleContentPosition. ++ firstVisibleItemKey: ?string, ++ // When > 0 the scroll position available in JS is considered stale and should not be used. ++ pendingScrollUpdateCount: number, + }; + + /** +@@ -447,9 +451,24 @@ class VirtualizedList extends StateSafePureComponent { + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); -+ pushOrUnshift(input: Array, item: Item) { -+ if (this.props.inverted) { -+ input.unshift(item) -+ } else { -+ input.push(item) ++ const minIndexForVisible = ++ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), ++ firstVisibleItemKey: ++ this.props.getItemCount(this.props.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) ++ : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: ++ this.props.initialScrollIndex != null && ++ this.props.initialScrollIndex > 0 ++ ? 1 ++ : 0, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -534,6 +553,40 @@ class VirtualizedList extends StateSafePureComponent { + } + } + ++ static _findItemIndexWithKey( ++ props: Props, ++ key: string, ++ hint: ?number, ++ ): ?number { ++ const itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ const curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } + } ++ for (let ii = 0; ii < itemCount; ii++) { ++ const curKey = VirtualizedList._getItemKey(props, ii); ++ if (curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ ++ static _getItemKey( ++ props: { ++ data: Props['data'], ++ getItem: Props['getItem'], ++ keyExtractor: Props['keyExtractor'], ++ ... ++ }, ++ index: number, ++ ): string { ++ const item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); + } + - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params?: ?{animated?: ?boolean, ...}) { - const animated = params ? params.animated : true; -@@ -438,7 +446,7 @@ class VirtualizedList extends StateSafePureComponent { + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, +@@ -617,6 +670,7 @@ class VirtualizedList extends StateSafePureComponent { + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, ++ pendingScrollUpdateCount: number, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( +@@ -648,21 +702,9 @@ class VirtualizedList extends StateSafePureComponent { + ), + }; } else { - const {onViewableItemsChanged, viewabilityConfig} = this.props; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged, - }); -@@ -814,13 +822,13 @@ class VirtualizedList extends StateSafePureComponent { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if ( +- props.initialScrollIndex && +- !this._scrollMetrics.offset && +- Math.abs(distanceFromEnd) >= Number.EPSILON +- ) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; +@@ -771,14 +813,59 @@ class VirtualizedList extends StateSafePureComponent { + return prevState; + } + ++ let maintainVisibleContentPositionAdjustment: ?number = null; ++ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ const minIndexForVisible = ++ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const newFirstVisibleItemKey = ++ newProps.getItemCount(newProps.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) ++ : null; ++ if ( ++ newProps.maintainVisibleContentPosition != null && ++ prevFirstVisibleItemKey != null && ++ newFirstVisibleItemKey != null ++ ) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ const hint = ++ itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( ++ newProps, ++ prevFirstVisibleItemKey, ++ hint, ++ ); ++ maintainVisibleContentPositionAdjustment = ++ firstVisibleItemIndex != null ++ ? firstVisibleItemIndex - minIndexForVisible ++ : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ + const constrainedCells = VirtualizedList._constrainToItemCount( +- prevState.cellsAroundViewport, ++ maintainVisibleContentPositionAdjustment != null ++ ? { ++ first: ++ prevState.cellsAroundViewport.first + ++ maintainVisibleContentPositionAdjustment, ++ last: ++ prevState.cellsAroundViewport.last + ++ maintainVisibleContentPositionAdjustment, ++ } ++ : prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: ++ maintainVisibleContentPositionAdjustment != null ++ ? prevState.pendingScrollUpdateCount + 1 ++ : prevState.pendingScrollUpdateCount, + }; + } + +@@ -810,7 +897,7 @@ class VirtualizedList extends StateSafePureComponent { + + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); +- const key = this._keyExtractor(item, ii, this.props); ++ const key = VirtualizedList._keyExtractor(item, ii, this.props); this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ this.pushOrUnshift(stickyHeaderIndices, (cells.length)); - } +@@ -853,15 +940,19 @@ class VirtualizedList extends StateSafePureComponent { + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); +- const last = Math.min(itemCount - 1, cells.last); ++ const lastPossibleCellIndex = itemCount - 1; - const shouldListenForLayout = - getItemLayout == null || debug || this._fillRateHelper.enabled(); ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); ++ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); -- cells.push( -+ this.pushOrUnshift(cells, - { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - const element = React.isValidElement(ListHeaderComponent) ? ( - ListHeaderComponent -@@ -932,7 +940,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[incompatible-type-arg] - - ); -- cells.push( -+ this.pushOrUnshift(cells, - { + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + +- _keyExtractor( ++ static _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, +- // $FlowFixMe[missing-local-annot] +- ) { ++ ): string { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -937,6 +1027,10 @@ class VirtualizedList extends StateSafePureComponent { cellKey={this._getCellKey() + '-header'} key="$header"> -@@ -963,7 +971,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[incompatible-type-arg] - - )): any); -- cells.push( -+ this.pushOrUnshift(cells, - -@@ -1017,7 +1025,7 @@ class VirtualizedList extends StateSafePureComponent { - const lastMetrics = this.__getFrameMetricsApprox(last, this.props); - const spacerSize = - lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push( -+ this.pushOrUnshift(cells, - { - // $FlowFixMe[incompatible-type-arg] - - ); -- cells.push( -+ this.pushOrUnshift(cells, - -@@ -1246,6 +1254,12 @@ class VirtualizedList extends StateSafePureComponent { - * LTI update could not be added via codemod */ - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; -+ const inversionStyle = this.props.inverted -+ ? this.props.horizontal -+ ? styles.rowReverse -+ : styles.columnReverse -+ : null; -+ - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; -@@ -1273,12 +1287,24 @@ class VirtualizedList extends StateSafePureComponent { - props.refreshControl - ) - } -+ contentContainerStyle={[ -+ inversionStyle, -+ this.props.contentContainerStyle, -+ ]} - /> - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return ; -+ return ( -+ -+ ); - } - }; + { + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, ++ maintainVisibleContentPosition: ++ this.props.maintainVisibleContentPosition != null ++ ? { ++ ...this.props.maintainVisibleContentPosition, ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: ++ this.props.maintainVisibleContentPosition.minIndexForVisible + ++ (this.props.ListHeaderComponent ? 1 : 0), ++ } ++ : undefined, + }; -@@ -1432,7 +1458,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; +@@ -1516,8 +1620,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, +- initialScrollIndex, + } = this.props; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; +@@ -1569,14 +1677,8 @@ class VirtualizedList extends StateSafePureComponent { + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({distanceFromStart}); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({distanceFromStart}); } - const windowTop = this.__getFrameMetricsApprox( -@@ -2044,6 +2070,12 @@ const styles = StyleSheet.create({ - borderColor: 'red', - borderWidth: 2, - }, -+ rowReverse: { -+ flexDirection: 'row-reverse', -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse', -+ }, - }); - export default VirtualizedList; -\ No newline at end of file + // If the user scrolls away from the start or end and back again, +@@ -1703,6 +1805,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale, + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -1818,6 +1925,7 @@ class VirtualizedList extends StateSafePureComponent { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, ++ state.pendingScrollUpdateCount, + ); + const renderMask = VirtualizedList._createRenderMask( + props, +@@ -1848,7 +1956,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable, + }; + }; +@@ -1909,13 +2017,12 @@ class VirtualizedList extends StateSafePureComponent { + inLayout?: boolean, + ... + } => { +- const {data, getItem, getItemCount, getItemLayout} = props; ++ const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); +- const item = getItem(data, index); +- const frame = this._frames[this._keyExtractor(item, index, props)]; ++ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -1950,11 +2057,8 @@ class VirtualizedList extends StateSafePureComponent { + // where it is. + if ( + focusedCellIndex >= itemCount || +- this._keyExtractor( +- props.getItem(props.data, focusedCellIndex), +- focusedCellIndex, +- props, +- ) !== this._lastFocusedCellKey ++ VirtualizedList._getItemKey(props, focusedCellIndex) !== ++ this._lastFocusedCellKey + ) { + return []; + } +@@ -1995,6 +2099,11 @@ class VirtualizedList extends StateSafePureComponent { + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, diff --git a/patches/react-native-web+0.19.9+002+fix-mvcp.patch b/patches/react-native-web+0.19.9+002+fix-mvcp.patch deleted file mode 100644 index afd681bba3b0..000000000000 --- a/patches/react-native-web+0.19.9+002+fix-mvcp.patch +++ /dev/null @@ -1,687 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index a6fe142..faeb323 100644 ---- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -293,7 +293,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[missing-local-annot] - - constructor(_props) { -- var _this$props$updateCel; -+ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; - super(_props); - this._getScrollMetrics = () => { - return this._scrollMetrics; -@@ -532,6 +532,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -581,7 +586,7 @@ class VirtualizedList extends StateSafePureComponent { - this._updateCellsToRender = () => { - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - this.setState((state, props) => { -- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); -+ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); - var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); - if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { - return null; -@@ -601,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable - }; - }; -@@ -633,12 +638,10 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._getFrameMetrics = (index, props) => { - var data = props.data, -- getItem = props.getItem, - getItemCount = props.getItemCount, - getItemLayout = props.getItemLayout; - invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); -- var item = getItem(data, index); -- var frame = this._frames[this._keyExtractor(item, index, props)]; -+ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -662,7 +665,7 @@ class VirtualizedList extends StateSafePureComponent { - - // The last cell we rendered may be at a new index. Bail if we don't know - // where it is. -- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { -+ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { - return []; - } - var first = focusedCellIndex; -@@ -702,9 +705,15 @@ class VirtualizedList extends StateSafePureComponent { - } - } - var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); -+ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; - this.state = { - cellsAroundViewport: initialRenderRegion, -- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) -+ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), -+ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -715,7 +724,7 @@ class VirtualizedList extends StateSafePureComponent { - var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight; - var isEventTargetScrollable = scrollLength > clientLength; - var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; -- var leftoverDelta = delta; -+ var leftoverDelta = delta * 0.5; - if (isEventTargetScrollable) { - leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); - } -@@ -760,6 +769,26 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -+ static _findItemIndexWithKey(props, key, hint) { -+ var itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ var curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (var ii = 0; ii < itemCount; ii++) { -+ var _curKey = VirtualizedList._getItemKey(props, ii); -+ if (_curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ static _getItemKey(props, index) { -+ var item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } - static _createRenderMask(props, cellsAroundViewport, additionalRegions) { - var itemCount = props.getItemCount(props.data); - invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); -@@ -808,7 +837,7 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -- _adjustCellsAroundViewport(props, cellsAroundViewport) { -+ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { - var data = props.data, - getItemCount = props.getItemCount; - var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); -@@ -831,17 +860,9 @@ class VirtualizedList extends StateSafePureComponent { - last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; - } - newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); -@@ -914,16 +935,36 @@ class VirtualizedList extends StateSafePureComponent { - } - } - static getDerivedStateFromProps(newProps, prevState) { -+ var _newProps$maintainVis, _newProps$maintainVis2; - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - var itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } -- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); -+ var maintainVisibleContentPositionAdjustment = null; -+ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; -+ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; -+ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); -+ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { -+ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, -+ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment -+ } : prevState.cellsAroundViewport, newProps); - return { - cellsAroundViewport: constrainedCells, -- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) -+ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount - }; - } - _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { -@@ -946,7 +987,7 @@ class VirtualizedList extends StateSafePureComponent { - last = Math.min(end, last); - var _loop = function _loop() { - var item = getItem(data, ii); -- var key = _this._keyExtractor(item, ii, _this.props); -+ var key = VirtualizedList._keyExtractor(item, ii, _this.props); - _this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { - _this.pushOrUnshift(stickyHeaderIndices, cells.length); -@@ -981,20 +1022,23 @@ class VirtualizedList extends StateSafePureComponent { - } - static _constrainToItemCount(cells, props) { - var itemCount = props.getItemCount(props.data); -- var last = Math.min(itemCount - 1, cells.last); -+ var lastPossibleCellIndex = itemCount - 1; -+ -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); -+ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last) - }; - } - _isNestedWithSameOrientation() { - var nestedContext = this.context; - return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); - } -- _keyExtractor(item, index, props -- // $FlowFixMe[missing-local-annot] -- ) { -+ static _keyExtractor(item, index, props) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -1034,7 +1078,12 @@ class VirtualizedList extends StateSafePureComponent { - this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-header', - key: "$header" -- }, /*#__PURE__*/React.createElement(View, { -+ }, /*#__PURE__*/React.createElement(View -+ // We expect that header component will be a single native view so make it -+ // not collapsable to avoid this view being flattened and make this assumption -+ // no longer true. -+ , { -+ collapsable: false, - onLayout: this._onLayoutHeader, - style: [inversionStyle, this.props.ListHeaderComponentStyle] - }, -@@ -1136,7 +1185,11 @@ class VirtualizedList extends StateSafePureComponent { - // TODO: Android support - invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, - stickyHeaderIndices, -- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style -+ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, -+ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) -+ }) : undefined - }); - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReached = _this$props8.onStartReached, - onStartReachedThreshold = _this$props8.onStartReachedThreshold, - onEndReached = _this$props8.onEndReached, -- onEndReachedThreshold = _this$props8.onEndReachedThreshold, -- initialScrollIndex = _this$props8.initialScrollIndex; -+ onEndReachedThreshold = _this$props8.onEndReachedThreshold; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - var _this$_scrollMetrics2 = this._scrollMetrics, - contentLength = _this$_scrollMetrics2.contentLength, - visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({ -- distanceFromStart -- }); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({ -+ distanceFromStart -+ }); - } - - // If the user scrolls away from the start or end and back again, -@@ -1424,6 +1475,11 @@ class VirtualizedList extends StateSafePureComponent { - } - } - _updateViewableItems(props, cellsAroundViewport) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); - }); -diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index d896fb1..f303b31 100644 ---- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { - type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -+ // Used to track items added at the start of the list for maintainVisibleContentPosition. -+ firstVisibleItemKey: ?string, -+ // When > 0 the scroll position available in JS is considered stale and should not be used. -+ pendingScrollUpdateCount: number, - }; - - /** -@@ -455,9 +459,24 @@ class VirtualizedList extends StateSafePureComponent { - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); - -+ const minIndexForVisible = -+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), -+ firstVisibleItemKey: -+ this.props.getItemCount(this.props.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) -+ : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: -+ this.props.initialScrollIndex != null && -+ this.props.initialScrollIndex > 0 -+ ? 1 -+ : 0, - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -470,7 +489,7 @@ class VirtualizedList extends StateSafePureComponent { - const delta = this.props.horizontal - ? ev.deltaX || ev.wheelDeltaX - : ev.deltaY || ev.wheelDeltaY; -- let leftoverDelta = delta; -+ let leftoverDelta = delta * 5; - if (isEventTargetScrollable) { - leftoverDelta = delta < 0 - ? Math.min(delta + scrollOffset, 0) -@@ -542,6 +561,40 @@ class VirtualizedList extends StateSafePureComponent { - } - } - -+ static _findItemIndexWithKey( -+ props: Props, -+ key: string, -+ hint: ?number, -+ ): ?number { -+ const itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ const curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (let ii = 0; ii < itemCount; ii++) { -+ const curKey = VirtualizedList._getItemKey(props, ii); -+ if (curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ -+ static _getItemKey( -+ props: { -+ data: Props['data'], -+ getItem: Props['getItem'], -+ keyExtractor: Props['keyExtractor'], -+ ... -+ }, -+ index: number, -+ ): string { -+ const item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } -+ - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, -@@ -625,6 +678,7 @@ class VirtualizedList extends StateSafePureComponent { - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, -+ pendingScrollUpdateCount: number, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( -@@ -656,21 +710,9 @@ class VirtualizedList extends StateSafePureComponent { - ), - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if ( -- props.initialScrollIndex && -- !this._scrollMetrics.offset && -- Math.abs(distanceFromEnd) >= Number.EPSILON -- ) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; -@@ -779,14 +821,59 @@ class VirtualizedList extends StateSafePureComponent { - return prevState; - } - -+ let maintainVisibleContentPositionAdjustment: ?number = null; -+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ const minIndexForVisible = -+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ const newFirstVisibleItemKey = -+ newProps.getItemCount(newProps.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) -+ : null; -+ if ( -+ newProps.maintainVisibleContentPosition != null && -+ prevFirstVisibleItemKey != null && -+ newFirstVisibleItemKey != null -+ ) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ const hint = -+ itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( -+ newProps, -+ prevFirstVisibleItemKey, -+ hint, -+ ); -+ maintainVisibleContentPositionAdjustment = -+ firstVisibleItemIndex != null -+ ? firstVisibleItemIndex - minIndexForVisible -+ : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ - const constrainedCells = VirtualizedList._constrainToItemCount( -- prevState.cellsAroundViewport, -+ maintainVisibleContentPositionAdjustment != null -+ ? { -+ first: -+ prevState.cellsAroundViewport.first + -+ maintainVisibleContentPositionAdjustment, -+ last: -+ prevState.cellsAroundViewport.last + -+ maintainVisibleContentPositionAdjustment, -+ } -+ : prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: -+ maintainVisibleContentPositionAdjustment != null -+ ? prevState.pendingScrollUpdateCount + 1 -+ : prevState.pendingScrollUpdateCount, - }; - } - -@@ -818,11 +905,11 @@ class VirtualizedList extends StateSafePureComponent { - - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); -- const key = this._keyExtractor(item, ii, this.props); -+ const key = VirtualizedList._keyExtractor(item, ii, this.props); - - this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- this.pushOrUnshift(stickyHeaderIndices, (cells.length)); -+ this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - - const shouldListenForLayout = -@@ -861,15 +948,19 @@ class VirtualizedList extends StateSafePureComponent { - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); -- const last = Math.min(itemCount - 1, cells.last); -+ const lastPossibleCellIndex = itemCount - 1; - -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); -+ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last, -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last), - }; - } - -@@ -891,15 +982,14 @@ class VirtualizedList extends StateSafePureComponent { - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; - -- _keyExtractor( -+ static _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, -- // $FlowFixMe[missing-local-annot] -- ) { -+ ): string { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -945,6 +1035,10 @@ class VirtualizedList extends StateSafePureComponent { - cellKey={this._getCellKey() + '-header'} - key="$header"> - { - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, -+ maintainVisibleContentPosition: -+ this.props.maintainVisibleContentPosition != null -+ ? { -+ ...this.props.maintainVisibleContentPosition, -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: -+ this.props.maintainVisibleContentPosition.minIndexForVisible + -+ (this.props.ListHeaderComponent ? 1 : 0), -+ } -+ : undefined, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; -@@ -1255,11 +1359,10 @@ class VirtualizedList extends StateSafePureComponent { - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; - const inversionStyle = this.props.inverted -- ? this.props.horizontal -- ? styles.rowReverse -- : styles.columnReverse -- : null; -- -+ ? this.props.horizontal -+ ? styles.rowReverse -+ : styles.columnReverse -+ : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; -@@ -1542,8 +1645,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, -- initialScrollIndex, - } = this.props; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; -@@ -1595,14 +1702,8 @@ class VirtualizedList extends StateSafePureComponent { - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({distanceFromStart}); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({distanceFromStart}); - } - - // If the user scrolls away from the start or end and back again, -@@ -1729,6 +1830,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale, - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -1844,6 +1950,7 @@ class VirtualizedList extends StateSafePureComponent { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, -+ state.pendingScrollUpdateCount, - ); - const renderMask = VirtualizedList._createRenderMask( - props, -@@ -1874,7 +1981,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable, - }; - }; -@@ -1935,13 +2042,12 @@ class VirtualizedList extends StateSafePureComponent { - inLayout?: boolean, - ... - } => { -- const {data, getItem, getItemCount, getItemLayout} = props; -+ const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); -- const item = getItem(data, index); -- const frame = this._frames[this._keyExtractor(item, index, props)]; -+ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -1976,11 +2082,8 @@ class VirtualizedList extends StateSafePureComponent { - // where it is. - if ( - focusedCellIndex >= itemCount || -- this._keyExtractor( -- props.getItem(props.data, focusedCellIndex), -- focusedCellIndex, -- props, -- ) !== this._lastFocusedCellKey -+ VirtualizedList._getItemKey(props, focusedCellIndex) !== -+ this._lastFocusedCellKey - ) { - return []; - } -@@ -2021,6 +2124,11 @@ class VirtualizedList extends StateSafePureComponent { - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, diff --git a/patches/react-native-web+0.19.9+003+measureInWindow.patch b/patches/react-native-web+0.19.9+002+measureInWindow.patch similarity index 100% rename from patches/react-native-web+0.19.9+003+measureInWindow.patch rename to patches/react-native-web+0.19.9+002+measureInWindow.patch diff --git a/patches/react-native-web+0.19.9+004+fix-pointer-events.patch b/patches/react-native-web+0.19.9+003+fix-pointer-events.patch similarity index 100% rename from patches/react-native-web+0.19.9+004+fix-pointer-events.patch rename to patches/react-native-web+0.19.9+003+fix-pointer-events.patch diff --git a/src/CONFIG.ts b/src/CONFIG.ts index 61e347671269..37da65f0c305 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -1,5 +1,6 @@ import {Platform} from 'react-native'; -import Config, {NativeConfig} from 'react-native-config'; +import type {NativeConfig} from 'react-native-config'; +import Config from 'react-native-config'; import CONST from './CONST'; import getPlatform from './libs/getPlatform'; import * as Url from './libs/Url'; diff --git a/src/CONST.ts b/src/CONST.ts index db3f479aa466..c6849db630f2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -103,6 +103,10 @@ const CONST = { MERCHANT_NAME_MAX_LENGTH: 255, + REQUEST_PREVIEW: { + MAX_LENGTH: 83, + }, + CALENDAR_PICKER: { // Numbers were arbitrarily picked. MIN_YEAR: CURRENT_YEAR - 100, @@ -265,7 +269,6 @@ const CONST = { CHRONOS_IN_CASH: 'chronosInCash', DEFAULT_ROOMS: 'defaultRooms', BETA_COMMENT_LINKING: 'commentLinking', - POLICY_ROOMS: 'policyRooms', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', }, @@ -512,6 +515,7 @@ const CONST = { CLOSED: 'CLOSED', CREATED: 'CREATED', IOU: 'IOU', + MARKEDREIMBURSED: 'MARKEDREIMBURSED', MODIFIEDEXPENSE: 'MODIFIEDEXPENSE', MOVED: 'MOVED', REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', @@ -854,7 +858,7 @@ const CONST = { // It's copied here so that the same regex pattern can be used in form validations to be consistent with the server. VALIDATE_FOR_HTML_TAG_REGEX: /<([^>\s]+)(?:[^>]*?)>/g, - VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX: /<([\s]+[\s\w~!@#$%^&*(){}[\];':"`|?.,/\\+\-=<]+.*[\s]*)>/g, + VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX: /<([\s]+.+[\s]*)>/g, WHITELISTED_TAGS: [/<>/, /< >/, /<->/, /<-->/, /
/, //], @@ -1361,6 +1365,7 @@ const CONST = { DIGITS_AND_PLUS: /^\+?[0-9]*$/, ALPHABETIC_AND_LATIN_CHARS: /^[\p{Script=Latin} ]*$/u, NON_ALPHABETIC_AND_NON_LATIN_CHARS: /[^\p{Script=Latin}]/gu, + ACCENT_LATIN_CHARS: /[\u00C0-\u017F]/g, POSITIVE_INTEGER: /^\d+$/, PO_BOX: /\b[P|p]?(OST|ost)?\.?\s*[O|o|0]?(ffice|FFICE)?\.?\s*[B|b][O|o|0]?[X|x]?\.?\s+[#]?(\d+)\b/, ANY_VALUE: /^.+$/, @@ -1418,6 +1423,7 @@ const CONST = { ROUTES: { VALIDATE_LOGIN: /\/v($|(\/\/*))/, UNLINK_LOGIN: /\/u($|(\/\/*))/, + REDUNDANT_SLASHES: /(\/{2,})|(\/$)/g, }, TIME_STARTS_01: /^01:\d{2} [AP]M$/, @@ -2885,8 +2891,10 @@ const CONST = { ATTACHMENT: 'common.attachment', }, TEACHERS_UNITE: { - PUBLIC_ROOM_ID: '7470147100835202', - POLICY_ID: 'B795B6319125BDF2', + PROD_PUBLIC_ROOM_ID: '7470147100835202', + PROD_POLICY_ID: 'B795B6319125BDF2', + TEST_PUBLIC_ROOM_ID: '207591744844000', + TEST_POLICY_ID: 'ABD1345ED7293535', POLICY_NAME: 'Expensify.org / Teachers Unite!', PUBLIC_ROOM_NAME: '#teachers-unite', }, @@ -2945,10 +2953,12 @@ const CONST = { PARENT_CHILD_SEPARATOR: ': ', CATEGORY_LIST_THRESHOLD: 8, TAG_LIST_THRESHOLD: 8, + TAX_RATES_LIST_THRESHOLD: 8, COLON: ':', MAPBOX: { PADDING: 50, DEFAULT_ZOOM: 10, + SINGLE_MARKER_ZOOM: 15, DEFAULT_COORDINATE: [-122.4021, 37.7911], STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq', }, @@ -3053,6 +3063,42 @@ const CONST = { CAROUSEL: 3, }, + VIOLATIONS: { + ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired', + AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense', + BILLABLE_EXPENSE: 'billableExpense', + CASH_EXPENSE_WITH_NO_RECEIPT: 'cashExpenseWithNoReceipt', + CATEGORY_OUT_OF_POLICY: 'categoryOutOfPolicy', + CONVERSION_SURCHARGE: 'conversionSurcharge', + CUSTOM_UNIT_OUT_OF_POLICY: 'customUnitOutOfPolicy', + DUPLICATED_TRANSACTION: 'duplicatedTransaction', + FIELD_REQUIRED: 'fieldRequired', + FUTURE_DATE: 'futureDate', + INVOICE_MARKUP: 'invoiceMarkup', + MAX_AGE: 'maxAge', + MISSING_CATEGORY: 'missingCategory', + MISSING_COMMENT: 'missingComment', + MISSING_TAG: 'missingTag', + MODIFIED_AMOUNT: 'modifiedAmount', + MODIFIED_DATE: 'modifiedDate', + NON_EXPENSIWORKS_EXPENSE: 'nonExpensiworksExpense', + OVER_AUTO_APPROVAL_LIMIT: 'overAutoApprovalLimit', + OVER_CATEGORY_LIMIT: 'overCategoryLimit', + OVER_LIMIT: 'overLimit', + OVER_LIMIT_ATTENDEE: 'overLimitAttendee', + PER_DAY_LIMIT: 'perDayLimit', + RECEIPT_NOT_SMART_SCANNED: 'receiptNotSmartScanned', + RECEIPT_REQUIRED: 'receiptRequired', + RTER: 'rter', + SMARTSCAN_FAILED: 'smartscanFailed', + SOME_TAG_LEVELS_REQUIRED: 'someTagLevelsRequired', + TAG_OUT_OF_POLICY: 'tagOutOfPolicy', + TAX_AMOUNT_CHANGED: 'taxAmountChanged', + TAX_OUT_OF_POLICY: 'taxOutOfPolicy', + TAX_RATE_CHANGED: 'taxRateChanged', + TAX_REQUIRED: 'taxRequired', + }, + /** Context menu types */ CONTEXT_MENU_TYPES: { LINK: 'LINK', diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index a3a041e65684..c68a950d3501 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -4,6 +4,7 @@ * */ export default { CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator', + LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator', RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', } as const; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 53cd37e71f67..89ddbdc06883 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,8 +1,8 @@ -import {OnyxEntry} from 'react-native-onyx/lib/types'; -import {ValueOf} from 'type-fest'; -import CONST from './CONST'; -import * as OnyxTypes from './types/onyx'; -import DeepValueOf from './types/utils/DeepValueOf'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {ValueOf} from 'type-fest'; +import type CONST from './CONST'; +import type * as OnyxTypes from './types/onyx'; +import type DeepValueOf from './types/utils/DeepValueOf'; /** * This is a file containing constants for all the top level keys in our store @@ -250,6 +250,7 @@ const ONYXKEYS = { POLICY_CATEGORIES: 'policyCategories_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', + POLICY_TAX_RATE: 'policyTaxRates_', POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', POLICY_REPORT_FIELDS: 'policyReportFields_', POLICY_RECENTLY_USED_REPORT_FIELDS: 'policyRecentlyUsedReportFields_', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index db17378684d6..e8a860582bb1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1,5 +1,5 @@ -import {IsEqual, ValueOf} from 'type-fest'; -import CONST from './CONST'; +import type {IsEqual, ValueOf} from 'type-fest'; +import type CONST from './CONST'; // This is a file containing constants for all the routes we want to be able to go to @@ -314,61 +314,71 @@ const ROUTES = { getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}` as const, }, MONEY_REQUEST_STEP_CONFIRMATION: { - route: 'create/:iouType/confirmation/:transactionID/:reportID/', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/confirmation/${transactionID}/${reportID}/` as const, + route: 'create/:iouType/confirmation/:transactionID/:reportID', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/confirmation/${transactionID}/${reportID}` as const, }, MONEY_REQUEST_STEP_AMOUNT: { - route: 'create/:iouType/amount/:transactionID/:reportID/', + route: 'create/:iouType/amount/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/amount/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/amount/${transactionID}/${reportID}`, backTo), + }, + MONEY_REQUEST_STEP_TAX_RATE: { + route: 'create/:iouType/taxRate/:transactionID/:reportID?', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo: string) => + getUrlWithBackToParam(`create/${iouType}/taxRate/${transactionID}/${reportID}`, backTo), + }, + MONEY_REQUEST_STEP_TAX_AMOUNT: { + route: 'create/:iouType/taxAmount/:transactionID/:reportID?', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo: string) => + getUrlWithBackToParam(`create/${iouType}/taxAmount/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_CATEGORY: { - route: 'create/:iouType/category/:transactionID/:reportID/', + route: 'create/:iouType/category/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/category/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/category/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_CURRENCY: { - route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?/', + route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => getUrlWithBackToParam(`create/${iouType}/currency/${transactionID}/${reportID}/${pageIndex}`, backTo), }, MONEY_REQUEST_STEP_DATE: { - route: 'create/:iouType/date/:transactionID/:reportID/', + route: 'create/:iouType/date/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DESCRIPTION: { - route: 'create/:iouType/description/:transactionID/:reportID/', + route: 'create/:iouType/description/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DISTANCE: { - route: 'create/:iouType/distance/:transactionID/:reportID/', + route: 'create/:iouType/distance/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_MERCHANT: { - route: 'create/:iouType/merchante/:transactionID/:reportID/', + route: 'create/:iouType/merchant/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/merchante/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/merchant/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_PARTICIPANTS: { - route: 'create/:iouType/participants/:transactionID/:reportID/', + route: 'create/:iouType/participants/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/participants/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/participants/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_SCAN: { - route: 'create/:iouType/scan/:transactionID/:reportID/', + route: 'create/:iouType/scan/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/scan/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/scan/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_TAG: { - route: 'create/:iouType/tag/:transactionID/:reportID/', + route: 'create/:iouType/tag/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_WAYPOINT: { - route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex/', + route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => getUrlWithBackToParam(`create/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo), }, @@ -469,6 +479,7 @@ const ROUTES = { route: 'referral/:contentType', getRoute: (contentType: string) => `referral/${contentType}` as const, }, + PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', } as const; export {getUrlWithBackToParam}; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c1d2059cd3b0..703cb309d641 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -2,7 +2,7 @@ * This is a file containing constants for all of the screen names. In most cases, we should use the routes for * navigation. But there are situations where we may need to access screen names directly. */ -import DeepValueOf from './types/utils/DeepValueOf'; +import type DeepValueOf from './types/utils/DeepValueOf'; const PROTECTED_SCREENS = { HOME: 'Home', @@ -81,10 +81,12 @@ const SCREENS = { SAVE_THE_WORLD: { ROOT: 'SaveTheWorld_Root', }, + LEFT_MODAL: { + SEARCH: 'Search', + }, RIGHT_MODAL: { SETTINGS: 'Settings', NEW_CHAT: 'NewChat', - SEARCH: 'Search', DETAILS: 'Details', PROFILE: 'Profile', REPORT_DETAILS: 'Report_Details', @@ -106,6 +108,7 @@ const SCREENS = { ROOM_MEMBERS: 'RoomMembers', ROOM_INVITE: 'RoomInvite', REFERRAL: 'Referral', + PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', }, SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', @@ -130,6 +133,8 @@ const SCREENS = { STEP_SCAN: 'Money_Request_Step_Scan', STEP_TAG: 'Money_Request_Step_Tag', STEP_WAYPOINT: 'Money_Request_Step_Waypoint', + STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount', + STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', ROOT: 'Money_Request', AMOUNT: 'Money_Request_Amount', PARTICIPANTS: 'Money_Request_Participants', @@ -227,6 +232,7 @@ const SCREENS = { SIGN_IN_ROOT: 'SignIn_Root', DETAILS_ROOT: 'Details_Root', PROFILE_ROOT: 'Profile_Root', + PROCESS_MONEY_REQUEST_HOLD_ROOT: 'ProcessMoneyRequestHold_Root', REPORT_WELCOME_MESSAGE_ROOT: 'Report_WelcomeMessage_Root', REPORT_PARTICIPANTS_ROOT: 'ReportParticipants_Root', ROOM_MEMBERS_ROOT: 'RoomMembers_Root', diff --git a/src/TIMEZONES.ts b/src/TIMEZONES.ts index 1eb49f291495..238563134872 100644 --- a/src/TIMEZONES.ts +++ b/src/TIMEZONES.ts @@ -1,5 +1,6 @@ +/* eslint-disable @typescript-eslint/naming-convention */ // All timezones were taken from: https://raw.githubusercontent.com/leon-do/Timezones/main/timezone.json -export default [ +const TIMEZONES = [ 'Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', @@ -419,3 +420,137 @@ export default [ 'Pacific/Wake', 'Pacific/Wallis', ] as const; + +/** + * The timezones supported in browser and on native devices differ, so we must map each timezone to its supported equivalent. + * Data sourced from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + */ +const timezoneBackwardMap: Record = { + 'Africa/Asmera': 'Africa/Nairobi', + 'Africa/Timbuktu': 'Africa/Abidjan', + 'America/Argentina/ComodRivadavia': 'America/Argentina/Catamarca', + 'America/Atka': 'America/Adak', + 'America/Buenos_Aires': 'America/Argentina/Buenos_Aires', + 'America/Catamarca': 'America/Argentina/Catamarca', + 'America/Coral_Harbour': 'America/Panama', + 'America/Cordoba': 'America/Argentina/Cordoba', + 'America/Ensenada': 'America/Tijuana', + 'America/Fort_Wayne': 'America/Indiana/Indianapolis', + 'America/Godthab': 'America/Nuuk', + 'America/Indianapolis': 'America/Indiana/Indianapolis', + 'America/Jujuy': 'America/Argentina/Jujuy', + 'America/Knox_IN': 'America/Indiana/Knox', + 'America/Louisville': 'America/Kentucky/Louisville', + 'America/Mendoza': 'America/Argentina/Mendoza', + 'America/Montreal': 'America/Toronto', + 'America/Nipigon': 'America/Toronto', + 'America/Pangnirtung': 'America/Iqaluit', + 'America/Porto_Acre': 'America/Rio_Branco', + 'America/Rainy_River': 'America/Winnipeg', + 'America/Rosario': 'America/Argentina/Cordoba', + 'America/Santa_Isabel': 'America/Tijuana', + 'America/Shiprock': 'America/Denver', + 'America/Thunder_Bay': 'America/Toronto', + 'America/Virgin': 'America/Puerto_Rico', + 'America/Yellowknife': 'America/Edmonton', + 'Antarctica/South_Pole': 'Pacific/Auckland', + 'Asia/Ashkhabad': 'Asia/Ashgabat', + 'Asia/Calcutta': 'Asia/Kolkata', + 'Asia/Chongqing': 'Asia/Shanghai', + 'Asia/Chungking': 'Asia/Shanghai', + 'Asia/Dacca': 'Asia/Dhaka', + 'Asia/Harbin': 'Asia/Shanghai', + 'Asia/Istanbul': 'Europe/Istanbul', + 'Asia/Kashgar': 'Asia/Urumqi', + 'Asia/Katmandu': 'Asia/Kathmandu', + 'Asia/Macao': 'Asia/Macau', + 'Asia/Rangoon': 'Asia/Yangon', + 'Asia/Saigon': 'Asia/Ho_Chi_Minh', + 'Asia/Tel_Aviv': 'Asia/Jerusalem', + 'Asia/Thimbu': 'Asia/Thimphu', + 'Asia/Ujung_Pandang': 'Asia/Makassar', + 'Asia/Ulan_Bator': 'Asia/Ulaanbaatar', + 'Atlantic/Faeroe': 'Atlantic/Faroe', + 'Atlantic/Jan_Mayen': 'Europe/Berlin', + 'Australia/ACT': 'Australia/Sydney', + 'Australia/Canberra': 'Australia/Sydney', + 'Australia/Currie': 'Australia/Hobart', + 'Australia/LHI': 'Australia/Lord_Howe', + 'Australia/NSW': 'Australia/Sydney', + 'Australia/North': 'Australia/Darwin', + 'Australia/Queensland': 'Australia/Brisbane', + 'Australia/South': 'Australia/Adelaide', + 'Australia/Tasmania': 'Australia/Hobart', + 'Australia/Victoria': 'Australia/Melbourne', + 'Australia/West': 'Australia/Perth', + 'Australia/Yancowinna': 'Australia/Broken_Hill', + 'Brazil/Acre': 'America/Rio_Branco', + 'Brazil/DeNoronha': 'America/Noronha', + 'Brazil/East': 'America/Sao_Paulo', + 'Brazil/West': 'America/Manaus', + 'Canada/Atlantic': 'America/Halifax', + 'Canada/Central': 'America/Winnipeg', + 'Canada/Eastern': 'America/Toronto', + 'Canada/Mountain': 'America/Edmonton', + 'Canada/Newfoundland': 'America/St_Johns', + 'Canada/Pacific': 'America/Vancouver', + 'Canada/Saskatchewan': 'America/Regina', + 'Canada/Yukon': 'America/Whitehorse', + 'Chile/Continental': 'America/Santiago', + 'Chile/EasterIsland': 'Pacific/Easter', + Cuba: 'America/Havana', + Egypt: 'Africa/Cairo', + Eire: 'Europe/Dublin', + 'Europe/Belfast': 'Europe/London', + 'Europe/Kiev': 'Europe/Kyiv', + 'Europe/Nicosia': 'Asia/Nicosia', + 'Europe/Tiraspol': 'Europe/Chisinau', + 'Europe/Uzhgorod': 'Europe/Kyiv', + 'Europe/Zaporozhye': 'Europe/Kyiv', + GB: 'Europe/London', + 'GB-Eire': 'Europe/London', + Hongkong: 'Asia/Hong_Kong', + Iceland: 'Africa/Abidjan', + Iran: 'Asia/Tehran', + Israel: 'Asia/Jerusalem', + Jamaica: 'America/Jamaica', + Japan: 'Asia/Tokyo', + Kwajalein: 'Pacific/Kwajalein', + Libya: 'Africa/Tripoli', + 'Mexico/BajaNorte': 'America/Tijuana', + 'Mexico/BajaSur': 'America/Mazatlan', + 'Mexico/General': 'America/Mexico_City', + NZ: 'Pacific/Auckland', + 'NZ-CHAT': 'Pacific/Chatham', + Navajo: 'America/Denver', + PRC: 'Asia/Shanghai', + 'Pacific/Enderbury': 'Pacific/Kanton', + 'Pacific/Johnston': 'Pacific/Honolulu', + 'Pacific/Ponape': 'Pacific/Guadalcanal', + 'Pacific/Samoa': 'Pacific/Pago_Pago', + 'Pacific/Truk': 'Pacific/Port_Moresby', + 'Pacific/Yap': 'Pacific/Port_Moresby', + Poland: 'Europe/Warsaw', + Portugal: 'Europe/Lisbon', + ROC: 'Asia/Taipei', + ROK: 'Asia/Seoul', + Singapore: 'Asia/Singapore', + Turkey: 'Europe/Istanbul', + 'US/Alaska': 'America/Anchorage', + 'US/Aleutian': 'America/Adak', + 'US/Arizona': 'America/Phoenix', + 'US/Central': 'America/Chicago', + 'US/East-Indiana': 'America/Indiana/Indianapolis', + 'US/Eastern': 'America/New_York', + 'US/Hawaii': 'Pacific/Honolulu', + 'US/Indiana-Starke': 'America/Indiana/Knox', + 'US/Michigan': 'America/Detroit', + 'US/Mountain': 'America/Denver', + 'US/Pacific': 'America/Los_Angeles', + 'US/Samoa': 'Pacific/Pago_Pago', + 'W-SU': 'Europe/Moscow', +}; + +export {timezoneBackwardMap}; + +export default TIMEZONES; diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 656694a785a3..a5160a13f8e9 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -249,7 +249,6 @@ function AddPlaidBankAccount({ height={iconSize} width={iconSize} additionalStyles={iconStyles} - fill={theme.icon} /> {bankName}
diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 31b04a3d954f..9f2635633318 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -349,6 +349,7 @@ function AddressSearch({ lat: successData.coords.latitude, lng: successData.coords.longitude, address: CONST.YOUR_LOCATION_TEXT, + name: CONST.YOUR_LOCATION_TEXT, }; onPress(location); }, diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js deleted file mode 100644 index 25e1ce6f05ec..000000000000 --- a/src/components/AmountTextInput.js +++ /dev/null @@ -1,89 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; -import refPropTypes from './refPropTypes'; -import TextInput from './TextInput'; - -const propTypes = { - /** Formatted amount in local currency */ - formattedAmount: PropTypes.string.isRequired, - - /** A ref to forward to amount text input */ - forwardedRef: refPropTypes, - - /** Function to call when amount in text input is changed */ - onChangeAmount: PropTypes.func.isRequired, - - /** Placeholder value for amount text input */ - placeholder: PropTypes.string.isRequired, - - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - - /** Function to call when selection in text input is changed */ - onSelectionChange: PropTypes.func, - - /** Style for the input */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** Style for the container */ - containerStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** Function to call to handle key presses in the text input */ - onKeyPress: PropTypes.func, -}; - -const defaultProps = { - forwardedRef: undefined, - selection: undefined, - onSelectionChange: () => {}, - onKeyPress: () => {}, - style: {}, - containerStyles: {}, -}; - -function AmountTextInput(props) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - return ( - - ); -} - -AmountTextInput.propTypes = propTypes; -AmountTextInput.defaultProps = defaultProps; -AmountTextInput.displayName = 'AmountTextInput'; - -const AmountTextInputWithRef = React.forwardRef((props, ref) => ( - -)); - -AmountTextInputWithRef.displayName = 'AmountTextInputWithRef'; - -export default AmountTextInputWithRef; diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx new file mode 100644 index 000000000000..0f3416076cc0 --- /dev/null +++ b/src/components/AmountTextInput.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type {TextSelection} from './Composer/types'; +import TextInput from './TextInput'; +import type {BaseTextInputRef} from './TextInput/BaseTextInput/types'; + +type AmountTextInputProps = { + /** Formatted amount in local currency */ + formattedAmount: string; + + /** Function to call when amount in text input is changed */ + onChangeAmount: (amount: string) => void; + + /** Placeholder value for amount text input */ + placeholder: string; + + /** Selection Object */ + selection?: TextSelection; + + /** Function to call when selection in text input is changed */ + onSelectionChange?: () => void; + + /** Style for the input */ + style?: StyleProp; + + /** Style for the container */ + touchableInputWrapperStyle?: StyleProp; + + /** Function to call to handle key presses in the text input */ + onKeyPress?: () => void; +}; + +function AmountTextInput( + {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress}: AmountTextInputProps, + ref: BaseTextInputRef, +) { + const styles = useThemeStyles(); + return ( + + ); +} + +AmountTextInput.displayName = 'AmountTextInput'; + +export default React.forwardRef(AmountTextInput); diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index 1bcdbb383f7a..bb3792f59d9f 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -1,6 +1,7 @@ import Str from 'expensify-common/lib/str'; import React, {useEffect, useRef} from 'react'; -import {Text as RNText, StyleSheet} from 'react-native'; +import type {Text as RNText} from 'react-native'; +import {StyleSheet} from 'react-native'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; diff --git a/src/components/AnchorForCommentsOnly/types.ts b/src/components/AnchorForCommentsOnly/types.ts index 005364c2077f..eea8571f5277 100644 --- a/src/components/AnchorForCommentsOnly/types.ts +++ b/src/components/AnchorForCommentsOnly/types.ts @@ -1,5 +1,5 @@ -import {StyleProp, TextStyle} from 'react-native'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {StyleProp, TextStyle} from 'react-native'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; type AnchorForCommentsOnlyProps = ChildrenProps & { /** The URL to open */ diff --git a/src/components/AnimatedStep/AnimatedStepContext.ts b/src/components/AnimatedStep/AnimatedStepContext.ts index 3b4c5f79a34f..14dba4b27cc2 100644 --- a/src/components/AnimatedStep/AnimatedStepContext.ts +++ b/src/components/AnimatedStep/AnimatedStepContext.ts @@ -1,6 +1,7 @@ -import React, {createContext} from 'react'; -import {ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; +import type React from 'react'; +import {createContext} from 'react'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; type AnimationDirection = ValueOf; diff --git a/src/components/AnimatedStep/AnimatedStepProvider.tsx b/src/components/AnimatedStep/AnimatedStepProvider.tsx index 53b3a0e0a53d..ea268e1d52cb 100644 --- a/src/components/AnimatedStep/AnimatedStepProvider.tsx +++ b/src/components/AnimatedStep/AnimatedStepProvider.tsx @@ -1,7 +1,8 @@ import React, {useMemo, useState} from 'react'; import CONST from '@src/CONST'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; -import AnimatedStepContext, {AnimationDirection} from './AnimatedStepContext'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {AnimationDirection} from './AnimatedStepContext'; +import AnimatedStepContext from './AnimatedStepContext'; function AnimatedStepProvider({children}: ChildrenProps): React.ReactNode { const [animationDirection, setAnimationDirection] = useState(CONST.ANIMATION_DIRECTION.IN); diff --git a/src/components/AnimatedStep/index.tsx b/src/components/AnimatedStep/index.tsx index e2b9952c0617..66d2108ca5d8 100644 --- a/src/components/AnimatedStep/index.tsx +++ b/src/components/AnimatedStep/index.tsx @@ -1,11 +1,11 @@ import React, {useMemo} from 'react'; -import {StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import * as Animatable from 'react-native-animatable'; import useThemeStyles from '@hooks/useThemeStyles'; import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; -import {AnimationDirection} from './AnimatedStepContext'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {AnimationDirection} from './AnimatedStepContext'; type AnimatedStepProps = ChildrenProps & { /** Styles to be assigned to Container */ diff --git a/src/components/AnimatedStep/useAnimatedStepContext.ts b/src/components/AnimatedStep/useAnimatedStepContext.ts index 3edc71e5289e..2adde8fd576e 100644 --- a/src/components/AnimatedStep/useAnimatedStepContext.ts +++ b/src/components/AnimatedStep/useAnimatedStepContext.ts @@ -1,5 +1,6 @@ import {useContext} from 'react'; -import AnimatedStepContext, {StepContext} from './AnimatedStepContext'; +import type {StepContext} from './AnimatedStepContext'; +import AnimatedStepContext from './AnimatedStepContext'; function useAnimatedStepContext(): StepContext { const context = useContext(AnimatedStepContext); diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index 877ca9444661..ad79e316baf3 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -1,11 +1,11 @@ import React from 'react'; import {Text, View} from 'react-native'; -import {OnyxCollection} from 'react-native-onyx'; -import {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Session from '@userActions/Session'; -import {PersonalDetails, Report} from '@src/types/onyx'; +import type {PersonalDetails, Report} from '@src/types/onyx'; import AvatarWithDisplayName from './AvatarWithDisplayName'; import Button from './Button'; import ExpensifyWordmark from './ExpensifyWordmark'; diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 2dae84106971..083c8340baa6 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -1,6 +1,7 @@ import lodashEscape from 'lodash/escape'; import React from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 51912c04eb31..1b4d350f7d4f 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -19,7 +19,6 @@ import fileDownload from '@libs/fileDownload'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import useNativeDriver from '@libs/useNativeDriver'; import reportPropTypes from '@pages/reportPropTypes'; @@ -95,6 +94,9 @@ const propTypes = { /** Whether it is a receipt attachment or not */ isReceiptAttachment: PropTypes.bool, + + /** Whether the receipt can be replaced */ + canEditReceipt: PropTypes.bool, }; const defaultProps = { @@ -113,6 +115,7 @@ const defaultProps = { onCarouselAttachmentChange: () => {}, isWorkspaceAvatar: false, isReceiptAttachment: false, + canEditReceipt: false, }; function AttachmentModal(props) { @@ -126,7 +129,7 @@ function AttachmentModal(props) { const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired); const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); - const [source, setSource] = useState(props.source); + const [source, setSource] = useState(() => props.source); const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1)); @@ -359,26 +362,22 @@ function AttachmentModal(props) { }, []); useEffect(() => { - setSource(props.source); + setSource(() => props.source); }, [props.source]); useEffect(() => { setIsAuthTokenRequired(props.isAuthTokenRequired); }, [props.isAuthTokenRequired]); - const sourceForAttachmentView = props.source || source; + const sourceForAttachmentView = source || props.source; const threeDotsMenuItems = useMemo(() => { if (!props.isReceiptAttachment || !props.parentReport || !props.parentReportActions) { return []; } - const menuItems = []; - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; - const canEdit = - ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT, props.transaction) && - !TransactionUtils.isDistanceRequest(props.transaction); - if (canEdit) { + const menuItems = []; + if (props.canEditReceipt) { menuItems.push({ icon: Expensicons.Camera, text: props.translate('common.replace'), @@ -393,7 +392,7 @@ function AttachmentModal(props) { text: props.translate('common.download'), onSelected: () => downloadAttachment(source), }); - if (TransactionUtils.hasReceipt(props.transaction) && !TransactionUtils.isReceiptBeingScanned(props.transaction) && canEdit) { + if (TransactionUtils.hasReceipt(props.transaction) && !TransactionUtils.isReceiptBeingScanned(props.transaction) && props.canEditReceipt) { menuItems.push({ icon: Expensicons.Trashcan, text: props.translate('receipt.deleteReceipt'), diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index b72662f989dc..fc3bf4659bd7 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,6 +1,7 @@ import {FlashList} from '@shopify/flash-list'; -import React, {ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useMemo, useRef} from 'react'; -import {View} from 'react-native'; +import type {ForwardedRef, ReactElement} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react'; +import type {View} from 'react-native'; // We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another import {ScrollView} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 9130f5139d71..61d614dcf2e4 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -1,4 +1,4 @@ -import {ReactElement} from 'react'; +import type {ReactElement} from 'react'; type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; diff --git a/src/components/AutoUpdateTime.tsx b/src/components/AutoUpdateTime.tsx index 258bdb281eb2..f6be32fd3970 100644 --- a/src/components/AutoUpdateTime.tsx +++ b/src/components/AutoUpdateTime.tsx @@ -6,9 +6,10 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; -import {Timezone} from '@src/types/onyx/PersonalDetails'; +import type {Timezone} from '@src/types/onyx/PersonalDetails'; import Text from './Text'; -import withLocalize, {WithLocalizeProps} from './withLocalize'; +import type {WithLocalizeProps} from './withLocalize'; +import withLocalize from './withLocalize'; type AutoUpdateTimeProps = WithLocalizeProps & { /** Timezone of the user from their personal details */ diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 65b0b6c36061..4da91c2e7d19 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,14 +1,15 @@ import React, {useEffect, useState} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; -import {AvatarSource} from '@libs/UserUtils'; +import type {AvatarSource} from '@libs/UserUtils'; import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; -import {AvatarType} from '@src/types/onyx/OnyxCommon'; +import type {AvatarType} from '@src/types/onyx/OnyxCommon'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Image from './Image'; diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.js index ff91a654f5dd..92cbe3a4da04 100644 --- a/src/components/AvatarCropModal/ImageCropView.js +++ b/src/components/AvatarCropModal/ImageCropView.js @@ -90,7 +90,7 @@ function ImageCropView(props) { diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index b9bae33d7e23..4580f3b7e4d4 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,7 +1,8 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,7 +12,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; import ParentNavigationSubtitle from './ParentNavigationSubtitle'; diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index d766d4cb6f22..71193147c292 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -322,7 +322,7 @@ function AvatarWithImagePicker({ src={Expensicons.Camera} width={variables.iconSizeSmall} height={variables.iconSizeSmall} - fill={theme.textLight} + fill={theme.icon} /> diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 70aebc30ee83..5be33e6ff2ec 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -1,5 +1,6 @@ import React, {useCallback} from 'react'; -import {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index d57257efab49..56fe7c4d0b42 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -1,5 +1,6 @@ import React, {memo} from 'react'; -import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; diff --git a/src/components/BaseMiniContextMenuItem.tsx b/src/components/BaseMiniContextMenuItem.tsx index 8d115a37cba7..7bed44cd8f13 100644 --- a/src/components/BaseMiniContextMenuItem.tsx +++ b/src/components/BaseMiniContextMenuItem.tsx @@ -1,11 +1,14 @@ -import React, {ForwardedRef} from 'react'; -import {PressableStateCallbackType, View} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React from 'react'; +import type {PressableStateCallbackType} from 'react-native'; +import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import DomUtils from '@libs/DomUtils'; import getButtonState from '@libs/getButtonState'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; @@ -64,6 +67,7 @@ function BaseMiniContextMenuItem({tooltipText, onPress, children, isDelayButtonS event.preventDefault(); }} accessibilityLabel={tooltipText} + role={CONST.ROLE.BUTTON} style={({hovered, pressed}) => [ styles.reportActionContextMenuMiniButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, isDelayButtonStateComplete)), diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx index 3a038c58d886..edec30604b88 100644 --- a/src/components/BlockingViews/BlockingView.tsx +++ b/src/components/BlockingViews/BlockingView.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import {ImageSourcePropType, View} from 'react-native'; -import {SvgProps} from 'react-native-svg'; +import type {ImageSourcePropType} from 'react-native'; +import {View} from 'react-native'; +import type {SvgProps} from 'react-native-svg'; import AutoEmailLink from '@components/AutoEmailLink'; import Icon from '@components/Icon'; import Text from '@components/Text'; @@ -9,7 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; -import {TranslationPaths} from '@src/languages/types'; +import type {TranslationPaths} from '@src/languages/types'; type BlockingViewProps = { /** Expensicon for the page */ diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 6d7f838bf6c2..5993e60861f5 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -6,7 +6,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; -import {TranslationPaths} from '@src/languages/types'; +import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import BlockingView from './BlockingView'; diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx index a9ebcf969ae5..787752dd4e72 100644 --- a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx +++ b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx @@ -3,7 +3,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import BlockingView from './BlockingView'; function FullPageOfflineBlockingView({children}: ChildrenProps) { diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index fa761218b4a0..6af3a4c6d477 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import LogoComponent from '@assets/images/expensify-wordmark.svg'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index cdafd0b0b93b..fb72f0cc845f 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -1,6 +1,8 @@ import {useIsFocused} from '@react-navigation/native'; -import React, {ForwardedRef, useCallback} from 'react'; -import {ActivityIndicator, GestureResponderEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React, {useCallback} from 'react'; +import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; @@ -12,8 +14,8 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import HapticFeedback from '@libs/HapticFeedback'; import CONST from '@src/CONST'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; -import IconAsset from '@src/types/utils/IconAsset'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type IconAsset from '@src/types/utils/IconAsset'; import validateSubmitShortcut from './validateSubmitShortcut'; type ButtonWithText = { @@ -169,16 +171,16 @@ function Button( const keyboardShortcutCallback = useCallback( (event?: GestureResponderEvent | KeyboardEvent) => { - if (!validateSubmitShortcut(isFocused, isDisabled, isLoading, event)) { + if (!validateSubmitShortcut(isDisabled, isLoading, event)) { return; } onPress(); }, - [isDisabled, isFocused, isLoading, onPress], + [isDisabled, isLoading, onPress], ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, keyboardShortcutCallback, { - isActive: pressOnEnter && !shouldDisableEnterShortcut, + isActive: pressOnEnter && !shouldDisableEnterShortcut && isFocused, shouldBubble: allowBubble, priority: enterKeyEventListenerPriority, shouldPreventDefault: false, @@ -221,7 +223,7 @@ function Button( @@ -232,7 +234,7 @@ function Button( diff --git a/src/components/Button/validateSubmitShortcut/index.native.ts b/src/components/Button/validateSubmitShortcut/index.native.ts index 7687855f109b..4602b40c832f 100644 --- a/src/components/Button/validateSubmitShortcut/index.native.ts +++ b/src/components/Button/validateSubmitShortcut/index.native.ts @@ -1,16 +1,15 @@ -import ValidateSubmitShortcut from './types'; +import type ValidateSubmitShortcut from './types'; /** * Validate if the submit shortcut should be triggered depending on the button state * - * @param isFocused Whether Button is on active screen * @param isDisabled Indicates whether the button should be disabled * @param isLoading Indicates whether the button should be disabled and in the loading state * @return Returns `true` if the shortcut should be triggered */ -const validateSubmitShortcut: ValidateSubmitShortcut = (isFocused, isDisabled, isLoading) => { - if (!isFocused || isDisabled || isLoading) { +const validateSubmitShortcut: ValidateSubmitShortcut = (isDisabled, isLoading) => { + if (isDisabled || isLoading) { return false; } diff --git a/src/components/Button/validateSubmitShortcut/index.ts b/src/components/Button/validateSubmitShortcut/index.ts index 55b3e44192e4..f8cea44f73d6 100644 --- a/src/components/Button/validateSubmitShortcut/index.ts +++ b/src/components/Button/validateSubmitShortcut/index.ts @@ -1,18 +1,17 @@ -import ValidateSubmitShortcut from './types'; +import type ValidateSubmitShortcut from './types'; /** * Validate if the submit shortcut should be triggered depending on the button state * - * @param isFocused Whether Button is on active screen * @param isDisabled Indicates whether the button should be disabled * @param isLoading Indicates whether the button should be disabled and in the loading state * @param event Focused input event * @returns Returns `true` if the shortcut should be triggered */ -const validateSubmitShortcut: ValidateSubmitShortcut = (isFocused, isDisabled, isLoading, event) => { +const validateSubmitShortcut: ValidateSubmitShortcut = (isDisabled, isLoading, event) => { const eventTarget = event?.target as HTMLElement; - if (!isFocused || isDisabled || isLoading || eventTarget.nodeName === 'TEXTAREA') { + if (isDisabled || isLoading || eventTarget.nodeName === 'TEXTAREA') { return false; } diff --git a/src/components/Button/validateSubmitShortcut/types.ts b/src/components/Button/validateSubmitShortcut/types.ts index 9970e1478a4c..d1ff24fb0510 100644 --- a/src/components/Button/validateSubmitShortcut/types.ts +++ b/src/components/Button/validateSubmitShortcut/types.ts @@ -1,5 +1,5 @@ -import {GestureResponderEvent} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; -type ValidateSubmitShortcut = (isFocused: boolean, isDisabled: boolean, isLoading: boolean, event?: GestureResponderEvent | KeyboardEvent) => boolean; +type ValidateSubmitShortcut = (isDisabled: boolean, isLoading: boolean, event?: GestureResponderEvent | KeyboardEvent) => boolean; export default ValidateSubmitShortcut; diff --git a/src/components/CardPreview.tsx b/src/components/CardPreview.tsx index 2196e6f441bb..3ac56d6b26a8 100644 --- a/src/components/CardPreview.tsx +++ b/src/components/CardPreview.tsx @@ -1,12 +1,13 @@ import React from 'react'; import {View} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import ExpensifyCardImage from '@assets/images/expensify-card.svg'; import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PrivatePersonalDetails, Session} from '@src/types/onyx'; +import type {PrivatePersonalDetails, Session} from '@src/types/onyx'; import ImageSVG from './ImageSVG'; import Text from './Text'; diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index ac18b550501d..7e7720b57a6e 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,10 +1,12 @@ -import React, {type ForwardedRef, forwardRef, type MouseEventHandler, type KeyboardEvent as ReactKeyboardEvent} from 'react'; -import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; +import React, {forwardRef} from 'react'; +import type {ForwardedRef, MouseEventHandler, KeyboardEvent as ReactKeyboardEvent} from 'react'; +import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index 9660c9e1a2e5..602fb154deba 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -1,5 +1,7 @@ -import React, {ComponentType, ForwardedRef, useState} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {ComponentType, ForwardedRef} from 'react'; +import React, {useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import Checkbox from './Checkbox'; @@ -40,7 +42,7 @@ type CheckboxWithLabelProps = RequiredLabelProps & { /** Error text to display */ errorText?: string; - /** Value for checkbox. This prop is intended to be set by Form.js only */ + /** Value for checkbox. This prop is intended to be set by FormProvider only */ value?: boolean; /** The default value for the checkbox */ diff --git a/src/components/CollapsibleSection/Collapsible/index.native.tsx b/src/components/CollapsibleSection/Collapsible/index.native.tsx index e8d3dc9439d0..63cb427f845f 100644 --- a/src/components/CollapsibleSection/Collapsible/index.native.tsx +++ b/src/components/CollapsibleSection/Collapsible/index.native.tsx @@ -1,6 +1,6 @@ import React from 'react'; import CollapsibleRN from 'react-native-collapsible'; -import CollapsibleProps from './types'; +import type CollapsibleProps from './types'; function Collapsible({isOpened = false, children}: CollapsibleProps) { return {children}; diff --git a/src/components/CollapsibleSection/Collapsible/index.tsx b/src/components/CollapsibleSection/Collapsible/index.tsx index 2585fd92f42b..5bf31c10ee68 100644 --- a/src/components/CollapsibleSection/Collapsible/index.tsx +++ b/src/components/CollapsibleSection/Collapsible/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {Collapse} from 'react-collapse'; -import CollapsibleProps from './types'; +import type CollapsibleProps from './types'; function Collapsible({isOpened = false, children}: CollapsibleProps) { return {children}; diff --git a/src/components/CollapsibleSection/Collapsible/types.ts b/src/components/CollapsibleSection/Collapsible/types.ts index 8b8e8aba6860..a60b8d79f935 100644 --- a/src/components/CollapsibleSection/Collapsible/types.ts +++ b/src/components/CollapsibleSection/Collapsible/types.ts @@ -1,4 +1,4 @@ -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; type CollapsibleProps = ChildrenProps & { /** Whether the section should start expanded. False by default */ diff --git a/src/components/CollapsibleSection/index.tsx b/src/components/CollapsibleSection/index.tsx index 18b24269b773..393a48e85616 100644 --- a/src/components/CollapsibleSection/index.tsx +++ b/src/components/CollapsibleSection/index.tsx @@ -7,7 +7,7 @@ import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import Collapsible from './Collapsible'; type CollapsibleSectionProps = ChildrenProps & { diff --git a/src/components/ComposeProviders.tsx b/src/components/ComposeProviders.tsx index 2c73719358d8..8fc487c78251 100644 --- a/src/components/ComposeProviders.tsx +++ b/src/components/ComposeProviders.tsx @@ -1,5 +1,6 @@ -import React, {ComponentType, ReactNode} from 'react'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {ComponentType, ReactNode} from 'react'; +import React from 'react'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; type ComposeProvidersProps = ChildrenProps & { /** Provider components go here */ diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx index 46c2a5f06ded..d60a41e0f263 100644 --- a/src/components/Composer/index.android.tsx +++ b/src/components/Composer/index.android.tsx @@ -1,10 +1,12 @@ -import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef} from 'react'; -import {StyleSheet, TextInput} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import type {TextInput} from 'react-native'; +import {StyleSheet} from 'react-native'; import RNTextInput from '@components/RNTextInput'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ComposerUtils from '@libs/ComposerUtils'; -import {ComposerProps} from './types'; +import type {ComposerProps} from './types'; function Composer( { diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx index 240dfabded0b..b1357fef9a46 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.ios.tsx @@ -1,10 +1,12 @@ -import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef} from 'react'; -import {StyleSheet, TextInput} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import type {TextInput} from 'react-native'; +import {StyleSheet} from 'react-native'; import RNTextInput from '@components/RNTextInput'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ComposerUtils from '@libs/ComposerUtils'; -import {ComposerProps} from './types'; +import type {ComposerProps} from './types'; function Composer( { diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 4ff5c6dbd75f..19b7bb6bb30a 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -1,9 +1,11 @@ import {useNavigation} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {BaseSyntheticEvent, ForwardedRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {BaseSyntheticEvent, ForwardedRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; -import {DimensionValue, NativeSyntheticEvent, Text as RNText, StyleSheet, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData, View} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; +import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; +import {StyleSheet, View} from 'react-native'; +import type {AnimatedProps} from 'react-native-reanimated'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; @@ -17,7 +19,7 @@ import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullCompo import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; -import {ComposerProps} from './types'; +import type {ComposerProps} from './types'; /** * Retrieves the characters from the specified cursor position up to the next space or new line. diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index cc0654b68019..bfdcb6715d40 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,4 +1,4 @@ -import {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; +import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; type TextSelection = { start: number; diff --git a/src/components/ConfirmContent.tsx b/src/components/ConfirmContent.tsx index 9e4ffa8720da..94146a2c2957 100644 --- a/src/components/ConfirmContent.tsx +++ b/src/components/ConfirmContent.tsx @@ -1,11 +1,13 @@ -import React, {ReactNode} from 'react'; -import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {ReactNode} from 'react'; +import React from 'react'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; -import IconAsset from '@src/types/utils/IconAsset'; +import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import Header from './Header'; import Icon from './Icon'; diff --git a/src/components/ConfirmPopover.js b/src/components/ConfirmPopover.js deleted file mode 100644 index 83001736b471..000000000000 --- a/src/components/ConfirmPopover.js +++ /dev/null @@ -1,85 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ConfirmContent from './ConfirmContent'; -import Popover from './Popover'; -import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; - -const propTypes = { - /** Title of the modal */ - title: PropTypes.string.isRequired, - - /** A callback to call when the form has been submitted */ - onConfirm: PropTypes.func.isRequired, - - /** A callback to call when the form has been closed */ - onCancel: PropTypes.func, - - /** Modal visibility */ - isVisible: PropTypes.bool.isRequired, - - /** Confirm button text */ - confirmText: PropTypes.string, - - /** Cancel button text */ - cancelText: PropTypes.string, - - /** Is the action destructive */ - danger: PropTypes.bool, - - /** Whether we should show the cancel button */ - shouldShowCancelButton: PropTypes.bool, - - /** Modal content text/element */ - prompt: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - - /** Where the popover should be positioned */ - anchorPosition: PropTypes.shape({ - top: PropTypes.number, - left: PropTypes.number, - }).isRequired, - - /** Styles for view */ - // eslint-disable-next-line react/forbid-prop-types - contentStyles: PropTypes.arrayOf(PropTypes.object), - - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - confirmText: '', - cancelText: '', - danger: false, - onCancel: () => {}, - shouldShowCancelButton: true, - prompt: '', - contentStyles: [], -}; - -function ConfirmPopover(props) { - return ( - - - - ); -} - -ConfirmPopover.propTypes = propTypes; -ConfirmPopover.defaultProps = defaultProps; -ConfirmPopover.displayName = 'ConfirmPopover'; -export default withWindowDimensions(ConfirmPopover); diff --git a/src/components/ConfirmationPage.tsx b/src/components/ConfirmationPage.tsx index 21813edd693d..fe4036a4435f 100644 --- a/src/components/ConfirmationPage.tsx +++ b/src/components/ConfirmationPage.tsx @@ -5,7 +5,7 @@ import Button from './Button'; import FixedFooter from './FixedFooter'; import Lottie from './Lottie'; import LottieAnimations from './LottieAnimations'; -import DotLottieAnimation from './LottieAnimations/types'; +import type DotLottieAnimation from './LottieAnimations/types'; import Text from './Text'; type ConfirmationPageProps = { diff --git a/src/components/CopyTextToClipboard.tsx b/src/components/CopyTextToClipboard.tsx index 6f3b42e88fee..3c99a85fd0e7 100644 --- a/src/components/CopyTextToClipboard.tsx +++ b/src/components/CopyTextToClipboard.tsx @@ -1,5 +1,5 @@ import React, {useCallback} from 'react'; -import {AccessibilityRole, StyleProp, TextStyle} from 'react-native'; +import type {AccessibilityRole, StyleProp, TextStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import * as Expensicons from './Icon/Expensicons'; diff --git a/src/components/CurrencySymbolButton.js b/src/components/CurrencySymbolButton.tsx similarity index 83% rename from src/components/CurrencySymbolButton.js rename to src/components/CurrencySymbolButton.tsx index d03834fc1fd6..18955bb0b391 100644 --- a/src/components/CurrencySymbolButton.js +++ b/src/components/CurrencySymbolButton.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -7,15 +6,15 @@ import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Text from './Text'; import Tooltip from './Tooltip'; -const propTypes = { +type CurrencySymbolButtonProps = { /** Currency symbol of selected currency */ - currencySymbol: PropTypes.string.isRequired, + currencySymbol: string; /** Function to call when currency button is pressed */ - onCurrencyButtonPress: PropTypes.func.isRequired, + onCurrencyButtonPress: () => void; }; -function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol}) { +function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol}: CurrencySymbolButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); return ( @@ -31,7 +30,6 @@ function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol}) { ); } -CurrencySymbolButton.propTypes = propTypes; CurrencySymbolButton.displayName = 'CurrencySymbolButton'; export default CurrencySymbolButton; diff --git a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx index 02c308705994..367e54e8be64 100644 --- a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx +++ b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {View} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; -import {ValueOf} from 'type-fest'; +import type {ValueOf} from 'type-fest'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; diff --git a/src/components/CurrentWalletBalance.tsx b/src/components/CurrentWalletBalance.tsx index 28a83fb1ae50..8761b50465f3 100644 --- a/src/components/CurrentWalletBalance.tsx +++ b/src/components/CurrentWalletBalance.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import {StyleProp, TextStyle} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {StyleProp, TextStyle} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import UserWallet from '@src/types/onyx/UserWallet'; +import type UserWallet from '@src/types/onyx/UserWallet'; import Text from './Text'; type CurrentWalletBalanceOnyxProps = { diff --git a/src/components/CustomDevMenu/index.native.tsx b/src/components/CustomDevMenu/index.native.tsx index d8a0ea987171..54f1336b4fef 100644 --- a/src/components/CustomDevMenu/index.native.tsx +++ b/src/components/CustomDevMenu/index.native.tsx @@ -1,7 +1,7 @@ import {useEffect} from 'react'; import DevMenu from 'react-native-dev-menu'; import toggleTestToolsModal from '@userActions/TestTool'; -import CustomDevMenuElement from './types'; +import type CustomDevMenuElement from './types'; const CustomDevMenu: CustomDevMenuElement = Object.assign( () => { diff --git a/src/components/CustomDevMenu/index.tsx b/src/components/CustomDevMenu/index.tsx index c8eae861b676..4306d0cae090 100644 --- a/src/components/CustomDevMenu/index.tsx +++ b/src/components/CustomDevMenu/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import CustomDevMenuElement from './types'; +import type CustomDevMenuElement from './types'; const CustomDevMenu: CustomDevMenuElement = Object.assign(() => <>, {displayName: 'CustomDevMenu'}); diff --git a/src/components/CustomDevMenu/types.ts b/src/components/CustomDevMenu/types.ts index bdfc800a17f0..17e2b30ef4f6 100644 --- a/src/components/CustomDevMenu/types.ts +++ b/src/components/CustomDevMenu/types.ts @@ -1,4 +1,4 @@ -import {ReactElement} from 'react'; +import type {ReactElement} from 'react'; type CustomDevMenuElement = { (): ReactElement; diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index b84f9c6a6630..f66a0204ac5e 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -1,4 +1,5 @@ import React, {useCallback, useContext, useEffect, useRef, useState} from 'react'; +import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; import {navigationRef} from '@libs/Navigation/Navigation'; import StatusBar from '@libs/StatusBar'; @@ -33,7 +34,27 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack }; }, [disableRootStatusBar, isNested]); + const prevStatusBarBackgroundColor = useRef(theme.appBG); + const statusBarBackgroundColor = useRef(theme.appBG); + const statusBarAnimation = useSharedValue(0); + + useAnimatedReaction( + () => statusBarAnimation.value, + (current, previous) => { + // Do not run if either of the animated value is null + // or previous animated value is greater than or equal to the current one + if (previous === null || current === null || current <= previous) { + return; + } + const backgroundColor = interpolateColor(statusBarAnimation.value, [0, 1], [prevStatusBarBackgroundColor.current, statusBarBackgroundColor.current]); + runOnJS(updateStatusBarAppearance)({backgroundColor}); + }, + ); + const listenerCount = useRef(0); + + // 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) => { // Check if this function is either called through the current navigation listener or the general useEffect which listens for theme changes. @@ -49,27 +70,40 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack currentRoute = navigationRef.getCurrentRoute(); } - let currentScreenBackgroundColor = theme.appBG; let newStatusBarStyle = theme.statusBarStyle; + let currentScreenBackgroundColor = theme.appBG; if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_THEMES) { - const screenTheme = theme.PAGE_THEMES[currentRoute.name]; - currentScreenBackgroundColor = screenTheme.backgroundColor; - newStatusBarStyle = screenTheme.statusBarStyle; + const pageTheme = theme.PAGE_THEMES[currentRoute.name]; + + newStatusBarStyle = pageTheme.statusBarStyle; + + const backgroundColorFromRoute = + currentRoute?.params && 'backgroundColor' in currentRoute.params && typeof currentRoute.params.backgroundColor === 'string' && currentRoute.params.backgroundColor; + + // It's possible for backgroundColorFromRoute to be empty string, so we must use "||" to fallback to backgroundColorFallback. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + currentScreenBackgroundColor = backgroundColorFromRoute || pageTheme.backgroundColor; + } + + prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current; + statusBarBackgroundColor.current = currentScreenBackgroundColor; + + if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.current !== theme.appBG) { + statusBarAnimation.value = 0; + statusBarAnimation.value = withDelay(300, withTiming(1)); } // Don't update the status bar style if it's the same as the current one, to prevent flashing. - if (newStatusBarStyle === statusBarStyle) { - updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor}); - } else { - updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor, statusBarStyle: newStatusBarStyle}); + if (newStatusBarStyle !== statusBarStyle) { + updateStatusBarAppearance({statusBarStyle: newStatusBarStyle}); setStatusBarStyle(newStatusBarStyle); } }, - [statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], + [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 properyl + // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properly useEffect(() => { if (isDisabled) { return; @@ -82,15 +116,6 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack return () => navigationRef.removeListener('state', listener); }, [isDisabled, theme.appBG, updateStatusBarStyle]); - // Update the status bar style everytime the theme changes - useEffect(() => { - if (isDisabled) { - return; - } - - updateStatusBarStyle(); - }, [isDisabled, theme, updateStatusBarStyle]); - // Update the global background (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. useEffect(() => { diff --git a/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.website.ts b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.website.ts index 481d866dbe4f..f92016048bde 100644 --- a/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.website.ts +++ b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.website.ts @@ -1,4 +1,4 @@ -import UpdateGlobalBackgroundColor from './types'; +import type UpdateGlobalBackgroundColor from './types'; const updateGlobalBackgroundColor: UpdateGlobalBackgroundColor = (theme) => { const htmlElement = document.getElementsByTagName('html')[0]; diff --git a/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/types.ts b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/types.ts index 83bd36a9428a..078703e3846d 100644 --- a/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/types.ts +++ b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/types.ts @@ -1,4 +1,4 @@ -import {ThemeColors} from '@styles/theme/types'; +import type {ThemeColors} from '@styles/theme/types'; type UpdateGlobalBackgroundColor = (theme: ThemeColors) => void; diff --git a/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.android.ts b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.android.ts index b7651d4549de..c25f45bfb050 100644 --- a/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.android.ts +++ b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.android.ts @@ -1,5 +1,5 @@ import StatusBar from '@libs/StatusBar'; -import UpdateStatusBarAppearanceProps from './types'; +import type UpdateStatusBarAppearanceProps from './types'; // eslint-disable-next-line @typescript-eslint/naming-convention export default function updateStatusBarAppearance({statusBarStyle}: UpdateStatusBarAppearanceProps) { diff --git a/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ios.ts b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ios.ts index 61fcb056bba5..e887b5f0a98b 100644 --- a/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ios.ts +++ b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ios.ts @@ -1,5 +1,5 @@ import StatusBar from '@libs/StatusBar'; -import UpdateStatusBarAppearanceProps from './types'; +import type UpdateStatusBarAppearanceProps from './types'; // eslint-disable-next-line @typescript-eslint/naming-convention export default function updateStatusBarAppearance({statusBarStyle}: UpdateStatusBarAppearanceProps) { diff --git a/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ts b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ts index 574efd90f0b5..a11b3619f8d9 100644 --- a/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ts +++ b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ts @@ -1,5 +1,5 @@ import StatusBar from '@libs/StatusBar'; -import UpdateStatusBarAppearanceProps from './types'; +import type UpdateStatusBarAppearanceProps from './types'; export default function updateStatusBarAppearance({backgroundColor, statusBarStyle}: UpdateStatusBarAppearanceProps) { if (backgroundColor) { diff --git a/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/types.ts b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/types.ts index 823f0059eccf..775082187395 100644 --- a/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/types.ts +++ b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/types.ts @@ -1,4 +1,4 @@ -import {StatusBarStyle} from '@styles/index'; +import type {StatusBarStyle} from '@styles/index'; type UpdateStatusBarAppearanceProps = { backgroundColor?: string; diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js index 571ddc820d43..f10af5e4a5a7 100644 --- a/src/components/DatePicker/CalendarPicker/index.js +++ b/src/components/DatePicker/CalendarPicker/index.js @@ -112,14 +112,44 @@ class CalendarPicker extends React.PureComponent { * Handles the user pressing the previous month arrow of the calendar picker. */ moveToPrevMonth() { - this.setState((prev) => ({currentDateView: subMonths(new Date(prev.currentDateView), 1)})); + this.setState((prev) => { + const prevMonth = subMonths(new Date(prev.currentDateView), 1); + // if year is subtracted, we need to update the years list + let newYears = prev.years; + if (prevMonth.getFullYear() < prev.currentDateView.getFullYear()) { + newYears = _.map(prev.years, (item) => ({ + ...item, + isSelected: item.value === prevMonth.getFullYear(), + })); + } + + return { + currentDateView: prevMonth, + years: newYears, + }; + }); } /** * Handles the user pressing the next month arrow of the calendar picker. */ moveToNextMonth() { - this.setState((prev) => ({currentDateView: addMonths(new Date(prev.currentDateView), 1)})); + this.setState((prev) => { + const nextMonth = addMonths(new Date(prev.currentDateView), 1); + // if year is added, we need to update the years list + let newYears = prev.years; + if (nextMonth.getFullYear() > prev.currentDateView.getFullYear()) { + newYears = _.map(prev.years, (item) => ({ + ...item, + isSelected: item.value === nextMonth.getFullYear(), + })); + } + + return { + currentDateView: nextMonth, + years: newYears, + }; + }); } render() { diff --git a/src/components/DeeplinkWrapper/DeeplinkRedirectLoadingIndicator.tsx b/src/components/DeeplinkWrapper/DeeplinkRedirectLoadingIndicator.tsx index 0e8996bf5022..ed91b51a2a44 100644 --- a/src/components/DeeplinkWrapper/DeeplinkRedirectLoadingIndicator.tsx +++ b/src/components/DeeplinkWrapper/DeeplinkRedirectLoadingIndicator.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -12,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import * as OnyxTypes from '@src/types/onyx'; +import type * as OnyxTypes from '@src/types/onyx'; type DeeplinkRedirectLoadingIndicatorOnyxProps = { /** Current user session */ @@ -36,7 +37,6 @@ function DeeplinkRedirectLoadingIndicator({openLinkInBrowser, session}: Deeplink
diff --git a/src/components/DeeplinkWrapper/index.tsx b/src/components/DeeplinkWrapper/index.tsx index 4b0382bd6b14..076e6b91460d 100644 --- a/src/components/DeeplinkWrapper/index.tsx +++ b/src/components/DeeplinkWrapper/index.tsx @@ -1,4 +1,4 @@ -import DeeplinkWrapperProps from './types'; +import type DeeplinkWrapperProps from './types'; function DeeplinkWrapper({children}: DeeplinkWrapperProps) { return children; diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx index 2cae91e2f2a0..1d509666ec98 100644 --- a/src/components/DeeplinkWrapper/index.website.tsx +++ b/src/components/DeeplinkWrapper/index.website.tsx @@ -8,7 +8,7 @@ import * as App from '@userActions/App'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import DeeplinkWrapperProps from './types'; +import type DeeplinkWrapperProps from './types'; function isMacOSWeb(): boolean { return !Browser.isMobile() && typeof navigator === 'object' && typeof navigator.userAgent === 'string' && /Mac/i.test(navigator.userAgent) && !/Electron/i.test(navigator.userAgent); diff --git a/src/components/DeeplinkWrapper/types.ts b/src/components/DeeplinkWrapper/types.ts index dfd56b62573d..db61e5b01c24 100644 --- a/src/components/DeeplinkWrapper/types.ts +++ b/src/components/DeeplinkWrapper/types.ts @@ -1,4 +1,4 @@ -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; type DeeplinkWrapperProps = ChildrenProps & { /** User authentication status */ diff --git a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx index 440457d22965..9b3eaeb4e447 100644 --- a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx +++ b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx @@ -1,15 +1,16 @@ -import React, {RefObject, useCallback} from 'react'; -import {Text as RNText, StyleProp, TextStyle} from 'react-native'; +import type {RefObject} from 'react'; +import React, {useCallback} from 'react'; +import type {Text as RNText, StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; import useThemeStyles from '@hooks/useThemeStyles'; -import {AvatarSource} from '@libs/UserUtils'; +import type {AvatarSource} from '@libs/UserUtils'; type DisplayNamesTooltipItemProps = { index?: number; /** The function to get a distance to shift the tooltip horizontally */ - getTooltipShiftX?: (index: number) => number | undefined; + getTooltipShiftX?: (index: number) => number; /** The Account ID for the tooltip */ accountID?: number; @@ -32,7 +33,7 @@ type DisplayNamesTooltipItemProps = { function DisplayNamesTooltipItem({ index = 0, - getTooltipShiftX = () => undefined, + getTooltipShiftX = () => 0, accountID = 0, avatar = '', login = '', diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx index 43061ada9a94..f22b1f0c2209 100644 --- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx +++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx @@ -1,10 +1,11 @@ import React, {Fragment, useCallback, useRef} from 'react'; -import {Text as RNText, View} from 'react-native'; +import type {Text as RNText} from 'react-native'; +import {View} from 'react-native'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThemeStyles from '@hooks/useThemeStyles'; import DisplayNamesTooltipItem from './DisplayNamesTooltipItem'; -import DisplayNamesProps from './types'; +import type DisplayNamesProps from './types'; type HTMLElementWithText = HTMLElement & RNText; @@ -23,13 +24,13 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit * 2. Now we get the tooltip original position. * 3. If inline node's right edge is overflowing the container's right edge, we set the tooltip to the center * of the distance between the left edge of the inline node and right edge of the container. - * @param {Number} index Used to get the Ref to the node at the current index - * @returns {Number} Distance to shift the tooltip horizontally + * @param index Used to get the Ref to the node at the current index + * @returns Distance to shift the tooltip horizontally */ const getTooltipShiftX = useCallback((index: number) => { // Only shift the tooltip in case the containerLayout or Refs to the text node are available if (!containerRef.current || !childRefs.current[index]) { - return; + return 0; } const {width: containerWidth, left: containerLeft} = containerRef.current.getBoundingClientRect(); diff --git a/src/components/DisplayNames/DisplayNamesWithoutTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithoutTooltip.tsx index 761b0b66ee2c..177bdb6a9fc4 100644 --- a/src/components/DisplayNames/DisplayNamesWithoutTooltip.tsx +++ b/src/components/DisplayNames/DisplayNamesWithoutTooltip.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {StyleProp, TextStyle} from 'react-native'; +import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/components/DisplayNames/index.native.tsx b/src/components/DisplayNames/index.native.tsx index 8f1fef37a6ba..b3eceb794bcb 100644 --- a/src/components/DisplayNames/index.native.tsx +++ b/src/components/DisplayNames/index.native.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import DisplayNamesProps from './types'; +import type DisplayNamesProps from './types'; // As we don't have to show tooltips of the Native platform so we simply render the full display names list. function DisplayNames({accessibilityLabel, fullTitle, textStyles = [], numberOfLines = 1}: DisplayNamesProps) { diff --git a/src/components/DisplayNames/index.tsx b/src/components/DisplayNames/index.tsx index 7ff1081937d5..155193368cc5 100644 --- a/src/components/DisplayNames/index.tsx +++ b/src/components/DisplayNames/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import useLocalize from '@hooks/useLocalize'; import DisplayNamesWithoutTooltip from './DisplayNamesWithoutTooltip'; import DisplayNamesWithToolTip from './DisplayNamesWithTooltip'; -import DisplayNamesProps from './types'; +import type DisplayNamesProps from './types'; function DisplayNames({fullTitle, tooltipEnabled, textStyles, numberOfLines, shouldUseFullTitle, displayNamesWithTooltips}: DisplayNamesProps) { const {translate} = useLocalize(); diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index 5137d6f54108..b0959d43aa96 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -1,4 +1,4 @@ -import {StyleProp, TextStyle} from 'react-native'; +import type {StyleProp, TextStyle} from 'react-native'; import type {AvatarSource} from '@libs/UserUtils'; type DisplayNameWithTooltip = { diff --git a/src/components/DistanceMapView/index.android.tsx b/src/components/DistanceMapView/index.android.tsx index 38e92163c3eb..168a480c6100 100644 --- a/src/components/DistanceMapView/index.android.tsx +++ b/src/components/DistanceMapView/index.android.tsx @@ -6,7 +6,7 @@ import MapView from '@components/MapView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import DistanceMapViewProps from './types'; +import type DistanceMapViewProps from './types'; function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) { const styles = useThemeStyles(); diff --git a/src/components/DistanceMapView/index.tsx b/src/components/DistanceMapView/index.tsx index 2abdc29865b0..3f1b3b8439ed 100644 --- a/src/components/DistanceMapView/index.tsx +++ b/src/components/DistanceMapView/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import MapView from '@components/MapView'; -import DistanceMapViewProps from './types'; +import type DistanceMapViewProps from './types'; function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) { // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/DistanceMapView/types.ts b/src/components/DistanceMapView/types.ts index 5ab3dbd8238e..18213235445f 100644 --- a/src/components/DistanceMapView/types.ts +++ b/src/components/DistanceMapView/types.ts @@ -1,4 +1,4 @@ -import {StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import type {MapViewProps} from '@components/MapView/MapViewTypes'; type DistanceMapViewProps = MapViewProps & { diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index 65afe8c7e4eb..d18704fdfb05 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/no-array-index-key */ import React from 'react'; -import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -84,9 +85,9 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica key={i} style={styles.offlineFeedback.text} > - {Localize.translateLocal('iou.error.receiptFailureMessage')} - {Localize.translateLocal('iou.error.saveFileMessage')} - {Localize.translateLocal('iou.error.loseFileMessage')} + {Localize.translateLocal('iou.error.receiptFailureMessage')} + {Localize.translateLocal('iou.error.saveFileMessage')} + {Localize.translateLocal('iou.error.loseFileMessage')} ) : ( diff --git a/src/components/DragAndDrop/Consumer/types.ts b/src/components/DragAndDrop/Consumer/types.ts index 1f85f32a0153..03bdee3f23d8 100644 --- a/src/components/DragAndDrop/Consumer/types.ts +++ b/src/components/DragAndDrop/Consumer/types.ts @@ -1,4 +1,4 @@ -import {ReactNode} from 'react'; +import type {ReactNode} from 'react'; type DragAndDropConsumerProps = { /** Children to render inside this component. */ diff --git a/src/components/DragAndDrop/NoDropZone/types.ts b/src/components/DragAndDrop/NoDropZone/types.ts index 09715ecd942f..0735f4936ada 100644 --- a/src/components/DragAndDrop/NoDropZone/types.ts +++ b/src/components/DragAndDrop/NoDropZone/types.ts @@ -1,4 +1,4 @@ -import {ReactNode} from 'react'; +import type {ReactNode} from 'react'; type NoDropZoneProps = { /** Content */ diff --git a/src/components/DragAndDrop/Provider/types.ts b/src/components/DragAndDrop/Provider/types.ts index eae83d10682a..b4394056cac5 100644 --- a/src/components/DragAndDrop/Provider/types.ts +++ b/src/components/DragAndDrop/Provider/types.ts @@ -1,4 +1,4 @@ -import {ReactNode} from 'react'; +import type {ReactNode} from 'react'; type DragAndDropProviderProps = { /** Children to render inside this component. */ diff --git a/src/components/DraggableList/index.native.tsx b/src/components/DraggableList/index.native.tsx index f532b21720da..7cf33112bece 100644 --- a/src/components/DraggableList/index.native.tsx +++ b/src/components/DraggableList/index.native.tsx @@ -1,6 +1,6 @@ import React from 'react'; import DraggableFlatList from 'react-native-draggable-flatlist'; -import {FlatList} from 'react-native-gesture-handler'; +import type {FlatList} from 'react-native-gesture-handler'; import useThemeStyles from '@hooks/useThemeStyles'; import type {DraggableListProps} from './types'; diff --git a/src/components/DraggableList/index.tsx b/src/components/DraggableList/index.tsx index b92691075424..dc78a3ce6222 100644 --- a/src/components/DraggableList/index.tsx +++ b/src/components/DraggableList/index.tsx @@ -1,5 +1,6 @@ import React, {useCallback} from 'react'; -import {DragDropContext, Draggable, Droppable, type OnDragEndResponder} from 'react-beautiful-dnd'; +import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd'; +import type {OnDragEndResponder} from 'react-beautiful-dnd'; import {ScrollView} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import type {DraggableListProps} from './types'; diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 2fcf8e827b4e..1c0306741048 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -1,8 +1,9 @@ -import React, {ReactElement, useCallback} from 'react'; +import type {ReactElement} from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; +import type {Emoji} from '@assets/emojis/types'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {SimpleEmoji} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import getStyledTextArray from '@libs/GetStyledTextArray'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; @@ -15,7 +16,7 @@ type EmojiSuggestionsProps = { highlightedEmojiIndex?: number; /** Array of suggested emoji */ - emojis: SimpleEmoji[]; + emojis: Emoji[]; /** Fired when the user selects an emoji */ onSelect: (index: number) => void; @@ -39,7 +40,7 @@ type EmojiSuggestionsProps = { /** * Create unique keys for each emoji item */ -const keyExtractor = (item: SimpleEmoji, index: number): string => `${item.name}+${index}}`; +const keyExtractor = (item: Emoji, index: number): string => `${item.name}+${index}}`; function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { const styles = useThemeStyles(); @@ -48,7 +49,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr * Render an emoji suggestion menu item component. */ const renderSuggestionMenuItem = useCallback( - (item: SimpleEmoji): ReactElement => { + (item: Emoji): ReactElement => { const styledTextArray = getStyledTextArray(item.name, prefix); return ( diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx index 2a6524d5a993..6a0f1a0ae55e 100644 --- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx +++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {ErrorBoundary} from 'react-error-boundary'; import BootSplash from '@libs/BootSplash'; import GenericErrorPage from '@pages/ErrorPage/GenericErrorPage'; -import {BaseErrorBoundaryProps, LogError} from './types'; +import type {BaseErrorBoundaryProps, LogError} from './types'; /** * This component captures an error in the child component tree and logs it to the server diff --git a/src/components/ErrorBoundary/index.native.tsx b/src/components/ErrorBoundary/index.native.tsx index b8e56ee528c2..5e0c9ca9af38 100644 --- a/src/components/ErrorBoundary/index.native.tsx +++ b/src/components/ErrorBoundary/index.native.tsx @@ -2,7 +2,7 @@ import crashlytics from '@react-native-firebase/crashlytics'; import React from 'react'; import Log from '@libs/Log'; import BaseErrorBoundary from './BaseErrorBoundary'; -import {BaseErrorBoundaryProps, LogError} from './types'; +import type {BaseErrorBoundaryProps, LogError} from './types'; const logError: LogError = (errorMessage, error, errorInfo) => { // Log the error to the server diff --git a/src/components/ErrorBoundary/index.tsx b/src/components/ErrorBoundary/index.tsx index fce70674dd97..890cf5f4e587 100644 --- a/src/components/ErrorBoundary/index.tsx +++ b/src/components/ErrorBoundary/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Log from '@libs//Log'; import BaseErrorBoundary from './BaseErrorBoundary'; -import {BaseErrorBoundaryProps, LogError} from './types'; +import type {BaseErrorBoundaryProps, LogError} from './types'; const logError: LogError = (errorMessage, error, errorInfo) => { // Log the error to the server diff --git a/src/components/ExpensifyWordmark.tsx b/src/components/ExpensifyWordmark.tsx index 798907c7eeb6..0e8f78686b07 100644 --- a/src/components/ExpensifyWordmark.tsx +++ b/src/components/ExpensifyWordmark.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import AdHocLogo from '@assets/images/expensify-logo--adhoc.svg'; import DevLogo from '@assets/images/expensify-logo--dev.svg'; import StagingLogo from '@assets/images/expensify-logo--staging.svg'; diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx index 7fd6811c1df6..35fa4d02f5e0 100644 --- a/src/components/FixedFooter.tsx +++ b/src/components/FixedFooter.tsx @@ -1,5 +1,7 @@ -import React, {ReactNode} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {ReactNode} from 'react'; +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; type FixedFooterProps = { diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js new file mode 100644 index 000000000000..0abb1dc4a873 --- /dev/null +++ b/src/components/FlatList/MVCPFlatList.js @@ -0,0 +1,207 @@ +/* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ +import PropTypes from 'prop-types'; +import React from 'react'; +import {FlatList} from 'react-native'; + +function mergeRefs(...args) { + return function forwardRef(node) { + args.forEach((ref) => { + if (ref == null) { + return; + } + if (typeof ref === 'function') { + ref(node); + return; + } + if (typeof ref === 'object') { + // eslint-disable-next-line no-param-reassign + ref.current = node; + return; + } + console.error(`mergeRefs cannot handle Refs of type boolean, number or string, received ref ${String(ref)}`); + }); + }; +} + +function useMergeRefs(...args) { + return React.useMemo( + () => mergeRefs(...args), + // eslint-disable-next-line + [...args], + ); +} + +const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizontal, onScroll, ...props}, forwardedRef) => { + const {minIndexForVisible: mvcpMinIndexForVisible, autoscrollToTopThreshold: mvcpAutoscrollToTopThreshold} = maintainVisibleContentPosition ?? {}; + const scrollRef = React.useRef(null); + const prevFirstVisibleOffsetRef = React.useRef(null); + const firstVisibleViewRef = React.useRef(null); + const mutationObserverRef = React.useRef(null); + const lastScrollOffsetRef = React.useRef(0); + + const getScrollOffset = React.useCallback(() => { + if (scrollRef.current == null) { + return 0; + } + return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; + }, [horizontal]); + + const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode().childNodes[0], []); + + const scrollToOffset = React.useCallback( + (offset, animated) => { + const behavior = animated ? 'smooth' : 'instant'; + scrollRef.current?.getScrollableNode().scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); + }, + [horizontal], + ); + + const prepareForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const contentView = getContentView(); + if (contentView == null) { + return; + } + + const scrollOffset = getScrollOffset(); + + const contentViewLength = contentView.childNodes.length; + for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) { + const subview = contentView.childNodes[i]; + const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; + if (subviewOffset > scrollOffset || i === contentViewLength - 1) { + prevFirstVisibleOffsetRef.current = subviewOffset; + firstVisibleViewRef.current = subview; + break; + } + } + }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]); + + const adjustForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const firstVisibleView = firstVisibleViewRef.current; + const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current; + if (firstVisibleView == null || prevFirstVisibleOffset == null) { + return; + } + + const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop; + const delta = firstVisibleViewOffset - prevFirstVisibleOffset; + if (Math.abs(delta) > 0.5) { + const scrollOffset = getScrollOffset(); + prevFirstVisibleOffsetRef.current = firstVisibleViewOffset; + scrollToOffset(scrollOffset + delta, false); + if (mvcpAutoscrollToTopThreshold != null && scrollOffset <= mvcpAutoscrollToTopThreshold) { + scrollToOffset(0, true); + } + } + }, [getScrollOffset, scrollToOffset, mvcpMinIndexForVisible, mvcpAutoscrollToTopThreshold, horizontal]); + + const setupMutationObserver = React.useCallback(() => { + const contentView = getContentView(); + if (contentView == null) { + return; + } + + mutationObserverRef.current?.disconnect(); + + const mutationObserver = new MutationObserver(() => { + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(() => { + // Chrome adjusts scroll position when elements are added at the top of the + // view. We want to have the same behavior as react-native / Safari so we + // reset the scroll position to the last value we got from an event. + const lastScrollOffset = lastScrollOffsetRef.current; + const scrollOffset = getScrollOffset(); + if (lastScrollOffset !== scrollOffset) { + scrollToOffset(lastScrollOffset, false); + } + + adjustForMaintainVisibleContentPosition(); + }); + }); + mutationObserver.observe(contentView, { + attributes: true, + childList: true, + subtree: true, + }); + + mutationObserverRef.current = mutationObserver; + }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); + + React.useEffect(() => { + requestAnimationFrame(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }); + }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); + + const setMergedRef = useMergeRefs(scrollRef, forwardedRef); + + const onRef = React.useCallback( + (newRef) => { + // Make sure to only call refs and re-attach listeners if the node changed. + if (newRef == null || newRef === scrollRef.current) { + return; + } + + setMergedRef(newRef); + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, + [prepareForMaintainVisibleContentPosition, setMergedRef, setupMutationObserver], + ); + + React.useEffect(() => { + const mutationObserver = mutationObserverRef.current; + return () => { + mutationObserver?.disconnect(); + }; + }, []); + + const onScrollInternal = React.useCallback( + (ev) => { + lastScrollOffsetRef.current = getScrollOffset(); + + prepareForMaintainVisibleContentPosition(); + + onScroll?.(ev); + }, + [getScrollOffset, prepareForMaintainVisibleContentPosition, onScroll], + ); + + return ( + + ); +}); + +MVCPFlatList.displayName = 'MVCPFlatList'; +MVCPFlatList.propTypes = { + maintainVisibleContentPosition: PropTypes.shape({ + minIndexForVisible: PropTypes.number.isRequired, + autoscrollToTopThreshold: PropTypes.number, + }), + horizontal: PropTypes.bool, +}; + +MVCPFlatList.defaultProps = { + maintainVisibleContentPosition: null, + horizontal: false, +}; + +export default MVCPFlatList; diff --git a/src/components/FlatList/index.android.tsx b/src/components/FlatList/index.android.tsx index 84345f6e0ed4..863930203863 100644 --- a/src/components/FlatList/index.android.tsx +++ b/src/components/FlatList/index.android.tsx @@ -1,6 +1,8 @@ import {useFocusEffect} from '@react-navigation/native'; -import React, {ForwardedRef, forwardRef, useCallback, useContext} from 'react'; -import {FlatList, FlatListProps} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useContext} from 'react'; +import type {FlatListProps} from 'react-native'; +import {FlatList} from 'react-native'; import {ActionListContext} from '@pages/home/ReportScreenContext'; // FlatList wrapped with the freeze component will lose its scroll state when frozen (only for Android). diff --git a/src/components/FlatList/index.web.js b/src/components/FlatList/index.web.js new file mode 100644 index 000000000000..7299776db9bc --- /dev/null +++ b/src/components/FlatList/index.web.js @@ -0,0 +1,3 @@ +import MVCPFlatList from './MVCPFlatList'; + +export default MVCPFlatList; diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.tsx similarity index 83% rename from src/components/FloatingActionButton.js rename to src/components/FloatingActionButton.tsx index 59e741001063..2e9996a92f87 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.tsx @@ -1,5 +1,6 @@ -import PropTypes from 'prop-types'; +import type {ForwardedRef} from 'react'; import React, {useEffect, useRef} from 'react'; +import type {GestureResponderEvent, Role} from 'react-native'; import {Platform, View} from 'react-native'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; @@ -16,8 +17,18 @@ AnimatedPath.displayName = 'AnimatedPath'; const AnimatedPressable = Animated.createAnimatedComponent(PressableWithFeedback); AnimatedPressable.displayName = 'AnimatedPressable'; +type AdapterPropsRecord = { + type: number; + payload?: number | null; +}; + +type AdapterProps = { + fill?: string | AdapterPropsRecord; + stroke?: string | AdapterPropsRecord; +}; + const adapter = createAnimatedPropAdapter( - (props) => { + (props: AdapterProps) => { // eslint-disable-next-line rulesdir/prefer-underscore-method if (Object.keys(props).includes('fill')) { // eslint-disable-next-line no-param-reassign @@ -31,31 +42,27 @@ const adapter = createAnimatedPropAdapter( }, ['fill', 'stroke'], ); -adapter.propTypes = { - fill: PropTypes.string, - stroke: PropTypes.string, -}; -const propTypes = { +type FloatingActionButtonProps = { /* Callback to fire on request to toggle the FloatingActionButton */ - onPress: PropTypes.func.isRequired, + onPress: (event: GestureResponderEvent | KeyboardEvent | undefined) => void; /* Current state (active or not active) of the component */ - isActive: PropTypes.bool.isRequired, + isActive: boolean; /* An accessibility label for the button */ - accessibilityLabel: PropTypes.string.isRequired, + accessibilityLabel: string; /* An accessibility role for the button */ - role: PropTypes.string.isRequired, + role: Role; }; -const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => { +function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) { const {success, buttonDefaultBG, textLight, textDark} = useTheme(); const styles = useThemeStyles(); const borderRadius = styles.floatingActionButton.borderRadius; const {translate} = useLocalize(); - const fabPressable = useRef(null); + const fabPressable = useRef(null); const sharedValue = useSharedValue(isActive ? 1 : 0); const buttonRef = ref; @@ -94,7 +101,8 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility { fabPressable.current = el; - if (buttonRef) { + + if (buttonRef && 'current' in buttonRef) { buttonRef.current = el; } }} @@ -103,7 +111,7 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility pressDimmingValue={1} onPress={(e) => { // Drop focus to avoid blue focus ring. - fabPressable.current.blur(); + fabPressable.current?.blur(); onPress(e); }} onLongPress={() => {}} @@ -122,9 +130,8 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility
); -}); +} -FloatingActionButton.propTypes = propTypes; FloatingActionButton.displayName = 'FloatingActionButton'; export default FloatingActionButton; diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index d8e30b27371d..512d2063dc0f 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import Button from './Button'; import FormAlertWrapper from './FormAlertWrapper'; diff --git a/src/components/FormAlertWrapper.tsx b/src/components/FormAlertWrapper.tsx index a144bf069502..65fa2311620d 100644 --- a/src/components/FormAlertWrapper.tsx +++ b/src/components/FormAlertWrapper.tsx @@ -1,8 +1,10 @@ -import React, {ReactNode} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {ReactNode} from 'react'; +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import Network from '@src/types/onyx/Network'; +import type Network from '@src/types/onyx/Network'; import FormHelpMessage from './FormHelpMessage'; import {withNetwork} from './OnyxProvider'; import RenderHTML from './RenderHTML'; diff --git a/src/components/FormElement.tsx b/src/components/FormElement.tsx index c61a09b9d1ec..da98d4dc565a 100644 --- a/src/components/FormElement.tsx +++ b/src/components/FormElement.tsx @@ -1,5 +1,7 @@ -import React, {ForwardedRef, forwardRef} from 'react'; -import {View, ViewProps} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; +import type {ViewProps} from 'react-native'; +import {View} from 'react-native'; import * as ComponentUtils from '@libs/ComponentUtils'; function FormElement(props: ViewProps, ref: ForwardedRef) { diff --git a/src/components/FormHelpMessage.tsx b/src/components/FormHelpMessage.tsx index 43709b51db44..4f1d784788bf 100644 --- a/src/components/FormHelpMessage.tsx +++ b/src/components/FormHelpMessage.tsx @@ -1,6 +1,7 @@ import isEmpty from 'lodash/isEmpty'; import React from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Localize from '@libs/Localize'; diff --git a/src/components/FormScrollView.tsx b/src/components/FormScrollView.tsx index 4646a534e750..ade167e9e628 100644 --- a/src/components/FormScrollView.tsx +++ b/src/components/FormScrollView.tsx @@ -1,5 +1,7 @@ -import React, {ForwardedRef} from 'react'; -import {ScrollView, ScrollViewProps} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React from 'react'; +import type {ScrollViewProps} from 'react-native'; +import {ScrollView} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; type FormScrollViewProps = ScrollViewProps & { diff --git a/src/components/FormSubmit/index.native.tsx b/src/components/FormSubmit/index.native.tsx index 22bf5353949d..5eae7b51d988 100644 --- a/src/components/FormSubmit/index.native.tsx +++ b/src/components/FormSubmit/index.native.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import {FormSubmitProps, FormSubmitRef} from './types'; +import type {FormSubmitProps, FormSubmitRef} from './types'; function FormSubmit({style, children}: FormSubmitProps, ref: FormSubmitRef) { return ( diff --git a/src/components/FormSubmit/index.tsx b/src/components/FormSubmit/index.tsx index ef3f3c39bbaa..2ccd006bf322 100644 --- a/src/components/FormSubmit/index.tsx +++ b/src/components/FormSubmit/index.tsx @@ -1,9 +1,10 @@ -import React, {KeyboardEvent, useEffect} from 'react'; +import type {KeyboardEvent} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import * as ComponentUtils from '@libs/ComponentUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import CONST from '@src/CONST'; -import {FormSubmitProps, FormSubmitRef} from './types'; +import type {FormSubmitProps, FormSubmitRef} from './types'; function FormSubmit({children, onSubmit, style}: FormSubmitProps, ref: FormSubmitRef) { /** diff --git a/src/components/FormSubmit/types.ts b/src/components/FormSubmit/types.ts index 20910647ecb8..722a3fbf746e 100644 --- a/src/components/FormSubmit/types.ts +++ b/src/components/FormSubmit/types.ts @@ -1,5 +1,6 @@ -import React, {ForwardedRef} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {ForwardedRef} from 'react'; +import type React from 'react'; +import type {StyleProp, View, ViewStyle} from 'react-native'; type FormSubmitProps = { children: React.ReactNode; diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 2b5f7b3ada72..bd3082db5fa4 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {ActivityIndicator, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {ActivityIndicator, StyleSheet, View} from 'react-native'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js index 0b5cbad29983..46d04ca9404d 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js @@ -4,7 +4,7 @@ import {defaultHTMLElementModels, RenderHTMLConfigProvider, TRenderEngineProvide import _ from 'underscore'; import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; -import singleFontFamily from '@styles/utils/fontFamily/singleFontFamily'; +import FontUtils from '@styles/utils/FontUtils'; import * as HTMLEngineUtils from './htmlEngineUtils'; import htmlRenderers from './HTMLRenderers'; @@ -62,7 +62,7 @@ function BaseHTMLEngineProvider(props) { 'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}), 'next-step': defaultHTMLElementModels.span.extend({ tagName: 'next-step', - mixedUAStyles: {...styles.textLabelSupporting}, + mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16}, }), 'next-step-email': defaultHTMLElementModels.span.extend({tagName: 'next-step-email'}), video: defaultHTMLElementModels.div.extend({ @@ -70,7 +70,7 @@ function BaseHTMLEngineProvider(props) { mixedUAStyles: {whiteSpace: 'pre'}, }), }), - [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting], + [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16], ); // We need to memoize this prop to make it referentially stable. @@ -82,7 +82,7 @@ function BaseHTMLEngineProvider(props) { baseStyle={styles.webViewStyles.baseFontStyle} tagsStyles={styles.webViewStyles.tagStyles} enableCSSInlineProcessing={false} - systemFonts={_.values(singleFontFamily)} + systemFonts={_.values(FontUtils.fontFamily.single)} domVisitors={{ // eslint-disable-next-line no-param-reassign onText: (text) => (text.data = convertToLTR(text.data)), diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.js similarity index 76% rename from src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js rename to src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.js index 07954cc97a00..27eff02d63ea 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.js @@ -1,14 +1,14 @@ import PropTypes from 'prop-types'; -import React, {forwardRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import React from 'react'; +import {View} from 'react-native'; import _ from 'underscore'; -import htmlRendererPropTypes from '@components/HTMLEngineProvider/HTMLRenderers/htmlRendererPropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; import withLocalize from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import htmlRendererPropTypes from './htmlRendererPropTypes'; const propTypes = { /** Press in handler for the code block */ @@ -31,20 +31,14 @@ const defaultProps = { onPressOut: undefined, }; -const BasePreRenderer = forwardRef((props, ref) => { +function PreRenderer(props) { const styles = useThemeStyles(); const TDefaultRenderer = props.TDefaultRenderer; const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'onPressIn', 'onPressOut', 'onLongPress']); const isLast = props.renderIndex === props.renderLength - 1; return ( - + {({anchor, report, action, checkIfContextMenuActive}) => ( { )} - + ); -}); +} -BasePreRenderer.displayName = 'BasePreRenderer'; -BasePreRenderer.propTypes = propTypes; -BasePreRenderer.defaultProps = defaultProps; +PreRenderer.displayName = 'PreRenderer'; +PreRenderer.propTypes = propTypes; +PreRenderer.defaultProps = defaultProps; -export default withLocalize(BasePreRenderer); +export default withLocalize(PreRenderer); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js deleted file mode 100644 index 3beb52e6ee81..000000000000 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, {useCallback, useEffect, useRef} from 'react'; -import _ from 'underscore'; -import htmlRendererPropTypes from '@components/HTMLEngineProvider/HTMLRenderers/htmlRendererPropTypes'; -import ControlSelection from '@libs/ControlSelection'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import BasePreRenderer from './BasePreRenderer'; - -const supportsPassive = DeviceCapabilities.hasPassiveEventListenerSupport(); - -const isScrollingVertically = (event) => - // Mark as vertical scrolling only when absolute value of deltaY is more than the double of absolute - // value of deltaX, so user can use trackpad scroll on the code block horizontally at a wide angle. - Math.abs(event.deltaY) > Math.abs(event.deltaX) * 2; - -const debouncedIsScrollingVertically = _.debounce(isScrollingVertically, 100, true); - -function PreRenderer(props) { - const scrollViewRef = useRef(); - - /** - * Checks if user is scrolling vertically based on deltaX and deltaY. We debounce this - * method in order to make sure it's called only for the first event. - * @param {WheelEvent} event Wheel event - * @returns {Boolean} true if user is scrolling vertically - */ - - /** - * Manually scrolls the code block if code block horizontal scrollable, then prevents the event from being passed up to the parent. - * @param {Object} event native event - */ - const scrollNode = useCallback((event) => { - const node = scrollViewRef.current.getScrollableNode(); - const horizontalOverflow = node.scrollWidth > node.offsetWidth; - if (event.currentTarget === node && horizontalOverflow && !debouncedIsScrollingVertically(event)) { - node.scrollLeft += event.deltaX; - } - }, []); - - useEffect(() => { - const eventListenerRefValue = scrollViewRef.current; - if (!eventListenerRefValue) { - return; - } - eventListenerRefValue.getScrollableNode().addEventListener('wheel', scrollNode, supportsPassive ? {passive: true} : false); - - return () => { - if (!eventListenerRefValue.getScrollableNode()) { - return; - } - eventListenerRefValue.getScrollableNode().removeEventListener('wheel', scrollNode); - }; - }, [scrollNode]); - - return ( - DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={ControlSelection.unblock} - /> - ); -} - -PreRenderer.propTypes = htmlRendererPropTypes; -PreRenderer.displayName = 'PreRenderer'; - -export default PreRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.native.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.native.js deleted file mode 100644 index b84dd43dd82f..000000000000 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.native.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import htmlRendererPropTypes from '@components/HTMLEngineProvider/HTMLRenderers/htmlRendererPropTypes'; -import withLocalize from '@components/withLocalize'; -import BasePreRenderer from './BasePreRenderer'; - -function PreRenderer(props) { - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -} - -PreRenderer.propTypes = htmlRendererPropTypes; -PreRenderer.displayName = 'PreRenderer'; - -export default withLocalize(PreRenderer); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 4eac2c7a6994..25532107016f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,5 +1,7 @@ -import React, {ReactNode} from 'react'; -import {StyleProp, TextStyle, View} from 'react-native'; +import type {ReactNode} from 'react'; +import React from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; +import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import EnvironmentBadge from './EnvironmentBadge'; import Text from './Text'; diff --git a/src/components/HeaderGap/types.ts b/src/components/HeaderGap/types.ts index 55a202c2e48c..1333ceac6198 100644 --- a/src/components/HeaderGap/types.ts +++ b/src/components/HeaderGap/types.ts @@ -1,5 +1,5 @@ -import {ReactNode} from 'react'; -import {StyleProp, ViewStyle} from 'react-native'; +import type {ReactNode} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; type HeaderGapProps = { styles?: StyleProp; diff --git a/src/components/HeaderPageLayout.js b/src/components/HeaderPageLayout.tsx similarity index 55% rename from src/components/HeaderPageLayout.js rename to src/components/HeaderPageLayout.tsx index 9ef5d4f83a06..304bb2ce49b1 100644 --- a/src/components/HeaderPageLayout.js +++ b/src/components/HeaderPageLayout.tsx @@ -1,56 +1,54 @@ -import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; +import type {ReactNode} from 'react'; import {ScrollView, View} from 'react-native'; -import _ from 'underscore'; +import type {StyleProp, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import FixedFooter from './FixedFooter'; import HeaderWithBackButton from './HeaderWithBackButton'; -import headerWithBackButtonPropTypes from './HeaderWithBackButton/headerWithBackButtonPropTypes'; +import type HeaderWithBackButtonProps from './HeaderWithBackButton/types'; import ScreenWrapper from './ScreenWrapper'; -const propTypes = { - ...headerWithBackButtonPropTypes, +type HeaderPageLayoutProps = ChildrenProps & + HeaderWithBackButtonProps & { + /** The background color to apply in the upper half of the screen. */ + backgroundColor?: string; - /** Children to display in the lower half of the page (below the header section w/ an animation) */ - children: PropTypes.node.isRequired, + /** A fixed footer to display at the bottom of the page. */ + footer?: ReactNode; - /** The background color to apply in the upper half of the screen. */ - backgroundColor: PropTypes.string, + /** The image to display in the upper half of the screen. */ + headerContent?: ReactNode; - /** A fixed footer to display at the bottom of the page. */ - footer: PropTypes.node, + /** Style to apply to the header image container */ + headerContainerStyles?: StyleProp; - /** The image to display in the upper half of the screen. */ - header: PropTypes.node, + /** Style to apply to the ScrollView container */ + scrollViewContainerStyles?: StyleProp; - /** Style to apply to the header image container */ - // eslint-disable-next-line react/forbid-prop-types - headerContainerStyles: PropTypes.arrayOf(PropTypes.object), + /** Style to apply to the children container */ + childrenContainerStyles?: StyleProp; - /** Style to apply to the ScrollView container */ - // eslint-disable-next-line react/forbid-prop-types - scrollViewContainerStyles: PropTypes.arrayOf(PropTypes.object), + /** Style to apply to the whole section container */ + style?: StyleProp; + }; - /** Style to apply to the children container */ - // eslint-disable-next-line react/forbid-prop-types - childrenContainerStyles: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - backgroundColor: undefined, - header: null, - headerContainerStyles: [], - scrollViewContainerStyles: [], - childrenContainerStyles: [], - footer: null, -}; - -function HeaderPageLayout({backgroundColor, children, footer, headerContainerStyles, scrollViewContainerStyles, childrenContainerStyles, style, headerContent, ...propsToPassToHeader}) { +function HeaderPageLayout({ + backgroundColor, + children, + footer, + headerContainerStyles, + scrollViewContainerStyles, + childrenContainerStyles, + style, + headerContent, + ...rest +}: HeaderPageLayoutProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -58,7 +56,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty const {isOffline} = useNetwork(); const appBGColor = StyleUtils.getBackgroundColorStyle(theme.appBG); const {titleColor, iconFill} = useMemo(() => { - const isColorfulBackground = (backgroundColor || theme.appBG) !== theme.appBG && (backgroundColor || theme.highlightBG) !== theme.highlightBG; + const isColorfulBackground = (backgroundColor ?? theme.appBG) !== theme.appBG && (backgroundColor ?? theme.highlightBG) !== theme.highlightBG; return { titleColor: isColorfulBackground ? theme.textColorfulBackground : undefined, iconFill: isColorfulBackground ? theme.iconColorfulBackground : undefined, @@ -67,7 +65,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty return ( - + {/** Safari on ios/mac has a bug where overscrolling the page scrollview shows green background color. This is a workaround to fix that. https://github.com/Expensify/App/issues/23422 */} {Browser.isSafari() && ( - + )} - - {!Browser.isSafari() && } - + + {!Browser.isSafari() && } + {headerContent} {children} - {!_.isNull(footer) && {footer}} + {!!footer && {footer}} )} @@ -107,8 +102,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty ); } -HeaderPageLayout.propTypes = propTypes; -HeaderPageLayout.defaultProps = defaultProps; HeaderPageLayout.displayName = 'HeaderPageLayout'; +export type {HeaderPageLayoutProps}; export default HeaderPageLayout; diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 9ec8bca55a95..209803f2a5d1 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -19,7 +19,7 @@ import getButtonState from '@libs/getButtonState'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import HeaderWithBackButtonProps from './types'; +import type HeaderWithBackButtonProps from './types'; function HeaderWithBackButton({ iconFill, diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 99e93e8d18d2..9ffb0b5ef2f3 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -1,5 +1,5 @@ -import {ReactNode} from 'react'; -import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {ReactNode} from 'react'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {Action} from '@hooks/useSingleExecution'; import type {StepCounterParams} from '@src/languages/types'; import type {AnchorPosition} from '@src/styles'; diff --git a/src/components/HoldMenuSectionList.tsx b/src/components/HoldMenuSectionList.tsx new file mode 100644 index 000000000000..aa5dd75ce159 --- /dev/null +++ b/src/components/HoldMenuSectionList.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {ImageSourcePropType} from 'react-native'; +import type {SvgProps} from 'react-native-svg'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import type {TranslationPaths} from '@src/languages/types'; +import Icon from './Icon'; +import * as Illustrations from './Icon/Illustrations'; +import Text from './Text'; + +type HoldMenuSection = { + /** The icon supplied with the section */ + icon: React.FC | ImageSourcePropType; + + /** Translation key for the title */ + titleTranslationKey: TranslationPaths; + + /** Translation key for the description */ + descriptionTranslationKey: TranslationPaths; +}; + +function HoldMenuSectionList() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const holdMenuSections: HoldMenuSection[] = [ + { + icon: Illustrations.Hourglass, + titleTranslationKey: 'iou.whatIsHoldTitle', + descriptionTranslationKey: 'iou.whatIsHoldExplain', + }, + { + icon: Illustrations.CommentBubbles, + titleTranslationKey: 'iou.holdIsTemporaryTitle', + descriptionTranslationKey: 'iou.holdIsTemporaryExplain', + }, + { + icon: Illustrations.TrashCan, + titleTranslationKey: 'iou.deleteHoldTitle', + descriptionTranslationKey: 'iou.deleteHoldExplain', + }, + ]; + + return ( + <> + {holdMenuSections.map((section, i) => ( + + + + {translate(section.titleTranslationKey)} + + {translate(section.descriptionTranslationKey)} + + + + ))} + + ); +} + +HoldMenuSectionList.displayName = 'HoldMenuSectionList'; + +export type {HoldMenuSection}; + +export default HoldMenuSectionList; diff --git a/src/components/Hoverable/index.native.tsx b/src/components/Hoverable/index.native.tsx index b3d49db9d96e..47268e0f440a 100644 --- a/src/components/Hoverable/index.native.tsx +++ b/src/components/Hoverable/index.native.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import HoverableProps from './types'; +import type HoverableProps from './types'; /** * On mobile, there is no concept of hovering, so we return a plain wrapper around the component's children, diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 9c641cfc19be..c82ba659593a 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -1,8 +1,9 @@ -import React, {ForwardedRef, forwardRef, MutableRefObject, ReactElement, RefAttributes, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef, MutableRefObject, ReactElement, RefAttributes} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import HoverableProps from './types'; +import type HoverableProps from './types'; /** * Maps the children of a Hoverable component to @@ -145,7 +146,7 @@ function Hoverable( // Expose inner ref to parent through outerRef. This enable us to use ref both in parent and child. useImperativeHandle(outerRef, () => ref.current, []); - const child = useMemo(() => React.Children.only(mapChildren(children, isHovered)), [children, isHovered]); + const child = useMemo(() => React.Children.only(mapChildren(children as ReactElement, isHovered)), [children, isHovered]); const enableHoveredOnMouseEnter = useCallback( (event: MouseEvent) => { diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index 430b865f50c5..921772743ab4 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -1,8 +1,8 @@ -import {ReactElement} from 'react'; +import type {ReactNode} from 'react'; type HoverableProps = { /** Children to wrap with Hoverable. */ - children: ((isHovered: boolean) => ReactElement) | ReactElement; + children: ((isHovered: boolean) => ReactNode) | ReactNode; /** Whether to disable the hover action */ disabled?: boolean; diff --git a/src/components/IFrame.tsx b/src/components/IFrame.tsx index 7520ad869507..ab27597aeebd 100644 --- a/src/components/IFrame.tsx +++ b/src/components/IFrame.tsx @@ -1,7 +1,8 @@ import React, {useEffect, useState} from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Session} from '@src/types/onyx'; +import type {Session} from '@src/types/onyx'; type OldDotIFrameOnyxProps = { session: OnyxEntry; diff --git a/src/components/Icon/BankIcons/index.native.ts b/src/components/Icon/BankIcons/index.native.ts index 2011b71588af..61721512ec04 100644 --- a/src/components/Icon/BankIcons/index.native.ts +++ b/src/components/Icon/BankIcons/index.native.ts @@ -1,9 +1,10 @@ import GenericBank from '@assets/images/bankicons/generic-bank-account.svg'; import GenericBankCard from '@assets/images/cardicons/generic-bank-card.svg'; -import {BankIconParams, getBankIconAsset, getBankNameKey} from '@components/Icon/BankIconsUtils'; +import type {BankIconParams} from '@components/Icon/BankIconsUtils'; +import {getBankIconAsset, getBankNameKey} from '@components/Icon/BankIconsUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import {BankIcon} from '@src/types/onyx/Bank'; +import type {BankIcon} from '@src/types/onyx/Bank'; /** * Returns Bank Icon Object that matches to existing bank icons or default icons diff --git a/src/components/Icon/BankIcons/index.ts b/src/components/Icon/BankIcons/index.ts index 0e0935b103e4..378a7107aa03 100644 --- a/src/components/Icon/BankIcons/index.ts +++ b/src/components/Icon/BankIcons/index.ts @@ -1,10 +1,11 @@ import GenericBank from '@assets/images/bankicons/generic-bank-account.svg'; import GenericBankCard from '@assets/images/cardicons/generic-bank-card.svg'; -import {BankIconParams, getBankIconAsset, getBankNameKey} from '@components/Icon/BankIconsUtils'; +import type {BankIconParams} from '@components/Icon/BankIconsUtils'; +import {getBankIconAsset, getBankNameKey} from '@components/Icon/BankIconsUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import {BankIcon} from '@src/types/onyx/Bank'; -import IconAsset from '@src/types/utils/IconAsset'; +import type {BankIcon} from '@src/types/onyx/Bank'; +import type IconAsset from '@src/types/utils/IconAsset'; /** * It's a wrapper type for a bank icon asset. Bank icons are imported using require(), on the web platform after importing in this way it's necessary to use the property "default" diff --git a/src/components/Icon/BankIconsUtils.ts b/src/components/Icon/BankIconsUtils.ts index cc877ef9ef6f..c91ec030c9ba 100644 --- a/src/components/Icon/BankIconsUtils.ts +++ b/src/components/Icon/BankIconsUtils.ts @@ -1,7 +1,7 @@ -import {type ThemeStyles} from '@styles/index'; +import type {ThemeStyles} from '@styles/index'; import CONST from '@src/CONST'; -import {BankName, BankNameKey} from '@src/types/onyx/Bank'; -import IconAsset from '@src/types/utils/IconAsset'; +import type {BankName, BankNameKey} from '@src/types/onyx/Bank'; +import type IconAsset from '@src/types/utils/IconAsset'; type BankIconParams = { styles: ThemeStyles; diff --git a/src/components/Icon/IconWrapperStyles/index.ios.ts b/src/components/Icon/IconWrapperStyles/index.ios.ts index 9507502d9d26..b039cff95f58 100644 --- a/src/components/Icon/IconWrapperStyles/index.ios.ts +++ b/src/components/Icon/IconWrapperStyles/index.ios.ts @@ -1,4 +1,4 @@ -import IconWrapperStyle from './types'; +import type IconWrapperStyle from './types'; const style: IconWrapperStyle = { top: 1, diff --git a/src/components/Icon/IconWrapperStyles/index.ts b/src/components/Icon/IconWrapperStyles/index.ts index 541a2c296c17..2c4be7b706e8 100644 --- a/src/components/Icon/IconWrapperStyles/index.ts +++ b/src/components/Icon/IconWrapperStyles/index.ts @@ -1,4 +1,4 @@ -import IconWrapperStyle from './types'; +import type IconWrapperStyle from './types'; const style: IconWrapperStyle = { top: 2, diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 1e574504001d..954c8d0392fc 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -32,6 +32,7 @@ import BigRocket from '@assets/images/simple-illustrations/simple-illustration__ import PinkBill from '@assets/images/simple-illustrations/simple-illustration__bill.svg'; import ChatBubbles from '@assets/images/simple-illustrations/simple-illustration__chatbubbles.svg'; import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg'; +import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg'; import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg'; import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg'; import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg'; @@ -39,6 +40,7 @@ import EmailAddress from '@assets/images/simple-illustrations/simple-illustratio import HandCard from '@assets/images/simple-illustrations/simple-illustration__handcard.svg'; import HandEarth from '@assets/images/simple-illustrations/simple-illustration__handearth.svg'; import HotDogStand from '@assets/images/simple-illustrations/simple-illustration__hotdogstand.svg'; +import Hourglass from '@assets/images/simple-illustrations/simple-illustration__hourglass.svg'; import InvoiceBlue from '@assets/images/simple-illustrations/simple-illustration__invoice.svg'; import LockOpen from '@assets/images/simple-illustrations/simple-illustration__lockopen.svg'; import Luggage from '@assets/images/simple-illustrations/simple-illustration__luggage.svg'; @@ -53,6 +55,7 @@ import ShieldYellow from '@assets/images/simple-illustrations/simple-illustratio import SmallRocket from '@assets/images/simple-illustrations/simple-illustration__smallrocket.svg'; import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg'; import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; +import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; export { @@ -111,5 +114,8 @@ export { Hands, HandEarth, SmartScan, + Hourglass, + CommentBubbles, + TrashCan, TeleScope, }; diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 732fe90deae2..20f3fd4a8acb 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -1,11 +1,12 @@ -import {ImageContentFit} from 'expo-image'; +import type {ImageContentFit} from 'expo-image'; import React from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import ImageSVG from '@components/ImageSVG'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; -import IconAsset from '@src/types/utils/IconAsset'; +import type IconAsset from '@src/types/utils/IconAsset'; import IconWrapperStyles from './IconWrapperStyles'; type IconProps = { diff --git a/src/components/IllustratedHeaderPageLayout.js b/src/components/IllustratedHeaderPageLayout.js deleted file mode 100644 index 9980d8a7879a..000000000000 --- a/src/components/IllustratedHeaderPageLayout.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import HeaderPageLayout from './HeaderPageLayout'; -import headerWithBackButtonPropTypes from './HeaderWithBackButton/headerWithBackButtonPropTypes'; -import Lottie from './Lottie'; - -const propTypes = { - ...headerWithBackButtonPropTypes, - - /** Children to display in the lower half of the page (below the header section w/ an animation) */ - children: PropTypes.node.isRequired, - - /** The illustration to display in the header. Can be either an SVG component or a JSON object representing a Lottie animation. */ - illustration: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, - - /** The background color to apply in the upper half of the screen. */ - backgroundColor: PropTypes.string, - - /** A fixed footer to display at the bottom of the page. */ - footer: PropTypes.node, - - /** Overlay content to display on top of animation */ - overlayContent: PropTypes.func, -}; - -const defaultProps = { - backgroundColor: undefined, - footer: null, - overlayContent: null, -}; - -function IllustratedHeaderPageLayout({backgroundColor, children, illustration, footer, overlayContent, ...propsToPassToHeader}) { - const theme = useTheme(); - const styles = useThemeStyles(); - return ( - - - {overlayContent && overlayContent()} - - } - headerContainerStyles={[styles.justifyContentCenter, styles.w100]} - footer={footer} - // eslint-disable-next-line react/jsx-props-no-spreading - {...propsToPassToHeader} - > - {children} - - ); -} - -IllustratedHeaderPageLayout.propTypes = propTypes; -IllustratedHeaderPageLayout.defaultProps = defaultProps; -IllustratedHeaderPageLayout.displayName = 'IllustratedHeaderPageLayout'; - -export default IllustratedHeaderPageLayout; diff --git a/src/components/IllustratedHeaderPageLayout.tsx b/src/components/IllustratedHeaderPageLayout.tsx new file mode 100644 index 000000000000..72ec0adf7672 --- /dev/null +++ b/src/components/IllustratedHeaderPageLayout.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type {ReactNode} from 'react'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import HeaderPageLayout from './HeaderPageLayout'; +import type {HeaderPageLayoutProps} from './HeaderPageLayout'; +import Lottie from './Lottie'; +import type DotLottieAnimation from './LottieAnimations/types'; + +type IllustratedHeaderPageLayoutProps = HeaderPageLayoutProps & { + /** The illustration to display in the header. Can be a JSON object representing a Lottie animation. */ + illustration: DotLottieAnimation; + + /** The background color to apply in the upper half of the screen. */ + backgroundColor?: string; + + /** Overlay content to display on top of animation */ + overlayContent?: () => ReactNode; +}; + +function IllustratedHeaderPageLayout({backgroundColor, children, illustration, overlayContent, ...rest}: IllustratedHeaderPageLayoutProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + return ( + + + {overlayContent?.()} + + } + headerContainerStyles={[styles.justifyContentCenter, styles.w100]} + // eslint-disable-next-line react/jsx-props-no-spreading + {...rest} + > + {children} + + ); +} + +IllustratedHeaderPageLayout.displayName = 'IllustratedHeaderPageLayout'; + +export default IllustratedHeaderPageLayout; diff --git a/src/components/ImageSVG/index.native.tsx b/src/components/ImageSVG/index.native.tsx index c397e047b7e0..bdd0b3830a22 100644 --- a/src/components/ImageSVG/index.native.tsx +++ b/src/components/ImageSVG/index.native.tsx @@ -1,7 +1,7 @@ import {Image} from 'expo-image'; import React from 'react'; -import {ImageSourcePropType} from 'react-native'; -import ImageSVGProps from './types'; +import type {ImageSourcePropType} from 'react-native'; +import type ImageSVGProps from './types'; function ImageSVG({src, width = '100%', height = '100%', fill, contentFit = 'cover', style}: ImageSVGProps) { const tintColorProp = fill ? {tintColor: fill} : {}; diff --git a/src/components/ImageSVG/index.tsx b/src/components/ImageSVG/index.tsx index 32d83fdcadd3..3ce04a1a190a 100644 --- a/src/components/ImageSVG/index.tsx +++ b/src/components/ImageSVG/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {SvgProps} from 'react-native-svg'; -import ImageSVGProps from './types'; +import type {SvgProps} from 'react-native-svg'; +import type ImageSVGProps from './types'; function ImageSVG({src, width = '100%', height = '100%', fill, hovered = false, pressed = false, style, pointerEvents, preserveAspectRatio}: ImageSVGProps) { const ImageSvgComponent = src as React.FC; diff --git a/src/components/ImageSVG/types.ts b/src/components/ImageSVG/types.ts index cc580651c30c..341934852303 100644 --- a/src/components/ImageSVG/types.ts +++ b/src/components/ImageSVG/types.ts @@ -1,6 +1,6 @@ -import {ImageContentFit, ImageStyle} from 'expo-image'; -import {StyleProp, ViewStyle} from 'react-native'; -import IconAsset from '@src/types/utils/IconAsset'; +import type {ImageContentFit, ImageStyle} from 'expo-image'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type IconAsset from '@src/types/utils/IconAsset'; type ImageSVGProps = { /** The asset to render. */ diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index af803bb360ee..b13d863d97e1 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -1,6 +1,7 @@ import delay from 'lodash/delay'; import React, {useEffect, useRef, useState} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import FullscreenLoadingIndicator from './FullscreenLoadingIndicator'; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 7b7eae97fd86..5bf93eb8a6b3 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -1,7 +1,8 @@ import React from 'react'; import {StyleSheet, View} from 'react-native'; -import {OnyxCollection, withOnyx} from 'react-native-onyx'; -import {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxCollection} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as PolicyUtils from '@libs/PolicyUtils'; diff --git a/src/components/InlineCodeBlock/WrappedText.tsx b/src/components/InlineCodeBlock/WrappedText.tsx index 1c66cef234ed..3c196f2a7492 100644 --- a/src/components/InlineCodeBlock/WrappedText.tsx +++ b/src/components/InlineCodeBlock/WrappedText.tsx @@ -1,5 +1,6 @@ import React, {Fragment} from 'react'; -import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; diff --git a/src/components/InlineCodeBlock/types.ts b/src/components/InlineCodeBlock/types.ts index a100177e41a7..ae847b293a60 100644 --- a/src/components/InlineCodeBlock/types.ts +++ b/src/components/InlineCodeBlock/types.ts @@ -1,4 +1,4 @@ -import {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import type {TDefaultRenderer, TDefaultRendererProps, TText} from 'react-native-render-html'; type InlineCodeBlockProps = { diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 08d990583572..4a4ba5560e60 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -1,8 +1,8 @@ -import React, {ForwardedRef, forwardRef} from 'react'; -import {FlatListProps} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; +import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; const WINDOW_SIZE = 15; function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { @@ -14,7 +14,6 @@ function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef diff --git a/src/components/InvertedFlatList/CellRendererComponent.tsx b/src/components/InvertedFlatList/CellRendererComponent.tsx index 252d47989064..b95fbf42cbb4 100644 --- a/src/components/InvertedFlatList/CellRendererComponent.tsx +++ b/src/components/InvertedFlatList/CellRendererComponent.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {StyleProp, View, ViewProps} from 'react-native'; +import type {StyleProp, ViewProps} from 'react-native'; +import {View} from 'react-native'; type CellRendererComponentProps = ViewProps & { index: number; diff --git a/src/components/InvertedFlatList/index.native.tsx b/src/components/InvertedFlatList/index.native.tsx index 906ef6d6ae94..70cabf5a536a 100644 --- a/src/components/InvertedFlatList/index.native.tsx +++ b/src/components/InvertedFlatList/index.native.tsx @@ -1,17 +1,15 @@ -import React, {ForwardedRef, forwardRef} from 'react'; -import {FlatList, FlatListProps} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; +import type {FlatList, FlatListProps} from 'react-native'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import CellRendererComponent from './CellRendererComponent'; function BaseInvertedFlatListWithRef(props: FlatListProps, ref: ForwardedRef) { - const styles = useThemeStyles(); return ( ({onScroll: onScrollProp = () => {}, contentContainerStyle, ...props}: FlatListProps, ref: ForwardedRef) { +function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: FlatListProps, ref: ForwardedRef) { const lastScrollEvent = useRef(null); const scrollEndTimeout = useRef(null); const updateInProgress = useRef(false); @@ -84,8 +87,8 @@ function InvertedFlatList({onScroll: onScrollProp = () => {}, contentContaine // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - contentContainerStyle={contentContainerStyle} onScroll={handleScroll} + CellRendererComponent={CellRendererComponent} /> ); } diff --git a/src/components/KeyboardAvoidingView/index.ios.tsx b/src/components/KeyboardAvoidingView/index.ios.tsx index fde4df6fc05b..a7cd767377ef 100644 --- a/src/components/KeyboardAvoidingView/index.ios.tsx +++ b/src/components/KeyboardAvoidingView/index.ios.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native'; -import KeyboardAvoidingViewProps from './types'; +import type KeyboardAvoidingViewProps from './types'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/KeyboardAvoidingView/index.tsx b/src/components/KeyboardAvoidingView/index.tsx index cf3a5c5ebef7..09ec21e5b219 100644 --- a/src/components/KeyboardAvoidingView/index.tsx +++ b/src/components/KeyboardAvoidingView/index.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import {View} from 'react-native'; -import KeyboardAvoidingViewProps from './types'; +import type KeyboardAvoidingViewProps from './types'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, ...rest} = props; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 06f8ee4cfeb6..45326edb4610 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -183,8 +183,8 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError onError={onError} onLoadEnd={() => setImageLoaded(true)} onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) / PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) / PixelRatio.get(); + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); }} /> @@ -205,8 +205,8 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError isAuthTokenRequired={isAuthTokenRequired} onLoadEnd={() => setFallbackLoaded(true)} onLoad={(e) => { - const width = e.nativeEvent?.width || 0; - const height = e.nativeEvent?.height || 0; + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); if (imageDimensions?.lightboxSize != null) { return; diff --git a/src/components/LinearGradient/index.native.ts b/src/components/LinearGradient/index.native.ts index 46bed24ebc10..29af26b96b64 100644 --- a/src/components/LinearGradient/index.native.ts +++ b/src/components/LinearGradient/index.native.ts @@ -1,5 +1,5 @@ import LinearGradientNative from 'react-native-linear-gradient'; -import LinearGradient from './types'; +import type LinearGradient from './types'; const LinearGradientImplementation: LinearGradient = LinearGradientNative; diff --git a/src/components/LinearGradient/index.ts b/src/components/LinearGradient/index.ts index 7246ccf2fb69..84d0fc2ce5c8 100644 --- a/src/components/LinearGradient/index.ts +++ b/src/components/LinearGradient/index.ts @@ -1,5 +1,5 @@ import LinearGradientWeb from 'react-native-web-linear-gradient'; -import LinearGradient from './types'; +import type LinearGradient from './types'; const LinearGradientImplementation: LinearGradient = LinearGradientWeb; diff --git a/src/components/LinearGradient/types.ts b/src/components/LinearGradient/types.ts index cf6661eaecaa..9e238ef71f12 100644 --- a/src/components/LinearGradient/types.ts +++ b/src/components/LinearGradient/types.ts @@ -1,4 +1,4 @@ -import LinearGradientNative from 'react-native-linear-gradient'; +import type LinearGradientNative from 'react-native-linear-gradient'; type LinearGradient = typeof LinearGradientNative; diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 3c649a8cb546..7313bb4aa7bb 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -1,6 +1,7 @@ import React, {createContext, useMemo} from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import * as LocaleDigitUtils from '@libs/LocaleDigitUtils'; @@ -8,9 +9,10 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; import * as NumberFormatUtils from '@libs/NumberFormatUtils'; import CONST from '@src/CONST'; -import {TranslationPaths} from '@src/languages/types'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import withCurrentUserPersonalDetails, {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; type Locale = ValueOf; diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index 46adb1a4895e..3a2d9a0fd7b9 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js b/src/components/LocationErrorMessage/BaseLocationErrorMessage.tsx similarity index 81% rename from src/components/LocationErrorMessage/BaseLocationErrorMessage.js rename to src/components/LocationErrorMessage/BaseLocationErrorMessage.tsx index d90783d94ad5..33a0bba7481c 100644 --- a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js +++ b/src/components/LocationErrorMessage/BaseLocationErrorMessage.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; @@ -7,29 +6,25 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import Text from '@components/Text'; import TextLink from '@components/TextLink'; import Tooltip from '@components/Tooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import colors from '@styles/theme/colors'; import CONST from '@src/CONST'; -import * as locationErrorMessagePropTypes from './locationErrorMessagePropTypes'; +import type LocationErrorMessageProps from './types'; -const propTypes = { +type BaseLocationErrorMessageProps = LocationErrorMessageProps & { /** A callback that runs when 'allow location permission' link is pressed */ - onAllowLocationLinkPress: PropTypes.func.isRequired, - - // eslint-disable-next-line react/forbid-foreign-prop-types - ...locationErrorMessagePropTypes.propTypes, - - /* Onyx Props */ - ...withLocalizePropTypes, + onAllowLocationLinkPress: () => void; }; -function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationErrorCode, translate}) { +function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationErrorCode}: BaseLocationErrorMessageProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + if (!locationErrorCode) { return null; } @@ -81,6 +76,5 @@ function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationEr } BaseLocationErrorMessage.displayName = 'BaseLocationErrorMessage'; -BaseLocationErrorMessage.propTypes = propTypes; -BaseLocationErrorMessage.defaultProps = locationErrorMessagePropTypes.defaultProps; -export default withLocalize(BaseLocationErrorMessage); + +export default BaseLocationErrorMessage; diff --git a/src/components/LocationErrorMessage/index.native.js b/src/components/LocationErrorMessage/index.native.tsx similarity index 67% rename from src/components/LocationErrorMessage/index.native.js rename to src/components/LocationErrorMessage/index.native.tsx index 467018538b6e..7936fff73c06 100644 --- a/src/components/LocationErrorMessage/index.native.js +++ b/src/components/LocationErrorMessage/index.native.tsx @@ -1,14 +1,14 @@ import React from 'react'; import {Linking} from 'react-native'; import BaseLocationErrorMessage from './BaseLocationErrorMessage'; -import * as locationErrorMessagePropTypes from './locationErrorMessagePropTypes'; +import type LocationErrorMessageProps from './types'; /** Opens app level settings from the native system settings */ const openAppSettings = () => { Linking.openSettings(); }; -function LocationErrorMessage(props) { +function LocationErrorMessage(props: LocationErrorMessageProps) { return ( { Linking.openURL(CONST.NEWHELP_URL); }; -function LocationErrorMessage(props) { +function LocationErrorMessage(props: LocationErrorMessageProps) { return ( void; /** * The location error code from onyx @@ -11,11 +9,7 @@ const propTypes = { * - code 2 = location is unavailable or there is some connection issue * - code 3 = location fetch timeout */ - locationErrorCode: PropTypes.oneOf([-1, 1, 2, 3]), -}; - -const defaultProps = { - locationErrorCode: null, + locationErrorCode?: -1 | 1 | 2 | 3; }; -export {propTypes, defaultProps}; +export default LocationErrorMessageProps; diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx index 2bf7df056c4d..5c672cf7cab6 100644 --- a/src/components/Lottie/index.tsx +++ b/src/components/Lottie/index.tsx @@ -1,7 +1,9 @@ -import LottieView, {LottieViewProps} from 'lottie-react-native'; -import React, {ForwardedRef, forwardRef} from 'react'; +import type {LottieViewProps} from 'lottie-react-native'; +import LottieView from 'lottie-react-native'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; import {View} from 'react-native'; -import DotLottieAnimation from '@components/LottieAnimations/types'; +import type DotLottieAnimation from '@components/LottieAnimations/types'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx index 078cdc0c6987..0d2cac253135 100644 --- a/src/components/LottieAnimations/index.tsx +++ b/src/components/LottieAnimations/index.tsx @@ -1,4 +1,4 @@ -import DotLottieAnimation from './types'; +import type DotLottieAnimation from './types'; const DotLottieAnimations: Record = { ExpensifyLounge: { diff --git a/src/components/LottieAnimations/types.ts b/src/components/LottieAnimations/types.ts index fcd793c6dfed..6000b9f853f0 100644 --- a/src/components/LottieAnimations/types.ts +++ b/src/components/LottieAnimations/types.ts @@ -1,4 +1,4 @@ -import {LottieViewProps} from 'lottie-react-native'; +import type {LottieViewProps} from 'lottie-react-native'; type DotLottieAnimation = { file: LottieViewProps['source']; diff --git a/src/components/MapView/Direction.tsx b/src/components/MapView/Direction.tsx index b3162149a48d..729d3c71e91f 100644 --- a/src/components/MapView/Direction.tsx +++ b/src/components/MapView/Direction.tsx @@ -1,6 +1,6 @@ import Mapbox from '@rnmapbox/maps'; import useThemeStyles from '@hooks/useThemeStyles'; -import {DirectionProps} from './MapViewTypes'; +import type {DirectionProps} from './MapViewTypes'; function Direction({coordinates}: DirectionProps) { const styles = useThemeStyles(); diff --git a/src/components/MapView/Direction.website.tsx b/src/components/MapView/Direction.website.tsx index f85bda125473..02fee83042a2 100644 --- a/src/components/MapView/Direction.website.tsx +++ b/src/components/MapView/Direction.website.tsx @@ -6,7 +6,7 @@ import React from 'react'; import {Layer, Source} from 'react-map-gl'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import {DirectionProps} from './MapViewTypes'; +import type {DirectionProps} from './MapViewTypes'; function Direction({coordinates}: DirectionProps) { const styles = useThemeStyles(); diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index f7a15ba10d2c..a3178f642852 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -1,5 +1,6 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'; -import Mapbox, {MapState, MarkerView, setAccessToken} from '@rnmapbox/maps'; +import type {MapState} from '@rnmapbox/maps'; +import Mapbox, {MarkerView, setAccessToken} from '@rnmapbox/maps'; import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -12,10 +13,10 @@ import useLocalize from '@src/hooks/useLocalize'; import useNetwork from '@src/hooks/useNetwork'; import ONYXKEYS from '@src/ONYXKEYS'; import Direction from './Direction'; -import {MapViewHandle} from './MapViewTypes'; +import type {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; import responder from './responder'; -import {ComponentProps, MapViewOnyxProps} from './types'; +import type {ComponentProps, MapViewOnyxProps} from './types'; import utils from './utils'; const MapView = forwardRef( @@ -104,7 +105,7 @@ const MapView = forwardRef( if (waypoints.length === 1) { cameraRef.current?.setCamera({ - zoomLevel: 15, + zoomLevel: CONST.MAPBOX.SINGLE_MARKER_ZOOM, animationDuration: 1500, centerCoordinate: waypoints[0].coordinate, }); diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index 82909001fefd..289f7d0d62a8 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -6,7 +6,8 @@ import {useFocusEffect} from '@react-navigation/native'; import mapboxgl from 'mapbox-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import Map, {MapRef, Marker} from 'react-map-gl'; +import type {MapRef} from 'react-map-gl'; +import Map, {Marker} from 'react-map-gl'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -20,10 +21,10 @@ import getCurrentPosition from '@src/libs/getCurrentPosition'; import ONYXKEYS from '@src/ONYXKEYS'; import Direction from './Direction'; import './mapbox.css'; -import {MapViewHandle} from './MapViewTypes'; +import type {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; import responder from './responder'; -import {ComponentProps, MapViewOnyxProps} from './types'; +import type {ComponentProps, MapViewOnyxProps} from './types'; import utils from './utils'; const MapView = forwardRef( @@ -116,7 +117,7 @@ const MapView = forwardRef( if (waypoints.length === 1) { mapRef.flyTo({ center: waypoints[0].coordinate, - zoom: CONST.MAPBOX.DEFAULT_ZOOM, + zoom: CONST.MAPBOX.SINGLE_MARKER_ZOOM, }); return; } diff --git a/src/components/MapView/MapViewTypes.ts b/src/components/MapView/MapViewTypes.ts index 6cc52ac91d18..4b9ab6af233b 100644 --- a/src/components/MapView/MapViewTypes.ts +++ b/src/components/MapView/MapViewTypes.ts @@ -1,4 +1,4 @@ -import {ComponentType} from 'react'; +import type {ComponentType} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; type MapViewProps = { diff --git a/src/components/MapView/PendingMapView.tsx b/src/components/MapView/PendingMapView.tsx index 0af816785e9a..32bf42a14b10 100644 --- a/src/components/MapView/PendingMapView.tsx +++ b/src/components/MapView/PendingMapView.tsx @@ -6,7 +6,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; -import {PendingMapViewProps} from './MapViewTypes'; +import type {PendingMapViewProps} from './MapViewTypes'; function PendingMapView({title = '', subtitle = '', style}: PendingMapViewProps) { const hasTextContent = !_.isEmpty(title) || !_.isEmpty(subtitle); diff --git a/src/components/MapView/types.ts b/src/components/MapView/types.ts index 2c8b9240c445..a0494a9ac499 100644 --- a/src/components/MapView/types.ts +++ b/src/components/MapView/types.ts @@ -1,6 +1,6 @@ -import {OnyxEntry} from 'react-native-onyx'; -import * as OnyxTypes from '@src/types/onyx'; -import {MapViewProps} from './MapViewTypes'; +import type {OnyxEntry} from 'react-native-onyx'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {MapViewProps} from './MapViewTypes'; type MapViewOnyxProps = { userLocation: OnyxEntry; diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 3e235a2fc88a..459131ecc434 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -5,7 +5,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getStyledTextArray from '@libs/GetStyledTextArray'; import CONST from '@src/CONST'; -import {Icon} from '@src/types/onyx/OnyxCommon'; +import type {Icon} from '@src/types/onyx/OnyxCommon'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; import Avatar from './Avatar'; import Text from './Text'; diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index db150d55f0d2..ce44db72598a 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,9 +1,11 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import {ImageContentFit} from 'expo-image'; -import React, {ForwardedRef, forwardRef, ReactNode, useEffect, useMemo, useRef, useState} from 'react'; -import {GestureResponderEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import {AnimatedStyle} from 'react-native-reanimated'; -import {ValueOf} from 'type-fest'; +import type {ImageContentFit} from 'expo-image'; +import type {ForwardedRef, ReactNode} from 'react'; +import React, {forwardRef, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {AnimatedStyle} from 'react-native-reanimated'; +import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,16 +14,16 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; -import {AvatarSource} from '@libs/UserUtils'; +import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; -import {Icon as IconType} from '@src/types/onyx/OnyxCommon'; -import IconAsset from '@src/types/utils/IconAsset'; +import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; +import type IconAsset from '@src/types/utils/IconAsset'; import Avatar from './Avatar'; import Badge from './Badge'; import DisplayNames from './DisplayNames'; -import {DisplayNameWithTooltip} from './DisplayNames/types'; +import type {DisplayNameWithTooltip} from './DisplayNames/types'; import FormHelpMessage from './FormHelpMessage'; import Hoverable from './Hoverable'; import Icon from './Icon'; @@ -33,30 +35,16 @@ import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -type ResponsiveProps = { - /** Function to fire when component is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent) => void; - - interactive?: true; -}; - -type UnresponsiveProps = { - onPress?: undefined; - - /** Whether the menu item should be interactive at all */ - interactive: false; -}; - type IconProps = { /** Flag to choose between avatar image or an icon */ - iconType: typeof CONST.ICON_TYPE_ICON; + iconType?: typeof CONST.ICON_TYPE_ICON; /** Icon to display on the left side of component */ icon: IconAsset; }; type AvatarProps = { - iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + iconType?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; icon: AvatarSource; }; @@ -67,170 +55,175 @@ type NoIcon = { icon?: undefined; }; -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & - (IconProps | AvatarProps | NoIcon) & { - /** Text to be shown as badge near the right end. */ - badgeText?: string; +type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { + /** Function to fire when component is pressed */ + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + + /** Whether the menu item should be interactive at all */ + interactive?: boolean; - /** Used to apply offline styles to child text components */ - style?: ViewStyle; + /** Text to be shown as badge near the right end. */ + badgeText?: string; - /** Any additional styles to apply */ - wrapperStyle?: StyleProp; + /** Used to apply offline styles to child text components */ + style?: ViewStyle; - /** Any additional styles to apply on the outer element */ - containerStyle?: StyleProp; + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; - /** Used to apply styles specifically to the title */ - titleStyle?: ViewStyle; + /** Any additional styles to apply on the outer element */ + containerStyle?: StyleProp; - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: StyleProp>; + /** Used to apply styles specifically to the title */ + titleStyle?: ViewStyle; - /** Additional styles to style the description text below the title */ - descriptionTextStyle?: StyleProp; + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle?: StyleProp>; - /** The fill color to pass into the icon. */ - iconFill?: string; + /** Additional styles to style the description text below the title */ + descriptionTextStyle?: StyleProp; - /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon?: IconAsset; + /** The fill color to pass into the icon. */ + iconFill?: string; - /** The fill color to pass into the secondary icon. */ - secondaryIconFill?: string; + /** Secondary icon to display on the left side of component, right of the icon */ + secondaryIcon?: IconAsset; - /** Icon Width */ - iconWidth?: number; + /** The fill color to pass into the secondary icon. */ + secondaryIconFill?: string; - /** Icon Height */ - iconHeight?: number; + /** Icon Width */ + iconWidth?: number; - /** Any additional styles to pass to the icon container. */ - iconStyles?: StyleProp; + /** Icon Height */ + iconHeight?: number; - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: IconAsset; + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; - /** An icon to display under the main item */ - furtherDetailsIcon?: IconAsset; + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: IconAsset; - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon?: boolean; + /** An icon to display under the main item */ + furtherDetailsIcon?: IconAsset; - /** Icon to display at right side of title */ - titleIcon?: IconAsset; + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon?: boolean; - /** Boolean whether to display the right icon */ - shouldShowRightIcon?: boolean; + /** Icon to display at right side of title */ + titleIcon?: IconAsset; - /** Overrides the icon for shouldShowRightIcon */ - iconRight?: IconAsset; + /** Boolean whether to display the right icon */ + shouldShowRightIcon?: boolean; - /** Should render component on the right */ - shouldShowRightComponent?: boolean; + /** Overrides the icon for shouldShowRightIcon */ + iconRight?: IconAsset; - /** Component to be displayed on the right */ - rightComponent?: ReactNode; + /** Should render component on the right */ + shouldShowRightComponent?: boolean; - /** A description text to show under the title */ - description?: string; + /** Component to be displayed on the right */ + rightComponent?: ReactNode; - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop?: boolean; + /** A description text to show under the title */ + description?: string; - /** Error to display below the title */ - error?: string; + /** Should the description be shown above the title (instead of the other way around) */ + shouldShowDescriptionOnTop?: boolean; - /** Error to display at the bottom of the component */ - errorText?: string; + /** Error to display below the title */ + error?: string; - /** A boolean flag that gives the icon a green fill if true */ - success?: boolean; + /** Error to display at the bottom of the component */ + errorText?: string; - /** Whether item is focused or active */ - focused?: boolean; + /** A boolean flag that gives the icon a green fill if true */ + success?: boolean; - /** Should we disable this menu item? */ - disabled?: boolean; + /** Whether item is focused or active */ + focused?: boolean; - /** Text that appears above the title */ - label?: string; + /** Should we disable this menu item? */ + disabled?: boolean; - /** Label to be displayed on the right */ - rightLabel?: string; + /** Text that appears above the title */ + label?: string; - /** Text to display for the item */ - title?: string; + /** Label to be displayed on the right */ + rightLabel?: string; - /** A right-aligned subtitle for this menu option */ - subtitle?: string | number; + /** Text to display for the item */ + title?: string; - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle?: boolean; + /** A right-aligned subtitle for this menu option */ + subtitle?: string | number; - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState?: boolean; + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle?: boolean; - /** Whether this item is selected */ - isSelected?: boolean; + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState?: boolean; - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; + /** Whether this item is selected */ + isSelected?: boolean; - /** Prop to represent the size of the avatar images to be shown */ - avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally?: boolean; - /** Avatars to show on the right of the menu item */ - floatRightAvatars?: IconType[]; + /** Prop to represent the size of the avatar images to be shown */ + avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; - /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize?: ValueOf; + /** Avatars to show on the right of the menu item */ + floatRightAvatars?: IconType[]; - /** Affects avatar size */ - viewMode?: ValueOf; + /** Prop to represent the size of the float right avatar images to be shown */ + floatRightAvatarSize?: ValueOf; - /** Used to truncate the text with an ellipsis after computing the text layout */ - numberOfLinesTitle?: number; + /** Affects avatar size */ + viewMode?: ValueOf; - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu?: boolean; + /** Used to truncate the text with an ellipsis after computing the text layout */ + numberOfLinesTitle?: number; - /** The type of brick road indicator to show. */ - brickRoadIndicator?: ValueOf; + /** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu?: boolean; - /** Should render the content in HTML format */ - shouldRenderAsHTML?: boolean; + /** The type of brick road indicator to show. */ + brickRoadIndicator?: ValueOf; - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled?: boolean; + /** Should render the content in HTML format */ + shouldRenderAsHTML?: boolean; - /** The action accept for anonymous user or not */ - isAnonymousAction?: boolean; + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled?: boolean; - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection?: boolean; + /** The action accept for anonymous user or not */ + isAnonymousAction?: boolean; - /** Whether should render title as HTML or as Text */ - shouldParseTitle?: false; + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection?: boolean; - /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress?: boolean; + /** Whether should render title as HTML or as Text */ + shouldParseTitle?: false; - /** Text to display under the main item */ - furtherDetails?: string; + /** Should check anonymous user in onPress function */ + shouldCheckActionAllowedOnPress?: boolean; - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; + /** Text to display under the main item */ + furtherDetails?: string; - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: DisplayNameWithTooltip[]; + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction?: (event: GestureResponderEvent | MouseEvent) => void; - /** Icon should be displayed in its own color */ - displayInDefaultIconColor?: boolean; + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips?: DisplayNameWithTooltip[]; - /** Determines how the icon should be resized to fit its container */ - contentFit?: ImageContentFit; - }; + /** Icon should be displayed in its own color */ + displayInDefaultIconColor?: boolean; + + /** Determines how the icon should be resized to fit its container */ + contentFit?: ImageContentFit; +}; function MenuItem( { @@ -523,17 +516,18 @@ function MenuItem( {error} )} - {furtherDetailsIcon && !!furtherDetails && ( + {!!furtherDetails && ( - + {!!furtherDetailsIcon && ( + + )} {furtherDetails} diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js deleted file mode 100644 index c9eee8e888e1..000000000000 --- a/src/components/MenuItemList.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import useSingleExecution from '@hooks/useSingleExecution'; -import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import MenuItem from './MenuItem'; -import menuItemPropTypes from './menuItemPropTypes'; - -const propTypes = { - /** An array of props that are pass to individual MenuItem components */ - menuItems: PropTypes.arrayOf(PropTypes.shape(menuItemPropTypes)), - - /** Whether or not to use the single execution hook */ - shouldUseSingleExecution: PropTypes.bool, -}; -const defaultProps = { - menuItems: [], - shouldUseSingleExecution: false, -}; - -function MenuItemList(props) { - let popoverAnchor; - const {isExecuting, singleExecution} = useSingleExecution(); - - /** - * Handle the secondary interaction for a menu item. - * - * @param {*} link the menu item link or function to get the link - * @param {Event} e the interaction event - */ - const secondaryInteraction = (link, e) => { - if (typeof link === 'function') { - link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, url, popoverAnchor)); - } else if (!_.isEmpty(link)) { - ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, link, popoverAnchor); - } - }; - - return ( - <> - {_.map(props.menuItems, (menuItemProps) => ( - secondaryInteraction(menuItemProps.link, e) : undefined} - ref={(el) => (popoverAnchor = el)} - shouldBlockSelection={Boolean(menuItemProps.link)} - // eslint-disable-next-line react/jsx-props-no-spreading - {...menuItemProps} - disabled={menuItemProps.disabled || isExecuting} - onPress={props.shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} - /> - ))} - - ); -} - -MenuItemList.displayName = 'MenuItemList'; -MenuItemList.propTypes = propTypes; -MenuItemList.defaultProps = defaultProps; - -export default MenuItemList; diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx new file mode 100644 index 000000000000..f83f173a644f --- /dev/null +++ b/src/components/MenuItemList.tsx @@ -0,0 +1,63 @@ +import React, {useRef} from 'react'; +import type {GestureResponderEvent, View} from 'react-native'; +import useSingleExecution from '@hooks/useSingleExecution'; +import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {MenuItemProps} from './MenuItem'; +import MenuItem from './MenuItem'; + +type MenuItemLink = string | (() => Promise); + +type MenuItemWithLink = MenuItemProps & { + /** The link to open when the menu item is clicked */ + link: MenuItemLink; +}; + +type MenuItemListProps = { + /** An array of props that are pass to individual MenuItem components */ + menuItems: MenuItemWithLink[]; + + /** Whether or not to use the single execution hook */ + shouldUseSingleExecution?: boolean; +}; + +function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuItemListProps) { + const popoverAnchor = useRef(null); + const {isExecuting, singleExecution} = useSingleExecution(); + + /** + * Handle the secondary interaction for a menu item. + * + * @param link the menu item link or function to get the link + * @param event the interaction event + */ + const secondaryInteraction = (link: MenuItemLink, event: GestureResponderEvent | MouseEvent) => { + if (typeof link === 'function') { + link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, url, popoverAnchor.current)); + } else if (link) { + ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, link, popoverAnchor.current); + } + }; + + return ( + <> + {menuItems.map((menuItemProps) => ( + secondaryInteraction(menuItemProps.link, e) : undefined} + ref={popoverAnchor} + shouldBlockSelection={!!menuItemProps.link} + // eslint-disable-next-line react/jsx-props-no-spreading + {...menuItemProps} + disabled={!!menuItemProps.disabled || isExecuting} + onPress={shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} + /> + ))} + + ); +} + +MenuItemList.displayName = 'MenuItemList'; + +export type {MenuItemWithLink}; +export default MenuItemList; diff --git a/src/components/MenuItemWithTopDescription.tsx b/src/components/MenuItemWithTopDescription.tsx index 48fa95ecf637..0a48740de62e 100644 --- a/src/components/MenuItemWithTopDescription.tsx +++ b/src/components/MenuItemWithTopDescription.tsx @@ -1,5 +1,6 @@ -import React, {ForwardedRef, forwardRef} from 'react'; -import {View} from 'react-native'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; +import type {View} from 'react-native'; import MenuItem from './MenuItem'; import type {MenuItemProps} from './MenuItem'; diff --git a/src/components/MessagesRow.tsx b/src/components/MessagesRow.tsx index f49a0927de06..cfec6fd292e9 100644 --- a/src/components/MessagesRow.tsx +++ b/src/components/MessagesRow.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Localize from '@libs/Localize'; +import type * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import DotIndicatorMessage from './DotIndicatorMessage'; diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index e03c32f6bfcc..6e5b4eddae9e 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -13,7 +13,7 @@ import useNativeDriver from '@libs/useNativeDriver'; import variables from '@styles/variables'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; -import BaseModalProps from './types'; +import type BaseModalProps from './types'; function BaseModal( { diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 2343cb4c70a9..4d7ae128a114 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -3,7 +3,7 @@ import {AppState} from 'react-native'; import withWindowDimensions from '@components/withWindowDimensions'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import BaseModal from './BaseModal'; -import BaseModalProps from './types'; +import type BaseModalProps from './types'; AppState.addEventListener('focus', () => { ComposerFocusManager.setReadyToFocus(); diff --git a/src/components/Modal/index.ios.tsx b/src/components/Modal/index.ios.tsx index f780775ec216..cbe58a071d7d 100644 --- a/src/components/Modal/index.ios.tsx +++ b/src/components/Modal/index.ios.tsx @@ -1,7 +1,7 @@ import React from 'react'; import withWindowDimensions from '@components/withWindowDimensions'; import BaseModal from './BaseModal'; -import BaseModalProps from './types'; +import type BaseModalProps from './types'; function Modal({children, ...rest}: BaseModalProps) { return ( diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 4269420dcd7f..56f3c76a8879 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -5,7 +5,7 @@ import useTheme from '@hooks/useTheme'; import StatusBar from '@libs/StatusBar'; import CONST from '@src/CONST'; import BaseModal from './BaseModal'; -import BaseModalProps from './types'; +import type BaseModalProps from './types'; function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = () => {}, children, ...rest}: BaseModalProps) { const theme = useTheme(); diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 461a5935eda9..0fed37ffea8b 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -1,8 +1,8 @@ -import {ViewStyle} from 'react-native'; -import {ModalProps} from 'react-native-modal'; -import {ValueOf} from 'type-fest'; -import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; -import CONST from '@src/CONST'; +import type {ViewStyle} from 'react-native'; +import type {ModalProps} from 'react-native-modal'; +import type {ValueOf} from 'type-fest'; +import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; +import type CONST from '@src/CONST'; type PopoverAnchorPosition = { top?: number; diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 8ed6d0746438..5b59fca6cdae 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -87,9 +87,9 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = lodashGet(policy, 'type'); const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; - const isGroupPolicy = _.contains([CONST.POLICY.TYPE.CORPORATE, CONST.POLICY.TYPE.TEAM], policyType); + const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicy(moneyRequestReport); const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID; - const isPayer = isGroupPolicy + const isPayer = isPaidGroupPolicy ? // In a group policy, the admin approver can pay the report directly by skipping the approval step isPolicyAdmin && (isApproved || isManager) : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); @@ -99,11 +99,11 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], ); const shouldShowApproveButton = useMemo(() => { - if (!isGroupPolicy) { + if (!isPaidGroupPolicy) { return false; } return isManager && !isDraft && !isApproved && !isSettled; - }, [isGroupPolicy, isManager, isDraft, isApproved, isSettled]); + }, [isPaidGroupPolicy, isManager, isDraft, isApproved, isSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx index 9d96d93423c5..4b30276a204f 100644 --- a/src/components/MoneyReportHeaderStatusBar.tsx +++ b/src/components/MoneyReportHeaderStatusBar.tsx @@ -4,7 +4,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as NextStepUtils from '@libs/NextStepUtils'; import CONST from '@src/CONST'; -import ReportNextStep from '@src/types/onyx/ReportNextStep'; +import type ReportNextStep from '@src/types/onyx/ReportNextStep'; import RenderHTML from './RenderHTML'; type MoneyReportHeaderStatusBarProps = { diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index b75f4e2df845..13dce9337673 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -40,6 +40,7 @@ import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; +import taxPropTypes from './taxPropTypes'; import Text from './Text'; import transactionPropTypes from './transactionPropTypes'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; @@ -164,6 +165,10 @@ const propTypes = { /** Collection of tags attached to a policy */ policyTags: tagPropTypes, + /* Onyx Props */ + /** Collection of tax rates attached to a policy */ + policyTaxRates: taxPropTypes, + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: iouPropTypes, }; @@ -200,6 +205,7 @@ const defaultProps = { shouldShowSmartScanFields: true, isPolicyExpenseChat: false, iou: iouDefaultProps, + policyTaxRates: {}, }; function MoneyRequestConfirmationList(props) { @@ -241,6 +247,9 @@ function MoneyRequestConfirmationList(props) { // A flag for showing the tags field const shouldShowTags = props.isPolicyExpenseChat && (props.iouTag || OptionsListUtils.hasEnabledOptions(_.values(policyTagList))); + // A flag for showing tax fields - tax rate and tax amount + const shouldShowTax = props.isPolicyExpenseChat && props.policy.isTaxTrackingEnabled; + // A flag for showing the billable field const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true); @@ -252,6 +261,11 @@ function MoneyRequestConfirmationList(props) { shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, props.isDistanceRequest ? currency : props.iouCurrencyCode, ); + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); + + const defaultTaxKey = props.policyTaxRates.defaultExternalID; + const defaultTaxName = (defaultTaxKey && `${props.policyTaxRates.taxes[defaultTaxKey].name} (${props.policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; + const taxRateTitle = (props.transaction.taxRate && props.transaction.taxRate.text) || defaultTaxName; const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); @@ -741,6 +755,40 @@ function MoneyRequestConfirmationList(props) { /> )} + {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')} @@ -777,12 +825,15 @@ export default compose( key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, }, - draftTransaction: { + splitTransactionDraft: { key: ({transactionID}) => `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, }, policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + policyTaxRates: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${policyID}`, + }, iou: { key: ONYXKEYS.IOU, }, diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index ecd95620c498..2fee67a3d632 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -39,6 +39,7 @@ import OptionsSelector from './OptionsSelector'; import SettlementButton from './SettlementButton'; import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; +import taxPropTypes from './taxPropTypes'; import Text from './Text'; import transactionPropTypes from './transactionPropTypes'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; @@ -160,6 +161,10 @@ const propTypes = { /** Collection of tags attached to a policy */ policyTags: tagPropTypes, + /* Onyx Props */ + /** Collection of tax rates attached to a policy */ + policyTaxRates: taxPropTypes, + /** Transaction that represents the money request */ transaction: transactionPropTypes, }; @@ -194,6 +199,7 @@ const defaultProps = { isDistanceRequest: false, shouldShowSmartScanFields: true, isPolicyExpenseChat: false, + policyTaxRates: {}, }; function MoneyTemporaryForRefactorRequestConfirmationList({ @@ -235,6 +241,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ session: {accountID}, shouldShowSmartScanFields, transaction, + policyTaxRates, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -269,6 +276,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // A flag for showing the tags field const shouldShowTags = isPolicyExpenseChat && OptionsListUtils.hasEnabledOptions(_.values(policyTagList)); + // A flag for showing tax rate + const shouldShowTax = isPolicyExpenseChat && policy && policy.isTaxTrackingEnabled; + // A flag for showing the billable field const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true); @@ -280,6 +290,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, ); + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); + + const defaultTaxKey = policyTaxRates.defaultExternalID; + const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; + const taxRateTitle = (transaction.taxRate && transaction.taxRate.text) || defaultTaxName; const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); @@ -796,6 +811,35 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ rightLabel={canUseViolations && Boolean(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')} @@ -835,5 +879,8 @@ export default compose( policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + policyTaxRates: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${policyID}`, + }, }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 23c795f281f6..a6f34cd459fc 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -1,10 +1,11 @@ import React, {memo, useMemo} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; -import {ValueOf} from 'type-fest'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {AvatarSource} from '@libs/UserUtils'; +import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {Icon} from '@src/types/onyx/OnyxCommon'; @@ -142,7 +143,7 @@ function MultipleAvatars({ if (icons.length === 1 && !shouldStackHorizontally) { return ( ( {icons.length === 2 ? ( (props: BasePickerProps, ref: ForwardedRef) { return ( diff --git a/src/components/Picker/index.tsx b/src/components/Picker/index.tsx index 18184b130bba..591914b74d4c 100644 --- a/src/components/Picker/index.tsx +++ b/src/components/Picker/index.tsx @@ -1,4 +1,5 @@ -import React, {ForwardedRef, forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; import BasePicker from './BasePicker'; import type {AdditionalPickerEvents, BasePickerHandle, BasePickerProps, OnChange, OnMouseDown} from './types'; diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index 58eed0371893..edf39a59c9d8 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -1,5 +1,5 @@ -import {ChangeEvent, Component, ReactElement} from 'react'; -import {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, ViewStyle} from 'react-native'; +import type {ChangeEvent, Component, ReactElement} from 'react'; +import type {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, ViewStyle} from 'react-native'; type MeasureLayoutOnFailCallback = () => void; diff --git a/src/components/PlaidLink/index.native.tsx b/src/components/PlaidLink/index.native.tsx index c46a9df2076e..24ab75eb62b7 100644 --- a/src/components/PlaidLink/index.native.tsx +++ b/src/components/PlaidLink/index.native.tsx @@ -1,8 +1,9 @@ import {useEffect} from 'react'; -import {dismissLink, LinkEvent, openLink, usePlaidEmitter} from 'react-native-plaid-link-sdk'; +import type {LinkEvent} from 'react-native-plaid-link-sdk'; +import {dismissLink, openLink, usePlaidEmitter} from 'react-native-plaid-link-sdk'; import Log from '@libs/Log'; import CONST from '@src/CONST'; -import PlaidLinkProps from './types'; +import type PlaidLinkProps from './types'; function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: PlaidLinkProps) { usePlaidEmitter((event: LinkEvent) => { diff --git a/src/components/PlaidLink/index.tsx b/src/components/PlaidLink/index.tsx index 08655458d12e..c3a913886f2c 100644 --- a/src/components/PlaidLink/index.tsx +++ b/src/components/PlaidLink/index.tsx @@ -1,10 +1,11 @@ import React, {useCallback, useEffect, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; -import {PlaidLinkOnSuccessMetadata, usePlaidLink} from 'react-plaid-link'; +import type {PlaidLinkOnSuccessMetadata} from 'react-plaid-link'; +import {usePlaidLink} from 'react-plaid-link'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; -import PlaidLinkProps from './types'; +import type PlaidLinkProps from './types'; function PlaidLink({token, onSuccess = () => {}, onError = () => {}, onExit = () => {}, onEvent, receivedRedirectURI}: PlaidLinkProps) { const [isPlaidLoaded, setIsPlaidLoaded] = useState(false); diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts index 1034eb935f74..48526520c736 100644 --- a/src/components/PlaidLink/types.ts +++ b/src/components/PlaidLink/types.ts @@ -1,5 +1,5 @@ -import {LinkEventMetadata, LinkSuccessMetadata} from 'react-native-plaid-link-sdk'; -import {PlaidLinkOnEventMetadata, PlaidLinkOnSuccessMetadata} from 'react-plaid-link'; +import type {LinkEventMetadata, LinkSuccessMetadata} from 'react-native-plaid-link-sdk'; +import type {PlaidLinkOnEventMetadata, PlaidLinkOnSuccessMetadata} from 'react-plaid-link'; type PlaidLinkProps = { // Plaid Link SDK public token used to initialize the Plaid SDK diff --git a/src/components/Popover/index.native.tsx b/src/components/Popover/index.native.tsx index 3fff04d00d2f..08ed15fd0d30 100644 --- a/src/components/Popover/index.native.tsx +++ b/src/components/Popover/index.native.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Modal from '@components/Modal'; import CONST from '@src/CONST'; -import {PopoverProps} from './types'; +import type {PopoverProps} from './types'; /* * This is a convenience wrapper around the Modal component for a responsive Popover. diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index bf79415d4794..762e79fab63c 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -5,7 +5,7 @@ import {PopoverContext} from '@components/PopoverProvider'; import PopoverWithoutOverlay from '@components/PopoverWithoutOverlay'; import withWindowDimensions from '@components/withWindowDimensions'; import CONST from '@src/CONST'; -import {PopoverWithWindowDimensionsProps} from './types'; +import type {PopoverWithWindowDimensionsProps} from './types'; /* * This is a convenience wrapper around the Modal component for a responsive Popover. diff --git a/src/components/Popover/popoverPropTypes.js b/src/components/Popover/popoverPropTypes.js index c13fd8fa0b85..c758c4e6d311 100644 --- a/src/components/Popover/popoverPropTypes.js +++ b/src/components/Popover/popoverPropTypes.js @@ -26,6 +26,9 @@ const propTypes = { /** The ref of the popover */ withoutOverlayRef: refPropTypes, + + /** Whether we want to show the popover on the right side of the screen */ + fromSidebarMediumScreen: PropTypes.bool, }; const defaultProps = { diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 7f7e2829770c..3d1f95822e6a 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -1,7 +1,16 @@ -import BaseModalProps, {PopoverAnchorPosition} from '@components/Modal/types'; -import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; - -type AnchorAlignment = {horizontal: string; vertical: string}; +import type {ValueOf} from 'type-fest'; +import type {PopoverAnchorPosition} from '@components/Modal/types'; +import type BaseModalProps from '@components/Modal/types'; +import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; +import type CONST from '@src/CONST'; + +type AnchorAlignment = { + /** The horizontal anchor alignment of the popover */ + horizontal: ValueOf; + + /** The vertical anchor alignment of the popover */ + vertical: ValueOf; +}; type PopoverDimensions = { width: number; @@ -39,4 +48,4 @@ type PopoverProps = BaseModalProps & { type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps; -export type {PopoverProps, PopoverWithWindowDimensionsProps}; +export type {PopoverProps, PopoverWithWindowDimensionsProps, AnchorAlignment}; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx new file mode 100644 index 000000000000..502bdbf83b53 --- /dev/null +++ b/src/components/PopoverMenu.tsx @@ -0,0 +1,177 @@ +import type {ImageContentFit} from 'expo-image'; +import type {RefObject} from 'react'; +import React, {useRef} from 'react'; +import {View} from 'react-native'; +import type {ModalProps} from 'react-native-modal'; +import type {SvgProps} from 'react-native-svg'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import CONST from '@src/CONST'; +import type {AnchorPosition} from '@src/styles'; +import MenuItem from './MenuItem'; +import type {AnchorAlignment} from './Popover/types'; +import PopoverWithMeasuredContent from './PopoverWithMeasuredContent'; +import Text from './Text'; + +type PopoverMenuItem = { + /** An icon element displayed on the left side */ + icon: React.FC; + + /** Text label */ + text: string; + + /** A callback triggered when this item is selected */ + onSelected: () => void; + + /** A description text to show under the title */ + description?: string; + + /** The fill color to pass into the icon. */ + iconFill?: string; + + /** Icon Width */ + iconWidth?: number; + + /** Icon Height */ + iconHeight?: number; + + /** Icon should be displayed in its own color */ + displayInDefaultIconColor?: boolean; + + /** Determines how the icon should be resized to fit its container */ + contentFit?: ImageContentFit; +}; + +type PopoverModalProps = Pick; + +type PopoverMenuProps = PopoverModalProps & { + /** Callback method fired when the user requests to close the modal */ + onClose: () => void; + + /** State that determines whether to display the modal or not */ + isVisible: boolean; + + /** Callback to fire when a CreateMenu item is selected */ + onItemSelected: (selectedItem: PopoverMenuItem, index: number) => void; + + /** Menu items to be rendered on the list */ + menuItems: PopoverMenuItem[]; + + /** Optional non-interactive text to display as a header for any create menu */ + headerText?: string; + + /** Whether disable the animations */ + disableAnimation?: boolean; + + /** The horizontal and vertical anchors points for the popover */ + anchorPosition: AnchorPosition; + + /** Ref of the anchor */ + anchorRef: RefObject; + + /** Where the popover should be positioned relative to the anchor points. */ + anchorAlignment?: AnchorAlignment; + + /** Whether we don't want to show overlay */ + withoutOverlay?: boolean; + + /** Should we announce the Modal visibility changes? */ + shouldSetModalVisibility?: boolean; + + /** Whether we want to show the popover on the right side of the screen */ + fromSidebarMediumScreen?: boolean; +}; + +function PopoverMenu({ + menuItems, + onItemSelected, + isVisible, + anchorPosition, + anchorRef, + onClose, + headerText, + fromSidebarMediumScreen, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, + animationIn = 'fadeIn', + animationOut = 'fadeOut', + animationInTiming = CONST.ANIMATED_TRANSITION, + disableAnimation = true, + withoutOverlay = false, + shouldSetModalVisibility = true, +}: PopoverMenuProps) { + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + const selectedItemIndex = useRef(null); + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItems.length - 1, isActive: isVisible}); + + const selectItem = (index: number) => { + const selectedItem = menuItems[index]; + onItemSelected(selectedItem, index); + selectedItemIndex.current = index; + }; + + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.ENTER, + () => { + if (focusedIndex === -1) { + return; + } + selectItem(focusedIndex); + setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu + }, + {isActive: isVisible}, + ); + + return ( + { + setFocusedIndex(-1); + if (selectedItemIndex.current !== null) { + menuItems[selectedItemIndex.current].onSelected(); + selectedItemIndex.current = null; + } + }} + animationIn={animationIn} + animationOut={animationOut} + animationInTiming={animationInTiming} + disableAnimation={disableAnimation} + fromSidebarMediumScreen={fromSidebarMediumScreen} + withoutOverlay={withoutOverlay} + shouldSetModalVisibility={shouldSetModalVisibility} + > + + {!!headerText && {headerText}} + {menuItems.map((item, menuIndex) => ( + selectItem(menuIndex)} + focused={focusedIndex === menuIndex} + displayInDefaultIconColor={item.displayInDefaultIconColor} + /> + ))} + + + ); +} + +PopoverMenu.displayName = 'PopoverMenu'; + +export default React.memo(PopoverMenu); diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js deleted file mode 100644 index 597105173b4c..000000000000 --- a/src/components/PopoverMenu/index.js +++ /dev/null @@ -1,114 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useRef} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import MenuItem from '@components/MenuItem'; -import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; -import refPropTypes from '@components/refPropTypes'; -import Text from '@components/Text'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; -import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import CONST from '@src/CONST'; -import {defaultProps as createMenuDefaultProps, propTypes as createMenuPropTypes} from './popoverMenuPropTypes'; - -const propTypes = { - ...createMenuPropTypes, - ...windowDimensionsPropTypes, - - /** Ref of the anchor */ - anchorRef: refPropTypes, - - withoutOverlay: PropTypes.bool, - - /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility: PropTypes.bool, -}; - -const defaultProps = { - ...createMenuDefaultProps, - anchorAlignment: { - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, - }, - anchorRef: () => {}, - withoutOverlay: false, - shouldSetModalVisibility: true, -}; - -function PopoverMenu(props) { - const styles = useThemeStyles(); - const {isSmallScreenWidth} = useWindowDimensions(); - const selectedItemIndex = useRef(null); - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: props.menuItems.length - 1, isActive: props.isVisible}); - - const selectItem = (index) => { - const selectedItem = props.menuItems[index]; - props.onItemSelected(selectedItem, index); - selectedItemIndex.current = index; - }; - - useKeyboardShortcut( - CONST.KEYBOARD_SHORTCUTS.ENTER, - () => { - if (focusedIndex === -1) { - return; - } - selectItem(focusedIndex); - setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu - }, - {isActive: props.isVisible}, - ); - - return ( - { - setFocusedIndex(-1); - if (selectedItemIndex.current !== null) { - props.menuItems[selectedItemIndex.current].onSelected(); - selectedItemIndex.current = null; - } - }} - animationIn={props.animationIn} - animationOut={props.animationOut} - animationInTiming={props.animationInTiming} - disableAnimation={props.disableAnimation} - fromSidebarMediumScreen={props.fromSidebarMediumScreen} - withoutOverlay={props.withoutOverlay} - shouldSetModalVisibility={props.shouldSetModalVisibility} - > - - {!_.isEmpty(props.headerText) && {props.headerText}} - {_.map(props.menuItems, (item, menuIndex) => ( - selectItem(menuIndex)} - focused={focusedIndex === menuIndex} - displayInDefaultIconColor={item.displayInDefaultIconColor} - /> - ))} - - - ); -} - -PopoverMenu.propTypes = propTypes; -PopoverMenu.defaultProps = defaultProps; -PopoverMenu.displayName = 'PopoverMenu'; - -export default React.memo(withWindowDimensions(PopoverMenu)); diff --git a/src/components/PopoverMenu/popoverMenuPropTypes.js b/src/components/PopoverMenu/popoverMenuPropTypes.js deleted file mode 100644 index 53eeb63b05e7..000000000000 --- a/src/components/PopoverMenu/popoverMenuPropTypes.js +++ /dev/null @@ -1,71 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; -import CONST from '@src/CONST'; - -const propTypes = { - /** Callback method fired when the user requests to close the modal */ - onClose: PropTypes.func.isRequired, - - /** State that determines whether to display the modal or not */ - isVisible: PropTypes.bool.isRequired, - - /** Callback to fire when a CreateMenu item is selected */ - onItemSelected: PropTypes.func.isRequired, - - /** Menu items to be rendered on the list */ - menuItems: PropTypes.arrayOf( - PropTypes.shape({ - /** An icon element displayed on the left side */ - icon: sourcePropTypes, - - /** Text label */ - text: PropTypes.string.isRequired, - - /** A callback triggered when this item is selected */ - onSelected: PropTypes.func.isRequired, - }), - ).isRequired, - - /** The anchor position of the CreateMenu popover */ - anchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }).isRequired, - - /** Where the popover should be positioned relative to the anchor points. */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), - - /** The anchor reference of the CreateMenu popover */ - anchorRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, - - /** A react-native-animatable animation definition for the modal display animation. */ - animationIn: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - - /** A react-native-animatable animation definition for the modal hide animation. */ - animationOut: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - - /** A react-native-animatable animation timing for the modal display animation. */ - animationInTiming: PropTypes.number, - - /** Optional non-interactive text to display as a header for any create menu */ - headerText: PropTypes.string, - - /** Whether disable the animations */ - disableAnimation: PropTypes.bool, -}; - -const defaultProps = { - animationIn: 'fadeIn', - animationOut: 'fadeOut', - animationInTiming: CONST.ANIMATED_TRANSITION, - headerText: undefined, - disableAnimation: true, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/PopoverProvider/index.native.tsx b/src/components/PopoverProvider/index.native.tsx index a87036c61808..b13909945bef 100644 --- a/src/components/PopoverProvider/index.native.tsx +++ b/src/components/PopoverProvider/index.native.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {PopoverContextProps, PopoverContextValue} from './types'; +import type {PopoverContextProps, PopoverContextValue} from './types'; const PopoverContext = React.createContext({ onOpen: () => {}, diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx index 06345ebdbc1c..b50b04289813 100644 --- a/src/components/PopoverProvider/index.tsx +++ b/src/components/PopoverProvider/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {AnchorRef, PopoverContextProps, PopoverContextValue} from './types'; +import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types'; const PopoverContext = React.createContext({ onOpen: () => {}, diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx index 9d10f7869f8a..792002441ac6 100644 --- a/src/components/PopoverWithMeasuredContent.tsx +++ b/src/components/PopoverWithMeasuredContent.tsx @@ -1,15 +1,17 @@ import isEqual from 'lodash/isEqual'; import React, {useMemo, useState} from 'react'; -import {LayoutChangeEvent, View} from 'react-native'; +import type {LayoutChangeEvent} from 'react-native'; +import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import PopoverWithMeasuredContentUtils from '@libs/PopoverWithMeasuredContentUtils'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; import Popover from './Popover'; -import {PopoverProps} from './Popover/types'; +import type {PopoverProps} from './Popover/types'; +import type {WindowDimensionsProps} from './withWindowDimensions/types'; -type PopoverWithMeasuredContentProps = Omit & { +type PopoverWithMeasuredContentProps = Omit & { /** The horizontal and vertical anchors points for the popover */ anchorPosition: AnchorPosition; }; @@ -34,6 +36,12 @@ function PopoverWithMeasuredContent({ }, children, withoutOverlay = false, + fullscreen = true, + shouldCloseOnOutsideClick = false, + shouldSetModalVisibility = true, + statusBarTranslucent = true, + avoidKeyboard = false, + hideModalContentWhileAnimating = false, ...props }: PopoverWithMeasuredContentProps) { const styles = useThemeStyles(); @@ -113,6 +121,12 @@ function PopoverWithMeasuredContent({ anchorAlignment={anchorAlignment} isVisible={isVisible} withoutOverlay={withoutOverlay} + fullscreen={fullscreen} + shouldCloseOnOutsideClick={shouldCloseOnOutsideClick} + shouldSetModalVisibility={shouldSetModalVisibility} + statusBarTranslucent={statusBarTranslucent} + avoidKeyboard={avoidKeyboard} + hideModalContentWhileAnimating={hideModalContentWhileAnimating} // eslint-disable-next-line react/jsx-props-no-spreading {...props} anchorPosition={shiftedAnchorPosition} diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index f83949bcbe9d..6aed275bd2dc 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -1,4 +1,5 @@ -import React, {ForwardedRef, forwardRef, useContext, useEffect, useMemo} from 'react'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useContext, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import {PopoverContext} from '@components/PopoverProvider'; @@ -7,7 +8,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Modal from '@userActions/Modal'; -import PopoverWithoutOverlayProps from './types'; +import type PopoverWithoutOverlayProps from './types'; function PopoverWithoutOverlay( { diff --git a/src/components/PopoverWithoutOverlay/types.ts b/src/components/PopoverWithoutOverlay/types.ts index 0d70c4a5facb..ff4f73fd4114 100644 --- a/src/components/PopoverWithoutOverlay/types.ts +++ b/src/components/PopoverWithoutOverlay/types.ts @@ -1,6 +1,6 @@ -import {View} from 'react-native'; -import BaseModalProps from '@components/Modal/types'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {View} from 'react-native'; +import type BaseModalProps from '@components/Modal/types'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; type PopoverWithoutOverlayProps = ChildrenProps & Omit & { diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index 0a9f544bcee1..f41a6b389001 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -1,6 +1,8 @@ -import React, {ForwardedRef, forwardRef, useCallback, useEffect, useMemo} from 'react'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo} from 'react'; +import type {GestureResponderEvent, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports -import {GestureResponderEvent, Pressable, View} from 'react-native'; +import {Pressable} from 'react-native'; import useSingleExecution from '@hooks/useSingleExecution'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -8,7 +10,8 @@ import Accessibility from '@libs/Accessibility'; import HapticFeedback from '@libs/HapticFeedback'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import CONST from '@src/CONST'; -import PressableProps, {PressableRef} from './types'; +import type {PressableRef} from './types'; +import type PressableProps from './types'; function GenericPressable( { diff --git a/src/components/Pressable/GenericPressable/index.native.tsx b/src/components/Pressable/GenericPressable/index.native.tsx index 968ee6063a95..c17163677cbe 100644 --- a/src/components/Pressable/GenericPressable/index.native.tsx +++ b/src/components/Pressable/GenericPressable/index.native.tsx @@ -1,6 +1,7 @@ import React, {forwardRef} from 'react'; import GenericPressable from './BaseGenericPressable'; -import PressableProps, {PressableRef} from './types'; +import type {PressableRef} from './types'; +import type PressableProps from './types'; function NativeGenericPressable(props: PressableProps, ref: PressableRef) { return ( diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx index 51099733e04f..3d6301379155 100644 --- a/src/components/Pressable/GenericPressable/index.tsx +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -1,7 +1,8 @@ import React, {forwardRef} from 'react'; -import {Role} from 'react-native'; +import type {Role} from 'react-native'; import GenericPressable from './BaseGenericPressable'; -import PressableProps, {PressableRef} from './types'; +import type {PressableRef} from './types'; +import type PressableProps from './types'; function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: PressableRef) { const accessible = props.accessible ?? props.accessible === undefined ? true : props.accessible; diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index cdb9a8624114..dc04b6fcf329 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -1,8 +1,8 @@ -import {ElementRef, ForwardedRef, RefObject} from 'react'; -import {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, View, ViewStyle} from 'react-native'; -import {ValueOf} from 'type-fest'; -import {Shortcut} from '@libs/KeyboardShortcut'; -import CONST from '@src/CONST'; +import type {ElementRef, ForwardedRef, RefObject} from 'react'; +import type {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, View, ViewStyle} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import type {Shortcut} from '@libs/KeyboardShortcut'; +import type CONST from '@src/CONST'; type StylePropWithFunction = StyleProp | ((state: PressableStateCallbackType) => StyleProp); diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index d265be929b0c..ab1fa95efeb5 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native-a11y/has-valid-accessibility-descriptors */ import React, {forwardRef} from 'react'; -import {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; @@ -10,8 +10,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import getButtonState from '@libs/getButtonState'; import variables from '@styles/variables'; -import IconAsset from '@src/types/utils/IconAsset'; -import PressableProps, {PressableRef} from './GenericPressable/types'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {PressableRef} from './GenericPressable/types'; +import type PressableProps from './GenericPressable/types'; import PressableWithoutFeedback from './PressableWithoutFeedback'; type PressableWithDelayToggleProps = PressableProps & { diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx index a4c439c4441c..b717c4890a2d 100644 --- a/src/components/Pressable/PressableWithFeedback.tsx +++ b/src/components/Pressable/PressableWithFeedback.tsx @@ -1,10 +1,11 @@ import React, {forwardRef, useState} from 'react'; -import {StyleProp, ViewStyle} from 'react-native'; -import {AnimatedStyle} from 'react-native-reanimated'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type {AnimatedStyle} from 'react-native-reanimated'; import OpacityView from '@components/OpacityView'; import variables from '@styles/variables'; import GenericPressable from './GenericPressable'; -import PressableProps, {PressableRef} from './GenericPressable/types'; +import type {PressableRef} from './GenericPressable/types'; +import type PressableProps from './GenericPressable/types'; type PressableWithFeedbackProps = PressableProps & { /** Style for the wrapper view */ diff --git a/src/components/Pressable/PressableWithoutFeedback.tsx b/src/components/Pressable/PressableWithoutFeedback.tsx index fd9d695cc2ed..f95050dd649e 100644 --- a/src/components/Pressable/PressableWithoutFeedback.tsx +++ b/src/components/Pressable/PressableWithoutFeedback.tsx @@ -1,6 +1,7 @@ import React from 'react'; import GenericPressable from './GenericPressable'; -import PressableProps, {PressableRef} from './GenericPressable/types'; +import type {PressableRef} from './GenericPressable/types'; +import type PressableProps from './GenericPressable/types'; function PressableWithoutFeedback( {pressStyle, hoverStyle, focusStyle, disabledStyle, screenReaderActiveStyle, shouldUseHapticsOnPress, shouldUseHapticsOnLongPress, ...rest}: PressableProps, diff --git a/src/components/Pressable/PressableWithoutFocus.tsx b/src/components/Pressable/PressableWithoutFocus.tsx index 32cb1708baf0..f887b0ea9b7d 100644 --- a/src/components/Pressable/PressableWithoutFocus.tsx +++ b/src/components/Pressable/PressableWithoutFocus.tsx @@ -1,7 +1,7 @@ import React, {useRef} from 'react'; -import {View} from 'react-native'; +import type {View} from 'react-native'; import GenericPressable from './GenericPressable'; -import PressableProps from './GenericPressable/types'; +import type PressableProps from './GenericPressable/types'; /** * This component prevents the tapped element from capturing focus. diff --git a/src/components/PressableWithSecondaryInteraction/index.native.tsx b/src/components/PressableWithSecondaryInteraction/index.native.tsx index 77dc9452f986..d448e6d6ecc3 100644 --- a/src/components/PressableWithSecondaryInteraction/index.native.tsx +++ b/src/components/PressableWithSecondaryInteraction/index.native.tsx @@ -1,9 +1,10 @@ -import React, {forwardRef, ReactNode} from 'react'; -import {GestureResponderEvent, TextProps} from 'react-native'; -import {PressableRef} from '@components/Pressable/GenericPressable/types'; +import type {ReactNode} from 'react'; +import React, {forwardRef} from 'react'; +import type {GestureResponderEvent, TextProps} from 'react-native'; +import type {PressableRef} from '@components/Pressable/GenericPressable/types'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; -import PressableWithSecondaryInteractionProps from './types'; +import type PressableWithSecondaryInteractionProps from './types'; /** This is a special Pressable that calls onSecondaryInteraction when LongPressed. */ function PressableWithSecondaryInteraction( diff --git a/src/components/PressableWithSecondaryInteraction/index.tsx b/src/components/PressableWithSecondaryInteraction/index.tsx index 0cb53e40325a..5e2de765f733 100644 --- a/src/components/PressableWithSecondaryInteraction/index.tsx +++ b/src/components/PressableWithSecondaryInteraction/index.tsx @@ -1,11 +1,11 @@ import React, {forwardRef, useEffect, useRef} from 'react'; -import {GestureResponderEvent} from 'react-native'; -import {PressableRef} from '@components/Pressable/GenericPressable/types'; +import type {GestureResponderEvent} from 'react-native'; +import type {PressableRef} from '@components/Pressable/GenericPressable/types'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import PressableWithSecondaryInteractionProps from './types'; +import type PressableWithSecondaryInteractionProps from './types'; /** This is a special Pressable that calls onSecondaryInteraction when LongPressed, or right-clicked. */ function PressableWithSecondaryInteraction( diff --git a/src/components/PressableWithSecondaryInteraction/types.ts b/src/components/PressableWithSecondaryInteraction/types.ts index bf999e9692b5..aa67d45d66fb 100644 --- a/src/components/PressableWithSecondaryInteraction/types.ts +++ b/src/components/PressableWithSecondaryInteraction/types.ts @@ -1,5 +1,5 @@ import type {GestureResponderEvent} from 'react-native'; -import {PressableWithFeedbackProps} from '@components/Pressable/PressableWithFeedback'; +import type {PressableWithFeedbackProps} from '@components/Pressable/PressableWithFeedback'; import type {ParsableStyle} from '@styles/utils/types'; type PressableWithSecondaryInteractionProps = PressableWithFeedbackProps & { diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx new file mode 100644 index 000000000000..1b711633ed3b --- /dev/null +++ b/src/components/ProcessMoneyRequestHoldMenu.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Button from './Button'; +import HoldMenuSectionList from './HoldMenuSectionList'; +import type {PopoverAnchorPosition} from './Modal/types'; +import Popover from './Popover'; +import type {AnchorAlignment} from './Popover/types'; +import Text from './Text'; +import TextPill from './TextPill'; + +type ProcessMoneyRequestHoldMenuProps = { + /** Whether the content is visible */ + isVisible: boolean; + + /** Method to trigger when pressing outside of the popover menu to close it */ + onClose: () => void; + + /** Method to trigger when pressing confirm button */ + onConfirm: () => void; + + /** The anchor position of the popover menu */ + anchorPosition?: PopoverAnchorPosition; + + /** The anchor alignment of the popover menu */ + anchorAlignment: AnchorAlignment; + + /** The anchor ref of the popover menu */ + anchorRef: React.RefObject; +}; + +function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosition, anchorAlignment, anchorRef}: ProcessMoneyRequestHoldMenuProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + return ( + + + + {translate('iou.holdEducationalTitle')} + {translate('iou.hold')}; + + +