diff --git a/.eslintrc.js b/.eslintrc.js index ed6f162ad8d4..9f839e45ce75 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,6 +55,11 @@ const restrictedImportPaths = [ name: 'date-fns/locale', message: "Do not import 'date-fns/locale' directly. Please use the submodule import instead, like 'date-fns/locale/en-GB'.", }, + { + name: 'expensify-common', + importNames: ['Device'], + message: "Do not import Device directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.", + }, ]; const restrictedImportPatterns = [ @@ -100,7 +105,6 @@ module.exports = { __DEV__: 'readonly', }, rules: { - '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', @@ -167,6 +171,7 @@ module.exports = { // Rulesdir specific rules 'rulesdir/no-default-props': 'error', + 'rulesdir/prefer-type-fest': 'error', 'rulesdir/no-multiple-onyx-in-file': 'off', 'rulesdir/prefer-underscore-method': 'off', 'rulesdir/prefer-import-module-contents': 'off', diff --git a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts index c486fdbd39f3..5231caa79ed5 100644 --- a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts +++ b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts @@ -1,6 +1,15 @@ import * as core from '@actions/core'; import fs from 'fs'; +type RegressionEntry = { + metadata?: { + creationDate: string; + }; + name: string; + meanDuration: number; + meanCount: number; +}; + const run = () => { // Prefix path to the graphite metric const GRAPHITE_PATH = 'reassure'; @@ -24,11 +33,11 @@ const run = () => { } try { - const current = JSON.parse(entry); + const current: RegressionEntry = JSON.parse(entry); // Extract timestamp, Graphite accepts timestamp in seconds if (current.metadata?.creationDate) { - timestamp = Math.floor(new Date(current.metadata.creationDate as string).getTime() / 1000); + timestamp = Math.floor(new Date(current.metadata.creationDate).getTime() / 1000); } if (current.name && current.meanDuration && current.meanCount && timestamp) { diff --git a/.github/libs/GitUtils.ts b/.github/libs/GitUtils.ts index 684e7c76aa86..dc8ae037be28 100644 --- a/.github/libs/GitUtils.ts +++ b/.github/libs/GitUtils.ts @@ -66,11 +66,11 @@ function getCommitHistoryAsJSON(fromTag: string, toTag: string): Promise { + spawnedProcess.stdout.on('data', (chunk: Buffer) => { console.log(chunk.toString()); stdout += chunk.toString(); }); - spawnedProcess.stderr.on('data', (chunk) => { + spawnedProcess.stderr.on('data', (chunk: Buffer) => { console.error(chunk.toString()); stderr += chunk.toString(); }); diff --git a/.github/scripts/enforceRedirect.sh b/.github/scripts/enforceRedirect.sh new file mode 100755 index 000000000000..4d7d169b01c5 --- /dev/null +++ b/.github/scripts/enforceRedirect.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# HelpDot - Whenever an article is moved/renamed/deleted we should verify that +# we have added a redirect link for it in redirects.csv. This ensures that we don't have broken links. + +declare -r RED='\033[0;31m' +declare -r GREEN='\033[0;32m' +declare -r NC='\033[0m' + +declare -r ARTICLES_DIRECTORY="docs/articles" +declare -r REDIRECTS_FILE="docs/redirects.csv" + +hasRenamedOrDeletedArticle=false +hasModifiedRedirect=false + +if git log origin/main..HEAD --name-status --pretty=format: $ARTICLES_DIRECTORY | grep -q -E "^(R|D)" +then + echo "Articles have been renamed/moved/deleted" + hasRenamedOrDeletedArticle=true +fi + +if git log origin/main..HEAD --name-status --pretty=format: $REDIRECTS_FILE | grep -q -E "^(M)" +then + echo "Redirects.csv has been modified" + hasModifiedRedirect=true +fi + +if [[ $hasRenamedOrDeletedArticle == true && $hasModifiedRedirect == false ]] +then + echo -e "${RED}Articles have been renamed or deleted. Please add a redirect link for the old article links in redirects.csv${NC}" + exit 1 +fi + +echo -e "${GREEN}Articles aren't moved or deleted, or a redirect has been added. Please verify that a redirect has been added for all the files moved or deleted${NC}" +exit 0 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 63148f9e4eb5..a792d069151b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,7 @@ jobs: with: ref: staging token: ${{ secrets.OS_BOTIFY_TOKEN }} - + - name: Setup git for OSBotify uses: ./.github/actions/composite/setupGitForOSBotifyApp id: setupGitForOSBotify @@ -65,9 +65,6 @@ jobs: PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }} - name: 🚀 Create release to trigger production deploy 🚀 - uses: softprops/action-gh-release@affa18ef97bc9db20076945705aba8c516139abd - with: - tag_name: ${{ env.PRODUCTION_VERSION }} - body: ${{ steps.getReleaseBody.outputs.RELEASE_BODY }} + run: gh release create ${{ env.PRODUCTION_VERSION }} --notes ${{ steps.getReleaseBody.outputs.RELEASE_BODY }} env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml index cda33d39102e..8a6237832340 100644 --- a/.github/workflows/deployExpensifyHelp.yml +++ b/.github/workflows/deployExpensifyHelp.yml @@ -29,9 +29,12 @@ jobs: env: IS_PR_FROM_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest + continue-on-error: true steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup NodeJS uses: ./.github/actions/composite/setupNode @@ -42,6 +45,9 @@ jobs: - name: Check for duplicates and cycles in redirects.csv run: ./.github/scripts/verifyRedirect.sh + - name: Enforce that a redirect link has been created + run: ./.github/scripts/enforceRedirect.sh + - name: Build with Jekyll uses: actions/jekyll-build-pages@0143c158f4fa0c5dcd99499a5d00859d79f70b0e with: diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 4ad6d54e2f24..5ceb760a452b 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -134,7 +134,7 @@ jobs: name: Build and deploy Desktop needs: validateActor if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} - runs-on: macos-13-large + runs-on: macos-14-large steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index fe6ea5bfc016..10912aaeb436 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -228,7 +228,7 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} env: PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }} - runs-on: macos-13-large + runs-on: macos-14-large steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml index d052b343cf0c..f65319f14be4 100644 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ b/.github/workflows/testGithubActionsWorkflows.yml @@ -25,6 +25,13 @@ jobs: - name: Setup Homebrew uses: Homebrew/actions/setup-homebrew@master + - name: Login to GitHub Container Regstry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: OSBotify + password: ${{ secrets.GITHUB_TOKEN }} + - name: Install Act run: brew install act diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index 4d638020cd42..09d881846b1e 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -3,6 +3,7 @@ /* eslint-disable no-param-reassign */ /* eslint-disable @typescript-eslint/naming-convention */ +import type Environment from 'config/webpack/types'; import dotenv from 'dotenv'; import path from 'path'; import {DefinePlugin} from 'webpack'; @@ -18,6 +19,8 @@ type CustomWebpackConfig = { }; }; +type CustomWebpackFunction = ({file, platform}: Environment) => CustomWebpackConfig; + let envFile: string; switch (process.env.ENV) { case 'production': @@ -31,9 +34,9 @@ switch (process.env.ENV) { } const env = dotenv.config({path: path.resolve(__dirname, `../${envFile}`)}); -const custom: CustomWebpackConfig = require('../config/webpack/webpack.common').default({ - envFile, -}); +const customFunction: CustomWebpackFunction = require('../config/webpack/webpack.common').default; + +const custom: CustomWebpackConfig = customFunction({file: envFile}); const webpackConfig = ({config}: {config: Configuration}) => { if (!config.resolve) { diff --git a/android/app/build.gradle b/android/app/build.gradle index e243f65c9efb..84bf8c678f56 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001048108 - versionName "1.4.81-8" + versionCode 1001048400 + versionName "1.4.84-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/animations/Abracadabra.lottie b/assets/animations/Abracadabra.lottie new file mode 100644 index 000000000000..8805aed1944e Binary files /dev/null and b/assets/animations/Abracadabra.lottie differ diff --git a/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg b/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg new file mode 100644 index 000000000000..17ff47e6ca12 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js index e4ed685f65fe..5a995fb5de91 100644 --- a/config/electronBuilder.config.js +++ b/config/electronBuilder.config.js @@ -45,6 +45,12 @@ module.exports = { notarize: { teamId: '368M544MTT', }, + target: [ + { + target: 'dmg', + arch: ['universal'], + }, + ], }, dmg: { title: 'New Expensify', diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 9d397b9557a3..bedd7e50ef94 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -4,14 +4,27 @@ import dotenv from 'dotenv'; import fs from 'fs'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import path from 'path'; -import type {Configuration} from 'webpack'; +import type {Compiler, Configuration} from 'webpack'; import {DefinePlugin, EnvironmentPlugin, IgnorePlugin, ProvidePlugin} from 'webpack'; import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; import CustomVersionFilePlugin from './CustomVersionFilePlugin'; import type Environment from './types'; +// importing anything from @vue/preload-webpack-plugin causes an error +type Options = { + rel: string; + as: string; + fileWhitelist: RegExp[]; + include: string; +}; + +type PreloadWebpackPluginClass = { + new (options?: Options): PreloadWebpackPluginClass; + apply: (compiler: Compiler) => void; +}; + // require is necessary, there are no types for this package and the declaration file can't be seen by the build process which causes an error. -const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin'); +const PreloadWebpackPlugin: PreloadWebpackPluginClass = require('@vue/preload-webpack-plugin'); const includeModules = [ 'react-native-animatable', diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index f3e928da8cb0..6af3a82c2ff6 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -23,6 +23,7 @@ - [Type imports/exports](#type-importsexports) - [Refs](#refs) - [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) + - [Default value for inexistent IDs](#default-value-for-inexistent-IDs) - [Naming Conventions](#naming-conventions) - [Type names](#type-names) - [Prop callbacks](#prop-callbacks) @@ -47,7 +48,6 @@ - [Forwarding refs](#forwarding-refs) - [Hooks and HOCs](#hooks-and-hocs) - [Stateless components vs Pure Components vs Class based components vs Render Props](#stateless-components-vs-pure-components-vs-class-based-components-vs-render-props---when-to-use-what) - - [Composition](#composition) - [Use Refs Appropriately](#use-refs-appropriately) - [Are we allowed to use [insert brand new React feature]?](#are-we-allowed-to-use-insert-brand-new-react-feature-why-or-why-not) - [React Hooks: Frequently Asked Questions](#react-hooks-frequently-asked-questions) @@ -471,6 +471,24 @@ if (ref.current && 'getBoundingClientRect' in ref.current) { - [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) +### Default value for inexistent IDs + + Use `'-1'` or `-1` when there is a possibility that the ID property of an Onyx value could be `null` or `undefined`. + +``` ts +// BAD +const foo = report?.reportID ?? ''; +const bar = report?.reportID ?? '0'; + +report ? report.reportID : '0'; +report ? report.reportID : ''; + +// GOOD +const foo = report?.reportID ?? '-1'; + +report ? report.reportID : '-1'; +``` + ## Naming Conventions ### Type names @@ -1075,51 +1093,6 @@ Class components are DEPRECATED. Use function components and React hooks. [https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function](https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function) -### Composition - -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. - -From React's documentation - ->Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions. ->If you want to reuse non-UI functionality between components, we suggest extracting it into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it. - - ```ts - // BAD - export default compose( - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), - )(Component); - - // GOOD - export default withCurrentUserPersonalDetails( - withReportOrNotFound()( - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component), - ), - ); - - // GOOD - alternative to HOC nesting - const ComponentWithOnyx = withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component); - const ComponentWithReportOrNotFound = withReportOrNotFound()(ComponentWithOnyx); - export default withCurrentUserPersonalDetails(ComponentWithReportOrNotFound); - ``` - -**Note:** If you find that none of these approaches work for you, please ask an Expensify engineer for guidance via Slack or GitHub. - ### Use Refs Appropriately React's documentation explains refs in [detail](https://reactjs.org/docs/refs-and-the-dom.html). It's important to understand when to use them and how to use them to avoid bugs and hard to maintain code. diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts index 132848c5da9e..4403f989263c 100644 --- a/desktop/createDownloadQueue.ts +++ b/desktop/createDownloadQueue.ts @@ -17,6 +17,11 @@ type DownloadItem = { options: Options; }; +type CreateDownloadQueue = () => { + enqueueDownloadItem: (item: DownloadItem) => void; + dequeueDownloadItem: () => DownloadItem | undefined; +}; + /** * Returns the filename with extension based on the given name and MIME type. * @param name - The name of the file. @@ -28,7 +33,7 @@ const getFilenameFromMime = (name: string, mime: string): string => { return `${name}.${extensions}`; }; -const createDownloadQueue = () => { +const createDownloadQueue: CreateDownloadQueue = () => { const downloadItemProcessor = (item: DownloadItem): Promise => new Promise((resolve, reject) => { let downloadTimeout: NodeJS.Timeout; @@ -114,3 +119,4 @@ const createDownloadQueue = () => { }; export default createDownloadQueue; +export type {DownloadItem, CreateDownloadQueue}; diff --git a/desktop/main.ts b/desktop/main.ts index 0f4774d3b73b..57ea647cc3e2 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -13,9 +13,10 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type PlatformSpecificUpdater from '@src/setup/platformSetup/types'; import type {Locale} from '@src/types/onyx'; +import type {CreateDownloadQueue, DownloadItem} from './createDownloadQueue'; import ELECTRON_EVENTS from './ELECTRON_EVENTS'; -const createDownloadQueue = require('./createDownloadQueue').default; +const createDownloadQueue: CreateDownloadQueue = require('./createDownloadQueue').default; const port = process.env.PORT ?? 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; @@ -617,7 +618,7 @@ const mainWindow = (): Promise => { const downloadQueue = createDownloadQueue(); ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { - const downloadItem = { + const downloadItem: DownloadItem = { ...downloadData, win: browserWindow, }; diff --git a/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md index 8cf0a18ba529..96653bc69763 100644 --- a/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md +++ b/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md @@ -4,69 +4,51 @@ description: How to connect a business bank account to New Expensify ---
-Adding a business bank account unlocks a myriad of features and automation in Expensify, such as: -- Reimburse expenses via direct bank transfer -- Pay bills -- Collect invoice payments -- Issue the Expensify Card - -# To connect a bank account -1. Go to **Settings > Workspaces > _Workspace Name_ > Bank account > Connect bank account** -2. Click **Connect online with Plaid** -3. Click **Continue** -4. When you reach the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access -5. Login to the business bank account: -- If the bank is not listed, click the X to go back to the connection type -- Here you’ll see the option to **Connect Manually** -- Enter your account and routing numbers -6. Enter your bank login credentials: -- If your bank requires additional security measures, you will be directed to obtain and enter a security code -- If you have more than one account available to choose from, you will be directed to choose the desired account - -## Enter company information -This is where you’ll add the legal business name as well as several other company details. - -- **Company address**: The company address must be located in the US and a physical location (If you input a maildrop address, PO box, or UPS Store, the address will be flagged for review, and adding the bank account to Expensify will be delayed) -- **Tax Identification Number**: This is the identification number that was assigned to the business by the IRS -- **Company website**: A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com -- **Industry Classification Code**: You can locate a list of Industry Classification Codes [here]([url](https://www.census.gov/naics/?input=software&year=2022)) - -## Enter personal information -Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section: -- The address must be a physical address -- The address must be located in the US -- The SSN must be US-issued - -This does not need to be a signor on the bank account. If someone other than the Expensify account owner enters their personal information in this section, the details will be flagged for review, and adding the bank account to Expensify will be delayed. - -## Upload ID +To connect a bank account in New Expensify, you must first enable the Make or Track Payments Workflow. +# Step 1: Enable Make or track payments +1. Head to **Workspaces** > **More Features** > **Enable Workflows** +2. From there, a Workflows setting will appear in the left-hand menu +3. Click on **Workflows** +4. Enable **Make or track payments** + +# Step 2: Connect bank account +1. Click Connect bank account +2. Select either Connect online with Plaid (preferred) or Connect manually +3. Enter bank details + +# Step 4: Upload ID After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following: 1. Upload a photo of the front and back of your ID (this cannot be a photo of an existing image) -2. Use your device to take a selfie and record a short video of yourself +2. Use your device to take a selfie and record a short video of yourself **Your ID must be:** - Issued in the US - Current (ie: the expiration date must be in the future) -## Additional Information -Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate: -- A Beneficial Owner refers to an **individual** who owns 25% or more of the business. -- If you or another **individual** owns 25% or more of the business, please check the appropriate box +# Step 5: Enter company information +This is where you’ll add the legal business name as well as several other company details. +- **Company address:** The company address must be located in the US and a physical location (If you input a maildrop address, PO box, or UPS Store, the address will be flagged for review, and adding the bank account to Expensify will be delayed) +- **Tax Identification Number:** This is the identification number that was assigned to the business by the IRS +- **Company website:** A company website is required to use most of Expensify’s payment features. +- **Industry Classification Code:** You can locate a list of Industry Classification Codes [here](https://www.census.gov/naics/?input=software&year=2022). + +# Step 6: Additional Information +Check the appropriate box under Additional Information, accept the agreement terms, and verify that all of the information is true and accurate: +- A Beneficial Owner refers to an individual who owns 25% or more of the business. +- If you or another individual owns 25% or more of the business, please check the appropriate box - If someone else owns 25% or more of the business, you will be prompted to provide their personal information If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section. -The details you submitted may require additional review. If that's the case, you'll receive a message from the Concierge outlining the next steps. Otherwise, your bank account will be connected automatically. +The details you submitted may require additional review. If that's the case, you'll receive a message from the Concierge outlining the next steps. Otherwise, your bank account will be connected automatically. {% include faq-begin.md %} ## What are the general requirements for adding a business bank account? -To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US), or to issue Expensify Cards: -- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location. -If you are adding the bank account to Expensify, you must do so from your Expensify account settings. -- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US-issued photo ID. For using features related to US ACH, your ID must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address -- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify. +To add a business bank account to issue reimbursements via ACH (US) or to issue Expensify Cards: +- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We cannot accept a PO Box or MailDrop location. +- If you are adding the bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US-issued photo ID. Your ID must be issued by the United States to use features related to US ACH. You and any Beneficial Owner (if one exists) must also have a US address. ## What is a Beneficial Owner? @@ -74,25 +56,27 @@ A Beneficial Owner refers to an **individual** who owns 25% or more of the busin ## What do I do if the Beneficial Owner section only asks for personal details, but my organization is owned by another company? -Please indicate you have a Beneficial Owner only if it is an individual who owns 25% or more of the business. +Please indicate you have a Beneficial Owner only if it is an individual who owns 25% or more of the business. ## Why can’t I input my address or upload my ID? -Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account and then share access with you once it is verified. +When adding a verified business bank account in Expensify, the individual adding the account and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account and then share access with you once it is verified. ## Why am I asked for documents when adding my bank account? -When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. -If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. +When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. + +If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. ## I don’t see all three microtransactions I need to validate my bank account. What should I do? -It's a good idea to wait until the end of that second business day. If you still don’t see them, please contact your bank and ask them to whitelist our ACH IDs **1270239450**, **4270239450**, and **2270239450**. Expensify’s ACH Originator Name is "Expensify." +Wait until the end of the second business day. If you still don’t see them, please contact your bank and ask them to whitelist our ACH IDs **1270239450**, **4270239450**, and **2270239450**. Expensify’s ACH Originator Name is "Expensify." Once that's all set, make sure to contact your account manager or concierge, and our team will be able to re-trigger those three test transactions! + {% include faq-end.md %}
diff --git a/docs/articles/new-expensify/settings/Add-profile-photo.md b/docs/articles/new-expensify/settings/Add-profile-photo.md new file mode 100644 index 000000000000..60e56deaafbc --- /dev/null +++ b/docs/articles/new-expensify/settings/Add-profile-photo.md @@ -0,0 +1,21 @@ +--- +title: Add profile photo +description: Add an image to your profile +--- +
+ +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click the Edit pencil icon next to your profile image or icon and select **Upload Image** to choose a new image from your saved files. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap the Edit pencil icon next to your profile image or icon and select **Upload Image** to choose a new image from your saved files. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/travel/Book-with-Expensify-Travel.md b/docs/articles/new-expensify/travel/Book-with-Expensify-Travel.md new file mode 100644 index 000000000000..5d25670ac5ab --- /dev/null +++ b/docs/articles/new-expensify/travel/Book-with-Expensify-Travel.md @@ -0,0 +1,89 @@ +--- +title: Book with Expensify Travel +description: Book flights, hotels, cars, trains, and more with Expensify Travel +--- +
+ +Expensify Travel allows members to search and book flights, hotels, cars, and trains globally at the most competitive rates available. + +With Expensify Travel, you can: +- Search and book travel arrangements all in one place +- Book travel for yourself or for someone else +- Get real-time support by chat or phone +- Manage all your T&E expenses in Expensify +- Create specific rules for booking travel +- Enable approvals for out-of-policy trips +- Book with any credit card on the market +- Book with the Expensify Card to get cash back and automatically reconcile transactions + +There is a flat fee of $15 per trip booked. A single trip can include multiple bookings, such as a flight, a hotel, and a car rental. + +# Book travel + +To book travel from the Expensify web app, + +1. Click the **Travel** tab. +2. Click **Book or manage travel**. +3. Use the icons at the top to select the type of travel arrangement you want to book: flights, hotels, cars, or trains. +4. Enter the travel information relevant to the travel arrangement selected (for example, the destination, dates of travel, etc.). +5. Select all the details for the arrangement you want to book. +6. Review the booking details and click **Book Flight / Book Hotel / Book Car / Book Rail** to complete the booking. + +The traveler is emailed an itinerary of the booking. Additionally, +- Their travel details are added to a Trip chat room under their primary workspace. +- An expense report for the trip is created. +- If booked with an Expensify Card, the trip is automatically reconciled. + +{% include info.html %} +The travel itinerary is also emailed to the traveler’s [copilots](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot), if applicable. +{% include end-info.html %} + +
+ +
+Expensify Travel allows members to search and book flights, hotels, cars, and trains globally at the most competitive rates available. + +With Expensify Travel, you can: +- Search and book travel arrangements all in one place +- Book travel for yourself or for someone else +- Get real-time support by chat or phone +- Manage all your T&E expenses in Expensify +- Create specific rules for booking travel +- Enable approvals for out-of-policy trips +- Book with any credit card on the market +- Book with the Expensify Card to get cash back and automatically reconcile transactions + +There is a flat fee of $15 per trip booked. A single trip can include multiple bookings, such as a flight, a hotel, and a car rental. + +# Book travel + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Book travel**. +2. Click **Book or manage travel**. +3. Agree to the terms and conditions and click **Continue**. +4. Use the icons at the top to select the type of travel arrangement you want to book: flights, hotels, cars, or trains. +5. Enter the travel information relevant to the travel arrangement selected (for example, the destination, dates of travel, etc.). +6. Select all the details for the arrangement you want to book. +7. Review the booking details and click **Book Flight / Book Hotel / Book Car / Book Rail** to complete the booking. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon in the bottom menu and select **Book travel**. +2. Tap **Book or manage travel**. +3. Agree to the terms and conditions and tap **Continue**. +4. Use the icons at the top to select the type of travel arrangement you want to book: flights, hotels, cars, or trains. +5. Enter the travel information relevant to the travel arrangement selected (for example, the destination, dates of travel, etc.). +6. Select all the details for the arrangement you want to book. +7. Review the booking details and click **Book Flight / Book Hotel / Book Car / Book Rail** to complete the booking. +{% include end-option.html %} + +{% include end-selector.html %} + +The traveler is emailed an itinerary of the booking. Additionally, +- Their travel details are added to a Trip chat room under their primary workspace. +- An expense report for the trip is created. +- If booked with an Expensify Card, the trip is automatically reconciled. + +
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 54b78cd6289c..94edac3cdbea 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.81 + 1.4.84 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.81.8 + 1.4.84.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 9f63f7ae9e98..d33022e74cdd 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.81 + 1.4.84 CFBundleSignature ???? CFBundleVersion - 1.4.81.8 + 1.4.84.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index af2cb1e4a29c..0129a591f4a9 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.81 + 1.4.84 CFBundleVersion - 1.4.81.8 + 1.4.84.0 NSExtension NSExtensionPointIdentifier diff --git a/jest/setup.ts b/jest/setup.ts index 174e59a7e493..416306ce8426 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -21,8 +21,8 @@ jest.mock('react-native-onyx/dist/storage', () => mockStorage); jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter'); // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise -jest.spyOn(console, 'debug').mockImplementation((...params) => { - if (params[0].indexOf('Timing:') === 0) { +jest.spyOn(console, 'debug').mockImplementation((...params: string[]) => { + if (params[0].startsWith('Timing:')) { return; } diff --git a/package-lock.json b/package-lock.json index 7f00798e6d29..618057fba1f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.81-8", + "version": "1.4.84-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.81-8", + "version": "1.4.84-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -26,7 +26,7 @@ "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", - "@kie/mock-github": "^1.0.0", + "@kie/mock-github": "2.0.1", "@onfido/react-native-sdk": "10.6.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", @@ -64,6 +64,7 @@ "expo-av": "~13.10.4", "expo-image": "1.11.0", "expo-image-manipulator": "11.8.0", + "focus-trap-react": "^10.2.3", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-expo": "50.0.1", @@ -102,7 +103,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.32", + "react-native-onyx": "2.0.48", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -207,7 +208,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.50", + "eslint-config-expensify": "^2.0.51", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -240,7 +241,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "^4.10.2", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", @@ -254,14 +255,6 @@ "npm": "10.7.0" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@actions/core": { "version": "1.10.0", "dev": true, @@ -3557,6 +3550,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@expensify/react-native-live-markdown": { "version": "0.1.84", "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.84.tgz", @@ -5681,6 +5683,19 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "dev": true, @@ -7051,48 +7066,12 @@ "act-js": "bin/act" } }, - "node_modules/@kie/act-js/node_modules/@kie/mock-github": { - "version": "2.0.0", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@octokit/openapi-types-ghec": "^18.0.0", - "ajv": "^8.11.0", - "express": "^4.18.1", - "fast-glob": "^3.2.12", - "fs-extra": "^10.1.0", - "nock": "^13.2.7", - "simple-git": "^3.8.0", - "totalist": "^3.0.0" - } - }, - "node_modules/@kie/act-js/node_modules/@octokit/openapi-types-ghec": { - "version": "18.1.1", - "license": "MIT" - }, - "node_modules/@kie/act-js/node_modules/fs-extra": { - "version": "10.1.0", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@kie/act-js/node_modules/totalist": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@kie/mock-github": { - "version": "1.1.0", - "license": "SEE LICENSE IN LICENSE", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@kie/mock-github/-/mock-github-2.0.1.tgz", + "integrity": "sha512-G1FD/jg1KyW7a6NvKI4uEVJCK3eJnzXkh4Ikxn2is5tiNC980lavi8ak6bn1QEFEgpYcfM4DpZM3yHDfOmyLuQ==", "dependencies": { - "@octokit/openapi-types-ghec": "^14.0.0", + "@octokit/openapi-types-ghec": "^18.0.0", "ajv": "^8.11.0", "express": "^4.18.1", "fast-glob": "^3.2.12", @@ -7542,8 +7521,9 @@ "license": "MIT" }, "node_modules/@octokit/openapi-types-ghec": { - "version": "14.0.0", - "license": "MIT" + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types-ghec/-/openapi-types-ghec-18.1.1.tgz", + "integrity": "sha512-5Ri7FLYX4gJSdG+G0Q8QDca/gOLfkPN4YR2hkbVg6hEL+0N62MIsJPTyNaT9pGEXCLd1KbYV6Lh3T2ggsmyBJw==" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "3.1.0", @@ -19370,14 +19350,16 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.50.tgz", - "integrity": "sha512-I+OMkEprqEWlSCZGJBJxpt2Wg4HQ41/QqpKVfcADiQ3xJ76bZ1mBueqz6DR4jfph1xC6XVRl4dqGNlwbeU/2Rg==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.51.tgz", + "integrity": "sha512-qEUPCI9vsAi5c5E6zM4QEYal13hIRHFvonf4U/x0JI4ceMdAejOoq/Zvt9r1ZwKT1RmA8eRoGWWIQ/4O/9hJPg==", "dev": true, "dependencies": { "@lwc/eslint-plugin-lwc": "^1.7.2", + "@typescript-eslint/parser": "^7.12.0", + "@typescript-eslint/utils": "^7.12.0", "babel-eslint": "^10.1.0", - "eslint": "^7.32.0", + "eslint": "^8.56.0", "eslint-config-airbnb": "19.0.4", "eslint-config-airbnb-base": "15.0.0", "eslint-plugin-es": "^4.1.0", @@ -19390,6 +19372,468 @@ "underscore": "^1.13.6" } }, + "node_modules/eslint-config-expensify/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-expensify/node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/eslint-config-expensify/node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/eslint-config-expensify/node_modules/@typescript-eslint/parser": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", + "integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-expensify/node_modules/@typescript-eslint/scope-manager": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", + "integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-expensify/node_modules/@typescript-eslint/types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", + "integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-expensify/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", + "integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-expensify/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-config-expensify/node_modules/@typescript-eslint/utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", + "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/eslint-config-expensify/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", + "integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-expensify/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint-config-expensify/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-config-expensify/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint-config-expensify/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint-config-expensify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-config-expensify/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint-config-expensify/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint-config-expensify/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint-config-expensify/node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-expensify/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-expensify/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-expensify/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-expensify/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint-config-expensify/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-config-expensify/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-config-expensify/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-config-expensify/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint-config-expensify/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint-config-expensify/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-config-expensify/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-config-prettier": { "version": "8.10.0", "dev": true, @@ -20025,22 +20469,6 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/optionator": { - "version": "0.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -21287,6 +21715,28 @@ "node": ">=0.4.0" } }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/focus-trap-react": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.3.tgz", + "integrity": "sha512-YXBpFu/hIeSu6NnmV2xlXzOYxuWkoOtar9jzgp3lOmjWLWY59C/b8DtDHEAV4SPU07Nd/t+nS/SBNGkhUBFmEw==", + "dependencies": { + "focus-trap": "^7.5.4", + "tabbable": "^6.2.0" + }, + "peerDependencies": { + "prop-types": "^15.8.1", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.5", "funding": [ @@ -29497,6 +29947,23 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -31472,17 +31939,17 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.32", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.32.tgz", - "integrity": "sha512-tB9wqMJGTLOYfrfplRP+9aq5JdD8w/hV/OZsMAVH+ewbE1zLY8OymUsAsIFdF1v+cB8HhehP569JVLZmhm6bsg==", + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.48.tgz", + "integrity": "sha512-qJQTWMzhLD7zy5/9vBZJSlb3//fYVx3obTdsw1tXZDVOZXUcBmd6evA2tzGe5KT8H2sIbvFR1UyvwE03oOqYYg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.6" }, "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" + "node": ">=20.14.0", + "npm": ">=10.7.0" }, "peerDependencies": { "idb-keyval": "^6.2.1", @@ -35100,6 +35567,11 @@ "version": "3.2.4", "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/table": { "version": "6.8.1", "dev": true, @@ -35760,11 +36232,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.1", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -36081,9 +36554,10 @@ } }, "node_modules/typescript": { - "version": "5.3.3", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -37740,6 +38214,15 @@ "version": "4.0.15", "license": "MIT" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/package.json b/package.json index 52b6b72f43ab..babb1610e34b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.81-8", + "version": "1.4.84-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.", @@ -78,7 +78,7 @@ "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", - "@kie/mock-github": "^1.0.0", + "@kie/mock-github": "2.0.1", "@onfido/react-native-sdk": "10.6.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", @@ -116,6 +116,7 @@ "expo-av": "~13.10.4", "expo-image": "1.11.0", "expo-image-manipulator": "11.8.0", + "focus-trap-react": "^10.2.3", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-expo": "50.0.1", @@ -154,7 +155,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.32", + "react-native-onyx": "2.0.48", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -259,7 +260,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.50", + "eslint-config-expensify": "^2.0.51", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -292,7 +293,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "^4.10.2", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", diff --git a/patches/@react-navigation+native+6.1.12.patch b/patches/@react-navigation+native+6.1.12.patch index d53f8677d225..0ac57a865dfd 100644 --- a/patches/@react-navigation+native+6.1.12.patch +++ b/patches/@react-navigation+native+6.1.12.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js b/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js -index 16fdbef..e660dd6 100644 +index 16fdbef..231a520 100644 --- a/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js +++ b/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js @@ -1,8 +1,23 @@ @@ -68,7 +68,7 @@ index 16fdbef..e660dd6 100644 // Need to keep the hash part of the path if there was no previous history entry // or the previous history entry had the same path - let pathWithHash = path; -+ let pathWithHash = path.replace(/(\/{2,})|(\/$)/g, (match, p1) => (p1 ? '/' : '')); ++ let pathWithHash = path.replace(/(\/{2,})/g, '/'); if (!items.length || items.findIndex(item => item.id === id) < 0) { // There are two scenarios for creating an array with only one history record: // - When loaded id not found in the items array, this function by default will replace diff --git a/patches/react-native-web+0.19.12+006+remove focus trap from modal.patch b/patches/react-native-web+0.19.12+006+remove focus trap from modal.patch new file mode 100644 index 000000000000..14dbc88b0b1c --- /dev/null +++ b/patches/react-native-web+0.19.12+006+remove focus trap from modal.patch @@ -0,0 +1,20 @@ +diff --git a/node_modules/react-native-web/dist/exports/Modal/index.js b/node_modules/react-native-web/dist/exports/Modal/index.js +index d5df021..e2c46cf 100644 +--- a/node_modules/react-native-web/dist/exports/Modal/index.js ++++ b/node_modules/react-native-web/dist/exports/Modal/index.js +@@ -86,13 +86,11 @@ var Modal = /*#__PURE__*/React.forwardRef((props, forwardedRef) => { + onDismiss: onDismissCallback, + onShow: onShowCallback, + visible: visible +- }, /*#__PURE__*/React.createElement(ModalFocusTrap, { +- active: isActive + }, /*#__PURE__*/React.createElement(ModalContent, _extends({}, rest, { + active: isActive, + onRequestClose: onRequestClose, + ref: forwardedRef, + transparent: transparent +- }), children)))); ++ }), children))); + }); + export default Modal; +\ No newline at end of file diff --git a/patches/react-pdf+7.7.1.patch b/patches/react-pdf+7.7.3.patch similarity index 93% rename from patches/react-pdf+7.7.1.patch rename to patches/react-pdf+7.7.3.patch index f6ec8d8c1685..5b1b3ebb6f6e 100644 --- a/patches/react-pdf+7.7.1.patch +++ b/patches/react-pdf+7.7.3.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-pdf/dist/esm/Document.js b/node_modules/react-pdf/dist/esm/Document.js -index 493ff15..8d5e734 100644 +index b1c5a81..569769e 100644 --- a/node_modules/react-pdf/dist/esm/Document.js +++ b/node_modules/react-pdf/dist/esm/Document.js @@ -261,6 +261,7 @@ const Document = forwardRef(function Document(_a, ref) { diff --git a/src/CONST.ts b/src/CONST.ts index d3132b17a2ea..6a936bc97087 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -569,6 +569,7 @@ const CONST = { LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, ACH_TERMS_URL: `${USE_EXPENSIFY_URL}/achterms`, WALLET_AGREEMENT_URL: `${USE_EXPENSIFY_URL}/walletagreement`, + BANCORP_WALLET_AGREEMENT_URL: `${USE_EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, HELP_LINK_URL: `${USE_EXPENSIFY_URL}/usa-patriot-act`, ELECTRONIC_DISCLOSURES_URL: `${USE_EXPENSIFY_URL}/esignagreement`, GITHUB_RELEASE_URL: 'https://api.github.com/repos/expensify/app/releases/latest', @@ -597,7 +598,6 @@ const CONST = { ADMIN_POLICIES_URL: 'admin_policies', ADMIN_DOMAINS_URL: 'admin_domains', INBOX: 'inbox', - DISMMISSED_REASON: '?dismissedReason=missingFeatures', }, SIGN_IN_FORM_WIDTH: 300, @@ -1429,6 +1429,7 @@ const CONST = { }, STEP: { // In the order they appear in the Wallet flow + ADD_BANK_ACCOUNT: 'AddBankAccountStep', ADDITIONAL_DETAILS: 'AdditionalDetailsStep', ADDITIONAL_DETAILS_KBA: 'AdditionalDetailsKBAStep', ONFIDO: 'OnfidoStep', @@ -1465,6 +1466,7 @@ const CONST = { CONCIERGE: 'CONCIERGE_NAVIGATE', }, MTL_WALLET_PROGRAM_ID: '760', + BANCORP_WALLET_PROGRAM_ID: '660', PROGRAM_ISSUERS: { EXPENSIFY_PAYMENTS: 'Expensify Payments LLC', BANCORP_BANK: 'The Bancorp Bank', @@ -3461,6 +3463,8 @@ const CONST = { TIMER: 'timer', /** Use for toolbars containing action buttons or components. */ TOOLBAR: 'toolbar', + /** Use for navigation elements */ + NAVIGATION: 'navigation', }, TRANSLATION_KEYS: { ATTACHMENT: 'common.attachment', @@ -4821,6 +4825,15 @@ const CONST = { }, SUBSCRIPTION_SIZE_LIMIT: 20000, + + PAYMENT_CARD_CURRENCY: { + USD: 'USD', + AUD: 'AUD', + GBP: 'GBP', + NZD: 'NZD', + }, + + SUBSCRIPTION_PRICE_FACTOR: 2, SUBSCRIPTION_POSSIBLE_COST_SAVINGS: { COLLECT_PLAN: 10, CONTROL_PLAN: 18, @@ -4843,6 +4856,8 @@ const CONST = { TRANSLATION_KEY: 'feedbackSurvey.businessClosing', }, }, + + EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[], } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; @@ -4851,6 +4866,8 @@ type IOUType = ValueOf; type IOUAction = ValueOf; type IOURequestType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType}; +type SubscriptionType = ValueOf; + +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType}; export default CONST; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ed20d388bb87..c1fdd68951fa 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -102,7 +102,10 @@ const ROUTES = { SETTINGS_PRONOUNS: 'settings/profile/pronouns', SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_SUBSCRIPTION: 'settings/subscription', - SETTINGS_SUBSCRIPTION_SIZE: 'settings/subscription/subscription-size', + SETTINGS_SUBSCRIPTION_SIZE: { + route: 'settings/subscription/subscription-size', + getRoute: (canChangeSize: 0 | 1) => `settings/subscription/subscription-size?canChangeSize=${canChangeSize}` as const, + }, SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card', SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', @@ -140,12 +143,7 @@ const ROUTES = { }, SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', - SETTINGS_ADD_BANK_ACCOUNT_REFACTOR: 'settings/wallet/add-bank-account-refactor', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', - // TODO: Added temporarily for testing purposes, remove after refactor - https://github.com/Expensify/App/issues/36648 - SETTINGS_ENABLE_PAYMENTS_REFACTOR: 'settings/wallet/enable-payments-refactor', - // TODO: Added temporarily for testing purposes, remove after refactor - https://github.com/Expensify/App/issues/36648 - SETTINGS_ENABLE_PAYMENTS_TEMPORARY_TERMS: 'settings/wallet/enable-payments-temporary-terms', SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: { route: 'settings/wallet/card/:domain/digital-details/update-address', getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9f7277a0ad0f..f884cca94ef5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -42,7 +42,6 @@ const SCREENS = { APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links', ADD_DEBIT_CARD: 'Settings_Add_Debit_Card', ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', - ADD_BANK_ACCOUNT_REFACTOR: 'Settings_Add_Bank_Account_Refactor', CLOSE: 'Settings_Close', TWO_FACTOR_AUTH: 'Settings_TwoFactorAuth', REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged', @@ -89,10 +88,6 @@ const SCREENS = { TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance', CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account', ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', - // TODO: Added temporarily for testing purposes, remove after refactor - https://github.com/Expensify/App/issues/36648 - ENABLE_PAYMENTS_REFACTOR: 'Settings_Wallet_EnablePayments_Refactor', - // TODO: Added temporarily for testing purposes, remove after refactor - https://github.com/Expensify/App/issues/36648 - ENABLE_PAYMENTS_TEMPORARY_TERMS: 'Settings_Wallet_EnablePayments_Temporary_Terms', CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', diff --git a/src/TIMEZONES.ts b/src/TIMEZONES.ts index 238563134872..69ef89e7467e 100644 --- a/src/TIMEZONES.ts +++ b/src/TIMEZONES.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ // All timezones were taken from: https://raw.githubusercontent.com/leon-do/Timezones/main/timezone.json +import type {TupleToUnion} from 'type-fest'; + const TIMEZONES = [ 'Africa/Abidjan', 'Africa/Accra', @@ -425,7 +427,7 @@ const TIMEZONES = [ * 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 = { +const timezoneBackwardMap: Record> = { 'Africa/Asmera': 'Africa/Nairobi', 'Africa/Timbuktu': 'Africa/Abidjan', 'America/Argentina/ComodRivadavia': 'America/Argentina/Catamarca', diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx index ac9657694500..734e8affa9ea 100644 --- a/src/components/AddPaymentMethodMenu.tsx +++ b/src/components/AddPaymentMethodMenu.tsx @@ -67,7 +67,7 @@ function AddPaymentMethodMenu({ // which then starts a bottom up flow and creates a Collect workspace where the payer is an admin and payee is an employee. const isIOUReport = ReportUtils.isIOUReport(iouReport ?? {}); const canUseBusinessBankAccount = - ReportUtils.isExpenseReport(iouReport ?? {}) || (isIOUReport && !ReportActionsUtils.hasRequestFromCurrentAccount(iouReport?.reportID ?? '', session?.accountID ?? 0)); + ReportUtils.isExpenseReport(iouReport ?? {}) || (isIOUReport && !ReportActionsUtils.hasRequestFromCurrentAccount(iouReport?.reportID ?? '-1', session?.accountID ?? -1)); const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOUReport; diff --git a/src/components/AddPlaidBankAccount.tsx b/src/components/AddPlaidBankAccount.tsx index 366f14ec9780..a1430615e37b 100644 --- a/src/components/AddPlaidBankAccount.tsx +++ b/src/components/AddPlaidBankAccount.tsx @@ -18,7 +18,6 @@ import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlocking import FormHelpMessage from './FormHelpMessage'; import Icon from './Icon'; import getBankIcon from './Icon/BankIcons'; -import Picker from './Picker'; import PlaidLink from './PlaidLink'; import RadioButtons from './RadioButtons'; import Text from './Text'; @@ -59,9 +58,6 @@ type AddPlaidBankAccountProps = AddPlaidBankAccountOnyxProps & { /** Are we adding a withdrawal account? */ allowDebit?: boolean; - /** Is displayed in new VBBA */ - isDisplayedInNewVBBA?: boolean; - /** Is displayed in new enable wallet flow */ isDisplayedInWalletFlow?: boolean; @@ -84,7 +80,6 @@ function AddPlaidBankAccount({ bankAccountID = 0, allowDebit = false, isPlaidDisabled, - isDisplayedInNewVBBA = false, errorText = '', onInputChange = () => {}, isDisplayedInWalletFlow = false, @@ -93,7 +88,7 @@ function AddPlaidBankAccount({ const styles = useThemeStyles(); const plaidBankAccounts = plaidData?.bankAccounts ?? []; const defaultSelectedPlaidAccount = plaidBankAccounts.find((account) => account.plaidAccountID === selectedPlaidAccountID); - const defaultSelectedPlaidAccountID = defaultSelectedPlaidAccount?.plaidAccountID ?? ''; + const defaultSelectedPlaidAccountID = defaultSelectedPlaidAccount?.plaidAccountID ?? '-1'; const defaultSelectedPlaidAccountMask = plaidBankAccounts.find((account) => account.plaidAccountID === selectedPlaidAccountID)?.mask ?? ''; const subscribedKeyboardShortcuts = useRef void>>([]); const previousNetworkState = useRef(); @@ -259,62 +254,32 @@ function AddPlaidBankAccount({ return {renderPlaidLink()}; } - if (isDisplayedInNewVBBA || isDisplayedInWalletFlow) { - return ( - - {translate(isDisplayedInWalletFlow ? 'walletPage.chooseYourBankAccount' : 'bankAccount.chooseAnAccount')} - {!!text && {text}} - - - - {bankName} - {selectedPlaidAccountMask.length > 0 && ( - {`${translate('bankAccount.accountEnding')} ${selectedPlaidAccountMask}`} - )} - - - {`${translate('bankAccount.chooseAnAccountBelow')}:`} - - - - ); - } - - // Plaid bank accounts view return ( - {!!text && {text}} - + {translate(isDisplayedInWalletFlow ? 'walletPage.chooseYourBankAccount' : 'bankAccount.chooseAnAccount')} + {!!text && {text}} + - {bankName} - - - + + {bankName} + {selectedPlaidAccountMask.length > 0 && ( + {`${translate('bankAccount.accountEnding')} ${selectedPlaidAccountMask}`} + )} + + {`${translate('bankAccount.chooseAnAccountBelow')}:`} + + ); } diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 9ad4643e834a..296ecce7d092 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -4,7 +4,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type {MaybePhraseKey} from '@libs/Localize'; -import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; @@ -149,8 +148,6 @@ function AddressForm({ label={translate('common.addressLine', {lineNumber: 1})} onValueChange={(data: unknown, key: unknown) => { onAddressChanged(data, key); - // This enforces the country selector to use the country from address instead of the country from URL - Navigation.setParams({country: undefined}); }} defaultValue={street1} renamedInputKeys={{ diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index a102b715d526..3319a28c58b9 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -12,6 +12,7 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import CONST from '@src/CONST'; import BigNumberPad from './BigNumberPad'; import FormHelpMessage from './FormHelpMessage'; +import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; import type TextInputWithCurrencySymbolProps from './TextInputWithCurrencySymbol/types'; @@ -94,7 +95,7 @@ function AmountForm( if (!textInput.current) { return; } - if (!textInput.current.isFocused()) { + if (!isTextInputFocused(textInput)) { textInput.current.focus(); } }; @@ -143,7 +144,7 @@ function AmountForm( */ const updateAmountNumberPad = useCallback( (key: string) => { - if (shouldUpdateSelection && !textInput.current?.isFocused()) { + if (shouldUpdateSelection && !isTextInputFocused(textInput)) { textInput.current?.focus(); } // Backspace button is pressed @@ -168,7 +169,7 @@ function AmountForm( */ const updateLongPressHandlerState = useCallback((value: boolean) => { setShouldUpdateSelection(!value); - if (!value && !textInput.current?.isFocused()) { + if (!value && !isTextInputFocused(textInput)) { textInput.current?.focus(); } }, []); diff --git a/src/components/AmountPicker/index.tsx b/src/components/AmountPicker/index.tsx index 014932f7736b..b84ec19e2ffd 100644 --- a/src/components/AmountPicker/index.tsx +++ b/src/components/AmountPicker/index.tsx @@ -2,15 +2,12 @@ import React, {forwardRef, useState} from 'react'; import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import useStyleUtils from '@hooks/useStyleUtils'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import callOrReturn from '@src/types/utils/callOrReturn'; import AmountSelectorModal from './AmountSelectorModal'; import type {AmountPickerProps} from './types'; function AmountPicker({value, description, title, errorText = '', onInputChange, furtherDetails, rightLabel, ...rest}: AmountPickerProps, forwardedRef: ForwardedRef) { - const StyleUtils = useStyleUtils(); const [isPickerVisible, setIsPickerVisible] = useState(false); const showPickerModal = () => { @@ -29,15 +26,12 @@ function AmountPicker({value, description, title, errorText = '', onInputChange, hidePickerModal(); }; - const descStyle = !value || value.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null; - return ( showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} shouldUseHapticsOnLongPress accessibilityLabel={displayName} role={CONST.ROLE.BUTTON} diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 173f84d392e5..bcc5acf83653 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -32,15 +32,15 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - const actorPersonalDetails = personalDetails?.[reportClosedAction?.actorAccountID ?? 0]; + const actorPersonalDetails = personalDetails?.[reportClosedAction?.actorAccountID ?? -1]; let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(actorPersonalDetails); let oldDisplayName: string | undefined; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { const newAccountID = originalMessage?.newAccountID; const oldAccountID = originalMessage?.oldAccountID; - displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? 0]); - oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? 0]); + displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? -1]); + oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? -1]); } const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 54a073e30567..3db946ce387e 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -282,9 +282,9 @@ function AttachmentModal({ * Detach the receipt and close the modal. */ const deleteAndCloseModal = useCallback(() => { - IOU.detachReceipt(transaction?.transactionID ?? ''); + IOU.detachReceipt(transaction?.transactionID ?? '-1'); setIsDeleteReceiptConfirmModalVisible(false); - Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '')); + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '-1')); }, [transaction, report]); const isValidFile = useCallback((fileObject: FileObject) => { @@ -427,8 +427,8 @@ function AttachmentModal({ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '', - report?.reportID ?? '', + transaction?.transactionID ?? '-1', + report?.reportID ?? '-1', Navigation.getActiveRouteWithoutParams(), ), ); @@ -616,11 +616,10 @@ AttachmentModal.displayName = 'AttachmentModal'; export default withOnyx({ transaction: { key: ({report}) => { - const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); - const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage.IOUTransactionID ?? '0' : '0'; + const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); + const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage.IOUTransactionID ?? '-1' : '-1'; return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, - initWithStoredValues: false, }, })(memo(AttachmentModal)); diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index c0d89f4acf1e..2fbe69a120a0 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -45,12 +45,12 @@ type AvatarProps = { /** Used to locate fallback icon in end-to-end tests. */ fallbackIconTestID?: string; - /** Denotes whether it is an avatar or a workspace avatar */ - type?: AvatarType; - /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; + /** Denotes whether it is an avatar or a workspace avatar */ + type: AvatarType; + /** Optional account id if it's user avatar or policy id if it's workspace avatar */ avatarID?: number | string; }; @@ -64,7 +64,7 @@ function Avatar({ fill, fallbackIcon = Expensicons.FallbackAvatar, fallbackIconTestID = '', - type = CONST.ICON_TYPE_AVATAR, + type, name = '', avatarID, }: AvatarProps) { @@ -80,9 +80,9 @@ function Avatar({ }, [originalSource]); const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; + const userAccountID = isWorkspace ? undefined : (avatarID as number); - // If it's user avatar then accountID will be a number - const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, avatarID as number); + const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, userAccountID); const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 9c7e26dacb6b..38bf3912ae4b 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -12,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 type {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; import CaretWrapper from './CaretWrapper'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; @@ -26,7 +26,7 @@ type AvatarWithDisplayNamePropsWithOnyx = { parentReportActions: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; }; type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { @@ -71,7 +71,7 @@ function AvatarWithDisplayName({ const actorAccountID = useRef(null); useEffect(() => { - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '']; + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; actorAccountID.current = parentReportAction?.actorAccountID ?? -1; }, [parentReportActions, report]); @@ -182,7 +182,7 @@ AvatarWithDisplayName.displayName = 'AvatarWithDisplayName'; export default withOnyx({ parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '-1'}`, canEvict: false, }, personalDetails: { diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index f024a1239f4e..bf8fe93b8b21 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -41,6 +41,7 @@ function AvatarWithIndicator({source, accountID, tooltipText = '', fallbackIcon source={UserUtils.getSmallSizeAvatar(source, accountID)} fallbackIcon={fallbackIcon} avatarID={accountID} + type={CONST.ICON_TYPE_AVATAR} /> diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 4e5c238fd692..404a92093119 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -93,17 +93,18 @@ function Banner({ )} {content && content} - {shouldRenderHTML && text ? ( - - ) : ( - - {text} - - )} + {text && + (shouldRenderHTML ? ( + + ) : ( + + {text} + + ))} {shouldShowCloseButton && !!onClose && ( diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 5e5f126da773..9e66c0b20c99 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,3 +1,4 @@ +import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; @@ -13,6 +14,9 @@ import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullCompo import * as EmojiUtils from '@libs/EmojiUtils'; import type {ComposerProps} from './types'; +const excludeNoStyles: Array = []; +const excludeReportMentionStyle: Array = ['mentionReport']; + function Composer( { shouldClear = false, @@ -37,7 +41,7 @@ function Composer( const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]); const theme = useTheme(); - const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []); + const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index eae27440d175..5bd8aa9175d3 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -1,3 +1,4 @@ +import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import lodashDebounce from 'lodash/debounce'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -46,6 +47,9 @@ const getNextChars = (inputString: string, cursorPosition: number): string => { return subString.substring(0, spaceIndex); }; +const excludeNoStyles: Array = []; +const excludeReportMentionStyle: Array = ['mentionReport']; + // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat function Composer( @@ -79,7 +83,7 @@ function Composer( const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]); const theme = useTheme(); const styles = useThemeStyles(); - const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []); + const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles); const StyleUtils = useStyleUtils(); const textRef = useRef(null); const textInput = useRef(null); @@ -165,7 +169,6 @@ function Composer( if (textInput.current !== event.target && !(isContenteditableDivFocused && !event.clipboardData?.files.length)) { const eventTarget = event.target as HTMLInputElement | HTMLTextAreaElement | null; - // To make sure the composer does not capture paste events from other inputs, we check where the event originated // If it did originate in another input, we return early to prevent the composer from handling the paste const isTargetInput = eventTarget?.nodeName === 'INPUT' || eventTarget?.nodeName === 'TEXTAREA' || eventTarget?.contentEditable === 'true'; diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx index 1776a0401403..fbca4fbb5dae 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx @@ -77,7 +77,7 @@ function ConnectToQuickbooksOnlineButton({ onClose={() => setWebViewOpen(false)} fullscreen isVisible - type={CONST.MODAL.MODAL_TYPE.CENTERED} + type={CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE} > setWebViewOpen(false)} fullscreen isVisible={isWebViewOpen} - type={CONST.MODAL.MODAL_TYPE.CENTERED} + type={CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE} > void; + + /** Handles what to do when the item loose focus */ + onBlur?: () => void; }; type ContextMenuItemHandle = { @@ -74,6 +77,7 @@ function ContextMenuItem( shouldPreventDefaultFocusOnPress = true, buttonRef = {current: null}, onFocus = () => {}, + onBlur = () => {}, }: ContextMenuItemProps, ref: ForwardedRef, ) { @@ -130,6 +134,7 @@ function ContextMenuItem( focused={isFocused} interactive={isThrottledButtonActive} onFocus={onFocus} + onBlur={onBlur} /> ); } diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 524c8a3903e0..356fbd3726a3 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -114,11 +114,6 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack [prevIsRootStatusBarEnabled, isRootStatusBarEnabled, statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], ); - useEffect(() => { - updateStatusBarAppearance({backgroundColor: theme.appBG}); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want this to run on first render - }, []); - useEffect(() => { didForceUpdateStatusBarRef.current = false; }, [isRootStatusBarEnabled]); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.tsx b/src/components/EmojiPicker/EmojiPickerMenu/index.tsx index 9d2cd4f60994..5a59585ad72e 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.tsx +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.tsx @@ -7,6 +7,7 @@ import {scrollTo} from 'react-native-reanimated'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import isTextInputFocused from '@components/TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; @@ -86,7 +87,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r } // If the input is not focused and the new index is out of range, focus the input - if (newIndex < 0 && !searchInputRef.current?.isFocused() && shouldFocusInputOnScreenFocus) { + if (newIndex < 0 && !isTextInputFocused(searchInputRef) && shouldFocusInputOnScreenFocus) { searchInputRef.current?.focus(); } }, @@ -165,12 +166,12 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input // is not focused, so that the navigation and tab cycling can be done using the keyboard without // interfering with the input behaviour. - if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && searchInputRef.current && !searchInputRef.current.isFocused())) { + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && searchInputRef.current && !isTextInputFocused(searchInputRef))) { setIsUsingKeyboardMovement(true); } // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (searchInputRef.current && !searchInputRef.current.isFocused() && ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) { + if (searchInputRef.current && !isTextInputFocused(searchInputRef) && ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) { searchInputRef.current.focus(); } }, diff --git a/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts b/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts new file mode 100644 index 000000000000..e6af466b12c5 --- /dev/null +++ b/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts @@ -0,0 +1,6 @@ +import type {BottomTabName} from '@libs/Navigation/types'; +import SCREENS from '@src/SCREENS'; + +const BOTTOM_TAB_SCREENS: BottomTabName[] = [SCREENS.HOME, SCREENS.SETTINGS.ROOT]; + +export default BOTTOM_TAB_SCREENS; diff --git a/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts b/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts new file mode 100644 index 000000000000..6bc2350a6c55 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts @@ -0,0 +1,6 @@ +type FocusTrapForModalProps = { + children: React.ReactNode; + active: boolean; +}; + +export default FocusTrapForModalProps; diff --git a/src/components/FocusTrap/FocusTrapForModal/index.tsx b/src/components/FocusTrap/FocusTrapForModal/index.tsx new file mode 100644 index 000000000000..01632998b079 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForModal/index.tsx @@ -0,0 +1,9 @@ +import type FocusTrapForModalProps from './FocusTrapForModalProps'; + +function FocusTrapForModal({children}: FocusTrapForModalProps) { + return children; +} + +FocusTrapForModal.displayName = 'FocusTrapForModal'; + +export default FocusTrapForModal; diff --git a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx new file mode 100644 index 000000000000..b06044404ee2 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx @@ -0,0 +1,23 @@ +import FocusTrap from 'focus-trap-react'; +import React from 'react'; +import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; +import type FocusTrapForModalProps from './FocusTrapForModalProps'; + +function FocusTrapForModal({children, active}: FocusTrapForModalProps) { + return ( + + {children} + + ); +} + +FocusTrapForModal.displayName = 'FocusTrapForModal'; + +export default FocusTrapForModal; diff --git a/src/components/FocusTrap/FocusTrapForScreen/FocusTrapProps.ts b/src/components/FocusTrap/FocusTrapForScreen/FocusTrapProps.ts new file mode 100644 index 000000000000..d2f6e5323445 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForScreen/FocusTrapProps.ts @@ -0,0 +1,5 @@ +type FocusTrapForScreenProps = { + children: React.ReactNode; +}; + +export default FocusTrapForScreenProps; diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.tsx new file mode 100644 index 000000000000..ae7ece116d1e --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForScreen/index.tsx @@ -0,0 +1,9 @@ +import type FocusTrapProps from './FocusTrapProps'; + +function FocusTrapForScreen({children}: FocusTrapProps) { + return children; +} + +FocusTrapForScreen.displayName = 'FocusTrapForScreen'; + +export default FocusTrapForScreen; diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx new file mode 100644 index 000000000000..6a1409ab4a93 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -0,0 +1,72 @@ +import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native'; +import FocusTrap from 'focus-trap-react'; +import React, {useCallback, useMemo} from 'react'; +import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS'; +import SCREENS_WITH_AUTOFOCUS from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; +import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; +import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS'; +import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type FocusTrapProps from './FocusTrapProps'; + +let activeRouteName = ''; +function FocusTrapForScreen({children}: FocusTrapProps) { + const isFocused = useIsFocused(); + const route = useRoute(); + const {isSmallScreenWidth} = useWindowDimensions(); + + const isActive = useMemo(() => { + // Focus trap can't be active on bottom tab screens because it would block access to the tab bar. + if (BOTTOM_TAB_SCREENS.find((screen) => screen === route.name)) { + return false; + } + + // in top tabs only focus trap for currently shown tab should be active + if (TOP_TAB_SCREENS.find((screen) => screen === route.name)) { + return isFocused; + } + + // Focus trap can't be active on these screens if the layout is wide because they may be displayed side by side. + if (WIDE_LAYOUT_INACTIVE_SCREENS.includes(route.name) && !isSmallScreenWidth) { + return false; + } + return true; + }, [isFocused, isSmallScreenWidth, route.name]); + + useFocusEffect( + useCallback(() => { + activeRouteName = route.name; + }, [route]), + ); + + return ( + { + if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + return false; + } + return undefined; + }, + setReturnFocus: (element) => { + if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + return false; + } + return element; + }, + }} + > + {children} + + ); +} + +FocusTrapForScreen.displayName = 'FocusTrapForScreen'; + +export default FocusTrapForScreen; diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts new file mode 100644 index 000000000000..2a77b52e3116 --- /dev/null +++ b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts @@ -0,0 +1,15 @@ +import {CENTRAL_PANE_WORKSPACE_SCREENS} from '@libs/Navigation/AppNavigator/Navigators/FullScreenNavigator'; +import SCREENS from '@src/SCREENS'; + +const SCREENS_WITH_AUTOFOCUS: string[] = [ + ...Object.keys(CENTRAL_PANE_WORKSPACE_SCREENS), + SCREENS.REPORT, + SCREENS.REPORT_DESCRIPTION_ROOT, + SCREENS.PRIVATE_NOTES.EDIT, + SCREENS.SETTINGS.PROFILE.STATUS, + SCREENS.SETTINGS.PROFILE.PRONOUNS, + SCREENS.NEW_TASK.DETAILS, + SCREENS.MONEY_REQUEST.CREATE, +]; + +export default SCREENS_WITH_AUTOFOCUS; diff --git a/src/components/FocusTrap/TOP_TAB_SCREENS.ts b/src/components/FocusTrap/TOP_TAB_SCREENS.ts new file mode 100644 index 000000000000..6bee36b86883 --- /dev/null +++ b/src/components/FocusTrap/TOP_TAB_SCREENS.ts @@ -0,0 +1,5 @@ +import CONST from '@src/CONST'; + +const TOP_TAB_SCREENS: string[] = [CONST.TAB.NEW_CHAT, CONST.TAB.NEW_ROOM, CONST.TAB_REQUEST.DISTANCE, CONST.TAB_REQUEST.MANUAL, CONST.TAB_REQUEST.SCAN]; + +export default TOP_TAB_SCREENS; diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts new file mode 100644 index 000000000000..5f10a7293457 --- /dev/null +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -0,0 +1,37 @@ +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +// Screens that should not have active focus trap when rendered on wide screen in order to allow Tab navigation in LHP and RHP +const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ + NAVIGATORS.BOTTOM_TAB_NAVIGATOR, + SCREENS.HOME, + SCREENS.SETTINGS.ROOT, + SCREENS.REPORT, + SCREENS.SETTINGS.PROFILE.ROOT, + SCREENS.SETTINGS.PREFERENCES.ROOT, + SCREENS.SETTINGS.SECURITY, + SCREENS.SETTINGS.WALLET.ROOT, + SCREENS.SETTINGS.ABOUT, + SCREENS.SETTINGS.WORKSPACES, + SCREENS.SETTINGS.SUBSCRIPTION.ROOT, + SCREENS.WORKSPACE.INITIAL, + SCREENS.WORKSPACE.PROFILE, + SCREENS.WORKSPACE.CARD, + SCREENS.WORKSPACE.WORKFLOWS, + SCREENS.WORKSPACE.WORKFLOWS_APPROVER, + SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, + SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET, + SCREENS.WORKSPACE.REIMBURSE, + SCREENS.WORKSPACE.BILLS, + SCREENS.WORKSPACE.INVOICES, + SCREENS.WORKSPACE.TRAVEL, + SCREENS.WORKSPACE.MEMBERS, + SCREENS.WORKSPACE.CATEGORIES, + SCREENS.WORKSPACE.MORE_FEATURES, + SCREENS.WORKSPACE.TAGS, + SCREENS.WORKSPACE.TAXES, + SCREENS.WORKSPACE.DISTANCE_RATES, + SCREENS.SEARCH.CENTRAL_PANE, +]; + +export default WIDE_LAYOUT_INACTIVE_SCREENS; diff --git a/src/components/FocusTrap/sharedTrapStack.ts b/src/components/FocusTrap/sharedTrapStack.ts new file mode 100644 index 000000000000..d0fa12f3fe93 --- /dev/null +++ b/src/components/FocusTrap/sharedTrapStack.ts @@ -0,0 +1,6 @@ +import type {FocusTrap as FocusTrapHandler} from 'focus-trap'; + +// focus-trap is capable of managing many traps. It's necessary for RHP and modals +const trapStack: FocusTrapHandler[] = []; + +export default trapStack; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 51f9981f1524..d95fc9e11a31 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -77,8 +77,13 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim mixedUAStyles: {...styles.textSupporting, ...styles.textLineThrough}, contentModel: HTMLContentModel.textual, }), + 'uploading-attachment': HTMLElementModel.fromCustomModel({ + tagName: 'uploading-attachment', + mixedUAStyles: {...styles.mt4}, + contentModel: HTMLContentModel.block, + }), }), - [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough], + [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4], ); /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index aa68687379ac..67bbc7986bc7 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -93,7 +93,7 @@ function ImageRenderer({tnode}: ImageRendererProps) { Navigation.navigate(route); } }} - onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} shouldUseHapticsOnLongPress accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index 9c033674c399..e5481c5d9094 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -86,7 +86,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona {({anchor, report, action, checkIfContextMenuActive}) => ( showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} onPress={(event) => { event.preventDefault(); Navigation.navigate(navigationRoute); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx index 39a1993c2334..4d1e58a42830 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx @@ -39,7 +39,7 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d onPress={onPressIn ?? (() => {})} onPressIn={onPressIn} onPressOut={onPressOut} - onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} shouldUseHapticsOnLongPress role={CONST.ROLE.PRESENTATION} accessibilityLabel={translate('accessibilityHints.prestyledText')} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx index 52d14df46471..599455f6d7d9 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx @@ -29,13 +29,13 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { { - const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID ?? '', CONST.ATTACHMENT_TYPE.REPORT, sourceURL); + const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID ?? '-1', CONST.ATTACHMENT_TYPE.REPORT, sourceURL); Navigation.navigate(route); }} /> diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 78cd92dbc223..2d73e3c2dd24 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -30,7 +30,7 @@ function HeaderWithBackButton({ onCloseButtonPress = () => Navigation.dismissModal(), onDownloadButtonPress = () => {}, onThreeDotsButtonPress = () => {}, - report = null, + report, policy, policyAvatar, shouldShowReportAvatarWithDisplay = false, @@ -158,7 +158,7 @@ function HeaderWithBackButton({ } }} style={[styles.touchableButtonImage]} - role="button" + role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.back')} id={CONST.BACK_BUTTON_NATIVE_ID} > diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index dd3f41c52578..b58433afb17c 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -8,6 +8,8 @@ import type HoverableProps from './types'; type ActiveHoverableProps = Omit; +type OnMouseEvent = (e: MouseEvent) => void; + function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreezeCapture, children}: ActiveHoverableProps, outerRef: Ref) { const [isHovered, setIsHovered] = useState(false); @@ -98,9 +100,10 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez const child = useMemo(() => getReturnValue(children, !isScrollingRef.current && isHovered), [children, isHovered]); - const childOnMouseEnter = child.props.onMouseEnter; - const childOnMouseLeave = child.props.onMouseLeave; - const childOnMouseMove = child.props.onMouseMove; + const childOnMouseEnter: OnMouseEvent = child.props.onMouseEnter; + const childOnMouseLeave: OnMouseEvent = child.props.onMouseLeave; + const childOnMouseMove: OnMouseEvent = child.props.onMouseMove; + const childOnBlur: OnMouseEvent = child.props.onBlur; const hoverAndForwardOnMouseEnter = useCallback( (e: MouseEvent) => { @@ -127,9 +130,9 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez setIsHovered(false); } - child.props.onBlur?.(event); + childOnBlur?.(event); }, - [child.props], + [childOnBlur], ); const handleAndForwardOnMouseMove = useCallback( diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 53b8aa8acb72..3fe36239d631 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -49,6 +49,7 @@ import CompanyCard from '@assets/images/simple-illustrations/simple-illustration 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'; +import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg'; import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg'; import FolderOpen from '@assets/images/simple-illustrations/simple-illustration__folder-open.svg'; import Gears from '@assets/images/simple-illustrations/simple-illustration__gears.svg'; @@ -190,4 +191,5 @@ export { ExpensifyApprovedLogoLight, SendMoney, CheckmarkCircle, + CreditCardEyes, }; diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 8c4a131284c8..818b4aff6b00 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -166,7 +166,7 @@ function KYCWall({ transferBalanceButtonRef.current = targetElement; - const isExpenseReport = ReportUtils.isExpenseReport(iouReport ?? null); + const isExpenseReport = ReportUtils.isExpenseReport(iouReport); const paymentCardList = fundList ?? {}; // Check to see if user has a valid payment method on file and display the add payment popover if they don't diff --git a/src/components/KeyboardAvoidingView/index.ios.tsx b/src/components/KeyboardAvoidingView/index.ios.tsx index a7cd767377ef..171210eab7ac 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 type 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 09ec21e5b219..c0882ae1e9cc 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 type KeyboardAvoidingViewProps from './types'; +import type {KeyboardAvoidingViewProps} from './types'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, ...rest} = props; diff --git a/src/components/KeyboardAvoidingView/types.ts b/src/components/KeyboardAvoidingView/types.ts index 48d354e8b53f..2c1ef64ced8f 100644 --- a/src/components/KeyboardAvoidingView/types.ts +++ b/src/components/KeyboardAvoidingView/types.ts @@ -1,3 +1,4 @@ -import {KeyboardAvoidingViewProps} from 'react-native'; +import type {KeyboardAvoidingViewProps} from 'react-native'; -export default KeyboardAvoidingViewProps; +// eslint-disable-next-line import/prefer-default-export +export type {KeyboardAvoidingViewProps}; diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 7a2650ab5bd6..60d5bf7034cc 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -4,13 +4,13 @@ import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {StyleSheet, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import BlockingView from '@components/BlockingViews/BlockingView'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import LottieAnimations from '@components/LottieAnimations'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; -import Text from '@components/Text'; +import TextBlock from '@components/TextBlock'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; @@ -23,35 +23,27 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHNData from './OptionRowLHNData'; -import type {LHNOptionsListOnyxProps, LHNOptionsListProps, RenderItemProps} from './types'; +import type {LHNOptionsListProps, RenderItemProps} from './types'; const keyExtractor = (item: string) => `report_${item}`; -function LHNOptionsList({ - style, - contentContainerStyles, - data, - onSelectRow, - optionMode, - shouldDisableFocusOptions = false, - reports = {}, - reportActions = {}, - policy = {}, - preferredLocale = CONST.LOCALES.DEFAULT, - personalDetails = {}, - transactions = {}, - draftComments = {}, - transactionViolations = {}, - onFirstItemRendered = () => {}, -}: LHNOptionsListProps) { +function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optionMode, shouldDisableFocusOptions = false, onFirstItemRendered = () => {}}: LHNOptionsListProps) { const {saveScrollOffset, getScrollOffset} = useContext(ScrollOffsetContext); const flashListRef = useRef>(null); const route = useRoute(); + const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); + const [policy] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); + const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const theme = useTheme(); const styles = useThemeStyles(); const {canUseViolations} = usePermissions(); - const {translate} = useLocalize(); + const {translate, preferredLocale} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const shouldShowEmptyLHN = shouldUseNarrowLayout && data.length === 0; @@ -69,36 +61,52 @@ function LHNOptionsList({ const emptyLHNSubtitle = useMemo( () => ( - - - {translate('common.emptyLHN.subtitleText1')} - - {translate('common.emptyLHN.subtitleText2')} - - {translate('common.emptyLHN.subtitleText3')} - + + + + + + ), - [theme, styles.alignItemsCenter, styles.textAlignCenter, translate], + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.mh1, + theme.icon, + theme.textSupporting, + styles.textNormal, + translate, + ], ); /** @@ -106,13 +114,13 @@ function LHNOptionsList({ */ const renderItem = useCallback( ({item: reportID}: RenderItemProps): ReactElement => { - const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; - const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; - const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; - const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; - const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; + const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`]; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? '-1']; + const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`]; + const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '-1' : '-1'; + const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); const lastReportAction = sortedReportActions[0]; @@ -121,7 +129,7 @@ function LHNOptionsList({ let lastReportActionTransactionID = ''; if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - lastReportActionTransactionID = lastReportAction.originalMessage?.IOUTransactionID ?? ''; + lastReportActionTransactionID = lastReportAction.originalMessage?.IOUTransactionID ?? '-1'; } const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`] ?? {}; @@ -246,31 +254,6 @@ function LHNOptionsList({ LHNOptionsList.displayName = 'LHNOptionsList'; -export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - policy: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - draftComments: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, -})(memo(LHNOptionsList)); +export default memo(LHNOptionsList); export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 851e2a9fdd16..49c5ee0568db 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,5 +1,4 @@ import {useFocusEffect} from '@react-navigation/native'; -import {ExpensiMark} from 'expensify-common'; import React, {useCallback, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; @@ -20,6 +19,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import DomUtils from '@libs/DomUtils'; +import {parseHtmlToText} from '@libs/OnyxAwareParser'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; @@ -29,8 +29,6 @@ import CONST from '@src/CONST'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OptionRowLHNProps} from './types'; -const parser = new ExpensiMark(); - function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style, onLayout = () => {}, hasDraftComment}: OptionRowLHNProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -105,7 +103,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti '', popoverAnchor.current, reportID, - '0', + '-1', reportID, undefined, () => {}, @@ -122,12 +120,12 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const statusClearAfterDate = optionItem.status?.clearAfter ?? ''; const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate); const statusContent = formattedDate ? `${statusText ? `${statusText} ` : ''}(${formattedDate})` : statusText; - const report = ReportUtils.getReport(optionItem.reportID ?? ''); - const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null); + const report = ReportUtils.getReport(optionItem.reportID ?? '-1'); + const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : undefined); const isGroupChat = ReportUtils.isGroupChat(optionItem) || ReportUtils.isDeprecatedGroupDM(optionItem); - const fullTitle = isGroupChat ? ReportUtils.getGroupChatName(undefined, false, optionItem.reportID ?? '') : optionItem.text; + const fullTitle = isGroupChat ? ReportUtils.getGroupChatName(undefined, false, optionItem.reportID ?? '-1') : optionItem.text; const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar; return ( {}, opti > - {(optionItem.icons?.length ?? 0) > 0 && + {!!optionItem.icons?.length && (optionItem.shouldShowSubscript ? ( ) : ( {}, opti !!optionItem.isThread || !!optionItem.isMoneyRequestReport || !!optionItem.isInvoiceReport || - ReportUtils.isGroupChat(report) + ReportUtils.isGroupChat(report) || + ReportUtils.isSystemChat(report) } /> {isStatusVisible && ( @@ -239,7 +238,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti numberOfLines={1} accessibilityLabel={translate('accessibilityHints.lastChatMessagePreview')} > - {parser.htmlToText(optionItem.alternateText)} + {parseHtmlToText(optionItem.alternateText)} ) : null} diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index c80017c39a3d..a2dd41eab0bd 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -35,7 +35,7 @@ function OptionRowLHNData({ const optionItemRef = useRef(); - const shouldDisplayViolations = canUseViolations && ReportUtils.shouldDisplayTransactionThreadViolations(fullReport, transactionViolations, parentReportAction ?? null); + const shouldDisplayViolations = canUseViolations && ReportUtils.shouldDisplayTransactionThreadViolations(fullReport, transactionViolations, parentReportAction); const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 0f0c921747b4..7248742654d2 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -10,32 +10,6 @@ import type {EmptyObject} from '@src/types/utils/EmptyObject'; type OptionMode = ValueOf; -type LHNOptionsListOnyxProps = { - /** The policy which the user has access to and which the report could be tied to */ - policy: OnyxCollection; - - /** All reports shared with the user */ - reports: OnyxCollection; - - /** Array of report actions for this report */ - reportActions: OnyxCollection; - - /** Indicates which locale the user currently has selected */ - preferredLocale: OnyxEntry; - - /** List of users' personal details */ - personalDetails: OnyxEntry; - - /** The transaction from the parent report action */ - transactions: OnyxCollection; - - /** List of draft comments */ - draftComments: OnyxCollection; - - /** The list of transaction violations */ - transactionViolations: OnyxCollection; -}; - type CustomLHNOptionsListProps = { /** Wrapper style for the section list */ style?: StyleProp; @@ -59,7 +33,7 @@ type CustomLHNOptionsListProps = { onFirstItemRendered: () => void; }; -type LHNOptionsListProps = CustomLHNOptionsListProps & LHNOptionsListOnyxProps; +type LHNOptionsListProps = CustomLHNOptionsListProps; type OptionRowLHNDataProps = { /** Whether row should be focused */ @@ -141,4 +115,4 @@ type OptionRowLHNProps = { type RenderItemProps = {item: string}; -export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; +export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, RenderItemProps}; diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index eb7d9324d2ab..e0e30d14d2a2 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -2,7 +2,6 @@ import React, {createContext, useMemo} from 'react'; 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'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; @@ -125,18 +124,17 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {} return {children}; } -const Provider = compose( +const Provider = withCurrentUserPersonalDetails( withOnyx({ preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, selector: (preferredLocale) => preferredLocale, }, - }), - withCurrentUserPersonalDetails, -)(LocaleContextProvider); + })(LocaleContextProvider), +); Provider.displayName = 'withOnyx(LocaleContextProvider)'; -export {Provider as LocaleContextProvider, LocaleContext}; +export {LocaleContext, Provider as LocaleContextProvider}; -export type {LocaleContextProps, Locale}; +export type {Locale, LocaleContextProps}; diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx index 598819e19361..657fe79b401f 100644 --- a/src/components/LottieAnimations/index.tsx +++ b/src/components/LottieAnimations/index.tsx @@ -3,6 +3,11 @@ import variables from '@styles/variables'; import type DotLottieAnimation from './types'; const DotLottieAnimations = { + Abracadabra: { + file: require('@assets/animations/Abracadabra.lottie'), + w: 375, + h: 400, + }, FastMoney: { file: require('@assets/animations/FastMoney.lottie'), w: 375, diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 8168cef99cd3..06128c0d06b7 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -10,7 +10,6 @@ import {PressableWithoutFeedback} from '@components/Pressable'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as UserLocation from '@libs/actions/UserLocation'; -import compose from '@libs/compose'; import getCurrentPosition from '@libs/getCurrentPosition'; import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; @@ -265,11 +264,8 @@ const MapView = forwardRef( }, ); -export default compose( - withOnyx({ - userLocation: { - key: ONYXKEYS.USER_LOCATION, - }, - }), - memo, -)(MapView); +export default withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + }, +})(memo(MapView)); diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 554ae9d4a84a..0ad729e13662 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -207,7 +207,7 @@ type MenuItemBaseProps = { shouldStackHorizontally?: boolean; /** Prop to represent the size of the avatar images to be shown */ - avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; + avatarSize?: ValueOf; /** Avatars to show on the right of the menu item */ floatRightAvatars?: IconType[]; @@ -281,6 +281,9 @@ type MenuItemBaseProps = { /** Handles what to do when the item is focused */ onFocus?: () => void; + /** Handles what to do when the item loose focus */ + onBlur?: () => void; + /** Optional account id if it's user avatar or policy id if it's workspace avatar */ avatarID?: number | string; }; @@ -365,6 +368,7 @@ function MenuItem( isPaneMenu = false, shouldPutLeftPaddingWhenNoIcon = false, onFocus, + onBlur, avatarID, }: MenuItemProps, ref: PressableRef, @@ -462,7 +466,7 @@ function MenuItem( }; return ( - + {!!label && !isLabelHoverable && ( {label} @@ -561,6 +565,7 @@ function MenuItem( avatarID={avatarID} fallbackIcon={fallbackIcon} size={avatarSize} + type={CONST.ICON_TYPE_AVATAR} /> )} diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index e24c5b7c9c80..2eb073bb39be 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -2,6 +2,7 @@ import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react' import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import useKeyboardState from '@hooks/useKeyboardState'; import usePrevious from '@hooks/usePrevious'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; @@ -214,7 +215,7 @@ function BaseModal( // a conflict between RN core and Reanimated shadow tree operations // position absolute is needed to prevent the view from interfering with flex layout collapsable={false} - style={[styles.pAbsolute]} + style={[styles.pAbsolute, {zIndex: 1}]} > : undefined} > - - {children} - + + + {children} + + diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 76f4b251ec83..325fb4ca6ab8 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -22,7 +22,7 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( const hideModal = () => { setStatusBarColor(previousStatusBarColor); onModalHide(); - if (window.history.state.shouldGoBack) { + if (window.history.state?.shouldGoBack) { window.history.back(); } }; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 5ea35c2d3c3f..90952157f179 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1,13 +1,15 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as HeaderUtils from '@libs/HeaderUtils'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -17,6 +19,7 @@ import * as TransactionActions from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -62,6 +65,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); const {translate} = useLocalize(); + const {isOffline} = useNetwork(); const {windowWidth} = useWindowDimensions(); const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport); const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); @@ -87,6 +91,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + const navigateBackToAfterDelete = useRef(); const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((transaction) => transaction.transactionID); const allHavePendingRTERViolation = TransactionUtils.allHavePendingRTERViolation(transactionIDs); @@ -146,12 +151,12 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const deleteTransaction = useCallback(() => { if (requestParentReportAction) { - const iouTransactionID = requestParentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + const iouTransactionID = requestParentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '-1' : '-1'; if (ReportActionsUtils.isTrackExpenseAction(requestParentReportAction)) { - IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, true); - return; + navigateBackToAfterDelete.current = IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '-1', iouTransactionID, requestParentReportAction, true); + } else { + navigateBackToAfterDelete.current = IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, true); } - IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, true); } setIsDeleteRequestModalVisible(false); @@ -161,8 +166,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (!requestParentReportAction) { return; } - const iouTransactionID = requestParentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; - const reportID = transactionThreadReport?.reportID ?? ''; + const iouTransactionID = requestParentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '-1' : '-1'; + const reportID = transactionThreadReport?.reportID ?? '-1'; TransactionActions.markAsCash(iouTransactionID, reportID); }, [requestParentReportAction, transactionThreadReport?.reportID]); @@ -231,7 +236,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea shouldDisableApproveButton={shouldDisableApproveButton} style={[styles.pv2]} formattedAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? displayedAmount : ''} - isDisabled={!canAllowSettlement} + isDisabled={isOffline && !canAllowSettlement} + isLoading={!isOffline && !canAllowSettlement} /> )} @@ -302,7 +308,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea shouldShowApproveButton={shouldShowApproveButton} formattedAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? displayedAmount : ''} shouldDisableApproveButton={shouldDisableApproveButton} - isDisabled={!canAllowSettlement} + isDisabled={isOffline && !canAllowSettlement} + isLoading={!isOffline && !canAllowSettlement} /> )} @@ -367,6 +374,12 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea isVisible={isDeleteRequestModalVisible} onConfirm={deleteTransaction} onCancel={() => setIsDeleteRequestModalVisible(false)} + onModalHide={() => { + if (!navigateBackToAfterDelete.current) { + return; + } + Navigation.goBack(navigateBackToAfterDelete.current); + }} prompt={translate('iou.deleteConfirmation')} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index fb17297dc642..0c3868312c41 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -9,6 +9,7 @@ import getOperatingSystem from '@libs/getOperatingSystem'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import shouldIgnoreSelectionWhenUpdatedManually from '@libs/shouldIgnoreSelectionWhenUpdatedManually'; import CONST from '@src/CONST'; +import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputRef} from './TextInput/BaseTextInput/types'; import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; @@ -201,7 +202,7 @@ function MoneyRequestAmountInput( })); useEffect(() => { - if (!currency || typeof amount !== 'number' || (formatAmountOnBlur && textInput.current?.isFocused()) || shouldKeepUserInput) { + if ((!currency || typeof amount !== 'number' || (formatAmountOnBlur && isTextInputFocused(textInput))) ?? shouldKeepUserInput) { return; } const frontendAmount = onFormatAmount(amount, currency); diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e7c799fea3b6..e08b35dd3866 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -133,7 +133,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & selectedParticipants: Participant[]; /** Payee of the expense with login */ - payeePersonalDetails?: OnyxEntry; + payeePersonalDetails?: OnyxEntry | null; /** Should the list be read only, and not editable? */ isReadOnly?: boolean; @@ -184,7 +184,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & type MoneyRequestConfirmationListItem = Participant | ReportUtils.OptionData; function MoneyRequestConfirmationList({ - transaction = null, + transaction, onSendMoney, onConfirm, iouType = CONST.IOU.TYPE.SUBMIT, @@ -240,8 +240,8 @@ function MoneyRequestConfirmationList({ const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE; const isScanRequest = useMemo(() => TransactionUtils.isScanRequest(transaction), [transaction]); - const transactionID = transaction?.transactionID ?? ''; - const customUnitRateID = TransactionUtils.getRateID(transaction) ?? ''; + const transactionID = transaction?.transactionID ?? '-1'; + const customUnitRateID = TransactionUtils.getRateID(transaction) ?? '-1'; useEffect(() => { if (customUnitRateID || !canUseP2PDistanceRequests) { @@ -510,7 +510,7 @@ function MoneyRequestConfirmationList({ rightElement: ( onSplitShareChange(participantOption.accountID ?? 0, Number(value))} + onAmountChange={(value: string) => onSplitShareChange(participantOption.accountID ?? -1, Number(value))} maxLength={formattedTotalAmount.length} /> ), @@ -709,7 +709,7 @@ function MoneyRequestConfirmationList({ if (selectedParticipants.length === 0) { return; } - if (!isEditingSplitBill && isMerchantRequired && (isMerchantEmpty || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null)))) { + if (!isEditingSplitBill && isMerchantRequired && (isMerchantEmpty || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction)))) { setFormError('iou.error.invalidMerchant'); return; } @@ -739,7 +739,7 @@ function MoneyRequestConfirmationList({ return; } - if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) { + if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { setDidConfirmSplit(true); setFormError('iou.error.genericSmartscanFailureMessage'); return; @@ -861,8 +861,8 @@ function MoneyRequestConfirmationList({ style={[styles.moneyRequestMenuItem, styles.mt2]} titleStyle={styles.moneyRequestConfirmationAmount} disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - errorText={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + errorText={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} /> ), shouldShow: shouldShowSmartScanFields, @@ -1096,7 +1096,7 @@ function MoneyRequestConfirmationList({ isThumbnail, fileExtension, isLocalFile, - } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); + } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); @@ -1186,7 +1186,9 @@ function MoneyRequestConfirmationList({ isLabelHoverable={false} interactive={!isReadOnly && canUpdateSenderWorkspace} onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '-1', reportID, Navigation.getActiveRouteWithoutParams()), + ); }} style={styles.moneyRequestMenuItem} labelStyle={styles.mt2} diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index e5a61a8fa23c..a1f214ae847a 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,5 +1,5 @@ import type {ReactNode} from 'react'; -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -18,9 +18,11 @@ import * as TransactionActions from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import type {Policy, Report, ReportAction} from '@src/types/onyx'; import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import type IconAsset from '@src/types/utils/IconAsset'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import Button from './Button'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; @@ -49,10 +51,11 @@ type MoneyRequestHeaderProps = { function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrowLayout = false, onBackButtonPress}: MoneyRequestHeaderProps) { const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`); - const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${(parentReportAction as ReportAction & OriginalMessageIOU)?.originalMessage?.IOUTransactionID ?? 0}`); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${(parentReportAction as ReportAction & OriginalMessageIOU)?.originalMessage?.IOUTransactionID ?? -1}`); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [session] = useOnyx(ONYXKEYS.SESSION); - const [shownHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_HOLD_USE_EXPLAINED, {initWithStoredValues: false}); + const [holdUseExplained, holdUseExplainedResult] = useOnyx(ONYXKEYS.NVP_HOLD_USE_EXPLAINED); + const isLoadingHoldUseExplained = isLoadingOnyxValue(holdUseExplainedResult); const styles = useThemeStyles(); const theme = useTheme(); @@ -67,28 +70,30 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? ''); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); - // Only the requestor can take delete the expense, admins can only edit it. + const navigateBackToAfterDelete = useRef(); + + // Only the requestor can take delete the request, admins can only edit it. const isActionOwner = typeof parentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && parentReportAction.actorAccountID === session?.accountID; const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; - const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '']); + const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']); const shouldShowMarkAsCashButton = isDraft && hasAllPendingRTERViolations; const deleteTransaction = useCallback(() => { if (parentReportAction) { - const iouTransactionID = parentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + const iouTransactionID = parentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '-1' : '-1'; if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) { - IOU.deleteTrackExpense(parentReport?.reportID ?? '', iouTransactionID, parentReportAction, true); - return; + navigateBackToAfterDelete.current = IOU.deleteTrackExpense(parentReport?.reportID ?? '-1', iouTransactionID, parentReportAction, true); + } else { + navigateBackToAfterDelete.current = IOU.deleteMoneyRequest(iouTransactionID, parentReportAction, true); } - IOU.deleteMoneyRequest(iouTransactionID, parentReportAction, true); } setIsDeleteModalVisible(false); }, [parentReport?.reportID, parentReportAction, setIsDeleteModalVisible]); const markAsCash = useCallback(() => { - TransactionActions.markAsCash(transaction?.transactionID ?? '', report.reportID); + TransactionActions.markAsCash(transaction?.transactionID ?? '-1', report.reportID); }, [report.reportID, transaction?.transactionID]); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); @@ -100,7 +105,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(report)) && !isDeletedParentAction; const changeMoneyRequestStatus = () => { - const iouTransactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + const iouTransactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '-1' : '-1'; if (isOnHold) { IOU.unholdRequest(iouTransactionID, report?.reportID); @@ -127,7 +132,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow if (TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction)) { return {title: getStatusIcon(Expensicons.CreditCardHourglass), description: translate('iou.transactionPendingDescription'), shouldShowBorderBottom: true}; } - if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '', transactionViolations))) { + if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) { return { title: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription'), @@ -172,8 +177,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow } useEffect(() => { - setShouldShowHoldMenu(isOnHold && !shownHoldUseExplanation); - }, [isOnHold, shownHoldUseExplanation]); + if (isLoadingHoldUseExplained) { + return; + } + setShouldShowHoldMenu(isOnHold && !holdUseExplained); + }, [isOnHold, holdUseExplained, isLoadingHoldUseExplained]); useEffect(() => { if (!shouldShowHoldMenu) { @@ -273,6 +281,12 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow isVisible={isDeleteModalVisible} onConfirm={deleteTransaction} onCancel={() => setIsDeleteModalVisible(false)} + onModalHide={() => { + if (!navigateBackToAfterDelete.current) { + return; + } + Navigation.goBack(navigateBackToAfterDelete.current); + }} prompt={translate('iou.deleteConfirmation')} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} diff --git a/src/components/OptionsPicker/index.tsx b/src/components/OptionsPicker/index.tsx index 621b8465adba..06df1c0172c3 100644 --- a/src/components/OptionsPicker/index.tsx +++ b/src/components/OptionsPicker/index.tsx @@ -1,6 +1,7 @@ import React, {Fragment} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type {TranslationPaths} from '@src/languages/types'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -36,6 +37,26 @@ type OptionsPickerProps = { function OptionsPicker({options, selectedOption, onOptionSelected, style, isDisabled}: OptionsPickerProps) { const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + if (shouldUseNarrowLayout) { + return ( + + {options.map((option, index) => ( + + onOptionSelected(option.key)} + /> + {index < options.length - 1 && } + + ))} + + ); + } return ( diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index b3b59fcb856a..3d4216704234 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -38,8 +38,8 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct return ( { - const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? ''); - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? ''); + const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '-1'); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1'); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)); if (isVisibleAction && !isOffline) { diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 0bdf83bf4f27..1ed819ca853b 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -12,6 +12,7 @@ import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import FocusableMenuItem from './FocusableMenuItem'; +import FocusTrapForModal from './FocusTrap/FocusTrapForModal'; import * as Expensicons from './Icon/Expensicons'; import type {MenuItemProps} from './MenuItem'; import MenuItem from './MenuItem'; @@ -210,39 +211,41 @@ function PopoverMenu({ shouldSetModalVisibility={shouldSetModalVisibility} shouldEnableNewFocusManagement={shouldEnableNewFocusManagement} > - - {!!headerText && enteredSubMenuIndexes.length === 0 && {headerText}} - {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} - {currentMenuItems.map((item, menuIndex) => ( - selectItem(menuIndex)} - focused={focusedIndex === menuIndex} - displayInDefaultIconColor={item.displayInDefaultIconColor} - shouldShowRightIcon={item.shouldShowRightIcon} - iconRight={item.iconRight} - shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon} - label={item.label} - isLabelHoverable={item.isLabelHoverable} - floatRightAvatars={item.floatRightAvatars} - floatRightAvatarSize={item.floatRightAvatarSize} - shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar} - disabled={item.disabled} - onFocus={() => setFocusedIndex(menuIndex)} - success={item.success} - containerStyle={item.containerStyle} - /> - ))} - + + + {!!headerText && enteredSubMenuIndexes.length === 0 && {headerText}} + {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} + {currentMenuItems.map((item, menuIndex) => ( + selectItem(menuIndex)} + focused={focusedIndex === menuIndex} + displayInDefaultIconColor={item.displayInDefaultIconColor} + shouldShowRightIcon={item.shouldShowRightIcon} + iconRight={item.iconRight} + shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon} + label={item.label} + isLabelHoverable={item.isLabelHoverable} + floatRightAvatars={item.floatRightAvatars} + floatRightAvatarSize={item.floatRightAvatarSize} + shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar} + disabled={item.disabled} + onFocus={() => setFocusedIndex(menuIndex)} + success={item.success} + containerStyle={item.containerStyle} + /> + ))} + + ); } diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index cf960df5c2cb..e866f76e1237 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -86,12 +86,11 @@ function GenericPressable( if (shouldUseHapticsOnLongPress) { HapticFeedback.longPress(); } - if (ref && 'current' in ref) { + if (ref && 'current' in ref && nextFocusRef) { ref.current?.blur(); + Accessibility.moveAccessibilityFocus(nextFocusRef); } onLongPress(event); - - Accessibility.moveAccessibilityFocus(nextFocusRef); }, [shouldUseHapticsOnLongPress, onLongPress, nextFocusRef, ref, isDisabled], ); @@ -107,11 +106,11 @@ function GenericPressable( if (shouldUseHapticsOnPress) { HapticFeedback.press(); } - if (ref && 'current' in ref) { + if (ref && 'current' in ref && nextFocusRef) { ref.current?.blur(); + Accessibility.moveAccessibilityFocus(nextFocusRef); } const onPressResult = onPress(event); - Accessibility.moveAccessibilityFocus(nextFocusRef); return onPressResult; }, [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled], diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 01896fb0a3cb..46ec51994d90 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -59,7 +59,7 @@ function ProcessMoneyReportHoldMenu({ const onSubmit = (full: boolean) => { if (isApprove) { IOU.approveMoneyRequest(moneyRequestReport, full); - if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '', moneyRequestReport.reportID)) { + if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport.reportID)) { Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport.reportID)); } } else if (chatReport && paymentType) { diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 946856ecec37..a520693cff57 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -110,7 +110,7 @@ function ReceiptImage({ return ( Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))} + onPress={() => Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '-1', reportField.fieldID))} shouldShowRightIcon disabled={isFieldDisabled} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index fd82e723c6b9..b8f02f83d1cd 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -85,12 +85,12 @@ function MoneyRequestAction({ const onMoneyRequestPreviewPressed = () => { if (isSplitBillAction) { - const reportActionID = action.reportActionID ?? '0'; + const reportActionID = action.reportActionID ?? '-1'; Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, reportActionID)); return; } - const childReportID = action?.childReportID ?? '0'; + const childReportID = action?.childReportID ?? '-1'; Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index e03ed59ffb79..83fb8cd25292 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -28,6 +28,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {TransactionDetails} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; @@ -83,7 +84,13 @@ function MoneyRequestPreviewContent({ // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerID === sessionAccountID; - const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant} = ReportUtils.getTransactionDetails(transaction) ?? {}; + const { + amount: requestAmount, + currency: requestCurrency, + comment: requestComment, + merchant, + } = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]); + const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(transaction); @@ -91,9 +98,9 @@ function MoneyRequestPreviewContent({ const isOnHold = TransactionUtils.isOnHold(transaction); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; const isPartialHold = isSettlementOrApprovalPartial && isOnHold; - const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '', transactionViolations); + const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '-1', transactionViolations); const hasNoticeTypeViolations = !!( - TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID ?? '', transactionViolations) && + TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID ?? '-1', transactionViolations) && ReportUtils.isPaidGroupPolicy(iouReport) && canUseViolations ); @@ -106,6 +113,7 @@ function MoneyRequestPreviewContent({ const isFullySettled = isSettled && !isSettlementOrApprovalPartial; const isFullyApproved = ReportUtils.isReportApproved(iouReport) && !isSettlementOrApprovalPartial; const shouldShowRBR = hasNoticeTypeViolations || hasViolations || hasFieldErrors || (!isFullySettled && !isFullyApproved && isOnHold); + const showCashOrCard = isCardTransaction ? translate('iou.card') : translate('iou.cash'); const shouldShowHoldMessage = !(isSettled && !isSettlementOrApprovalPartial) && isOnHold; /* @@ -140,7 +148,7 @@ function MoneyRequestPreviewContent({ }; const getPreviewHeaderText = (): string => { - let message = translate('iou.cash'); + let message = showCashOrCard; if (isDistanceRequest) { message = translate('common.distance'); @@ -196,7 +204,7 @@ function MoneyRequestPreviewContent({ if (TransactionUtils.isPending(transaction)) { return {shouldShow: true, messageIcon: Expensicons.CreditCardHourglass, messageDescription: translate('iou.transactionPending')}; } - if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '', transactionViolations))) { + if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; @@ -363,7 +371,7 @@ function MoneyRequestPreviewContent({ onPressOut={() => ControlSelection.unblock()} onLongPress={showContextMenu} shouldUseHapticsOnLongPress - accessibilityLabel={isBillSplit ? translate('iou.split') : translate('iou.cash')} + accessibilityLabel={isBillSplit ? translate('iou.split') : showCashOrCard} accessibilityHint={CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency)} style={[ styles.moneyRequestPreviewBox, diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index cc99a3f6e108..96345ac117ff 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -28,6 +28,7 @@ import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {TransactionDetails} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; @@ -98,7 +99,7 @@ function MoneyRequestView({ const session = useSession(); const {isOffline} = useNetwork(); const {translate, toLocaleDigit} = useLocalize(); - const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null; + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '-1'] ?? null; const isTrackExpense = ReportUtils.isTrackExpenseReport(report); const {canUseViolations, canUseP2PDistanceRequests} = usePermissions(isTrackExpense ? CONST.IOU.TYPE.TRACK : undefined); const moneyRequestReport = parentReport; @@ -115,7 +116,7 @@ function MoneyRequestView({ originalAmount: transactionOriginalAmount, originalCurrency: transactionOriginalCurrency, cardID: transactionCardID, - } = ReportUtils.getTransactionDetails(transaction) ?? {}; + } = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; @@ -179,7 +180,7 @@ function MoneyRequestView({ let amountDescription = `${translate('iou.amount')}`; const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); - const rateID = transaction?.comment.customUnit?.customUnitRateID ?? '0'; + const rateID = transaction?.comment.customUnit?.customUnitRateID ?? '-1'; const currency = policy ? policy.outputCurrency : PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; @@ -203,7 +204,7 @@ function MoneyRequestView({ Navigation.dismissModal(); return; } - IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '', report?.reportID, newBillable, policy, policyTagList, policyCategories); + IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '-1', report?.reportID, newBillable, policy, policyTagList, policyCategories); Navigation.dismissModal(); }, [transaction, report, policy, policyTagList, policyCategories], @@ -284,7 +285,7 @@ function MoneyRequestView({ interactive={canEditDistance} shouldShowRightIcon={canEditDistance} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))} /> {/* TODO: correct the pending field action https://github.com/Expensify/App/issues/36987 */} @@ -309,7 +310,7 @@ function MoneyRequestView({ interactive={canEditDistance} shouldShowRightIcon={canEditDistance} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))} /> ); @@ -413,7 +414,7 @@ function MoneyRequestView({ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '', + transaction?.transactionID ?? '-1', report.reportID, Navigation.getActiveRouteWithoutParams(), ), @@ -432,7 +433,7 @@ function MoneyRequestView({ titleStyle={styles.textHeadlineH2} interactive={canEditAmount} shouldShowRightIcon={canEditAmount} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))} brickRoadIndicator={getErrorForField('amount') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('amount')} /> @@ -445,7 +446,9 @@ function MoneyRequestView({ interactive={canEdit} shouldShowRightIcon={canEdit} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID))} + onPress={() => + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID)) + } wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} brickRoadIndicator={getErrorForField('comment') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('comment')} @@ -463,7 +466,7 @@ function MoneyRequestView({ shouldShowRightIcon={canEditMerchant} titleStyle={styles.flex1} onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID)) + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID)) } wrapperStyle={[styles.taskDescriptionMenuItem]} brickRoadIndicator={getErrorForField('merchant') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} @@ -479,7 +482,7 @@ function MoneyRequestView({ interactive={canEditDate} shouldShowRightIcon={canEditDate} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))} brickRoadIndicator={getErrorForField('date') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('date')} /> @@ -493,7 +496,7 @@ function MoneyRequestView({ shouldShowRightIcon={canEdit} titleStyle={styles.flex1} onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID)) + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID)) } brickRoadIndicator={getErrorForField('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('category')} @@ -520,7 +523,7 @@ function MoneyRequestView({ shouldShowRightIcon={canEditTaxFields} titleStyle={styles.flex1} onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID)) + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID)) } brickRoadIndicator={getErrorForField('tax') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('tax')} @@ -537,7 +540,7 @@ function MoneyRequestView({ shouldShowRightIcon={canEditTaxFields} titleStyle={styles.flex1} onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', report.reportID)) + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID)) } /> @@ -584,7 +587,7 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, }, parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '-1'}`, canEvict: false, }, distanceRates: { @@ -595,17 +598,17 @@ export default withOnyx({ transaction: { key: ({report, parentReportActions}) => { - const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '']; + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '-1']; const originalMessage = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage : undefined; - const transactionID = originalMessage?.IOUTransactionID ?? 0; + const transactionID = originalMessage?.IOUTransactionID ?? -1; return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, }, transactionViolations: { key: ({report, parentReportActions}) => { - const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '']; + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '-1']; const originalMessage = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage : undefined; - const transactionID = originalMessage?.IOUTransactionID ?? 0; + const transactionID = originalMessage?.IOUTransactionID ?? -1; return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; }, }, diff --git a/src/components/ReportActionItem/RenameAction.tsx b/src/components/ReportActionItem/RenameAction.tsx index b4b2553d652a..f2a3fead2336 100644 --- a/src/components/ReportActionItem/RenameAction.tsx +++ b/src/components/ReportActionItem/RenameAction.tsx @@ -16,9 +16,9 @@ function RenameAction({currentUserPersonalDetails, action}: RenameActionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const currentUserAccountID = currentUserPersonalDetails.accountID ?? ''; + const currentUserAccountID = currentUserPersonalDetails.accountID ?? '-1'; const userDisplayName = action.person?.[0]?.text; - const actorAccountID = action.actorAccountID ?? ''; + const actorAccountID = action.actorAccountID ?? '-1'; const displayName = actorAccountID === currentUserAccountID ? `${translate('common.you')}` : `${userDisplayName}`; const originalMessage = action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED ? action.originalMessage : null; const oldName = originalMessage?.oldName ?? ''; diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 9bebef8f75ec..1251be83994b 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -128,7 +128,7 @@ function ReportActionItemImage({ - Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(transactionThreadReport?.reportID ?? report?.reportID ?? '', transaction?.transactionID ?? '')) + Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(transactionThreadReport?.reportID ?? report?.reportID ?? '-1', transaction?.transactionID ?? '-1')) } accessibilityLabel={translate('accessibilityHints.viewAttachment')} accessibilityRole={CONST.ROLE.BUTTON} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 00fd3cc8e036..daa7e24709c2 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -15,6 +15,7 @@ import SettlementButton from '@components/SettlementButton'; import {showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -109,6 +110,7 @@ function ReportPreview({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {canUseViolations} = usePermissions(); + const {isOffline} = useNetwork(); const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyTransactionsWithPendingRoutes, hasNonReimbursableTransactions} = useMemo( () => ({ @@ -155,7 +157,7 @@ function ReportPreview({ const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction})); const showRTERViolationMessage = numberOfRequests === 1 && - TransactionUtils.hasPendingUI(allTransactions[0], TransactionUtils.getTransactionViolations(allTransactions[0]?.transactionID ?? '', transactionViolations)); + TransactionUtils.hasPendingUI(allTransactions[0], TransactionUtils.getTransactionViolations(allTransactions[0]?.transactionID ?? '-1', transactionViolations)); let formattedMerchant = numberOfRequests === 1 ? TransactionUtils.getMerchant(allTransactions[0]) : null; const formattedDescription = numberOfRequests === 1 ? TransactionUtils.getDescription(allTransactions[0]) : null; @@ -421,7 +423,8 @@ function ReportPreview({ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }} - isDisabled={!canAllowSettlement} + isDisabled={isOffline && !canAllowSettlement} + isLoading={!isOffline && !canAllowSettlement} /> )} {shouldShowSubmitButton && ( diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 553051e07104..be64dfd60f59 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -72,7 +72,7 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? '')); - const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? ''; + const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? '-1'; const htmlForTaskPreview = taskAssigneeAccountID !== 0 ? ` ${taskTitle}` : `${taskTitle}`; const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action); diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 1e9cfd23f669..c597b8c741db 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -19,6 +19,7 @@ import type {CentralPaneNavigatorParamList, RootStackParamList} from '@libs/Navi import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; +import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen'; import HeaderGap from './HeaderGap'; import KeyboardAvoidingView from './KeyboardAvoidingView'; import OfflineIndicator from './OfflineIndicator'; @@ -100,7 +101,9 @@ type ScreenWrapperProps = { shouldShowOfflineIndicatorInWideScreen?: boolean; }; -const ScreenWrapperStatusContext = createContext({didScreenTransitionEnd: false}); +type ScreenWrapperStatusContextType = {didScreenTransitionEnd: boolean}; + +const ScreenWrapperStatusContext = createContext(undefined); function ScreenWrapper( { @@ -211,7 +214,7 @@ function ScreenWrapper( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isAvoidingViewportScroll = useTackInputFocus(shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileSafari()); + const isAvoidingViewportScroll = useTackInputFocus(shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit()); const contextValue = useMemo(() => ({didScreenTransitionEnd}), [didScreenTransitionEnd]); return ( @@ -239,53 +242,55 @@ function ScreenWrapper( } return ( - + - - - - - {isDevelopment && } - - { - // If props.children is a function, call it to provide the insets to the children. - typeof children === 'function' - ? children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd, - }) - : children - } - - {isSmallScreenWidth && shouldShowOfflineIndicator && } - {!isSmallScreenWidth && shouldShowOfflineIndicatorInWideScreen && ( - - )} - - + + + + {isDevelopment && } + + { + // If props.children is a function, call it to provide the insets to the children. + typeof children === 'function' + ? children({ + insets, + safeAreaPaddingBottomStyle, + didScreenTransitionEnd, + }) + : children + } + {isSmallScreenWidth && shouldShowOfflineIndicator && } + {!isSmallScreenWidth && shouldShowOfflineIndicatorInWideScreen && ( + + )} + + + + - + ); }} diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 3d295e72c09e..2de6aa8cfcef 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -66,7 +66,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { }, [hash, isOffline]); const isLoadingItems = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || searchResults?.data === undefined; - const isLoadingMoreItems = !isLoadingItems && searchResults?.search?.isLoading; + const isLoadingMoreItems = !isLoadingItems && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; const shouldShowEmptyState = !isLoadingItems && isEmptyObject(searchResults?.data); if (isLoadingItems) { @@ -122,6 +122,8 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const isSortingAllowed = sortableSearchTabs.includes(query); + const shouldShowYear = SearchUtils.shouldShowYear(searchResults?.data); + return ( customListHeader={ @@ -131,6 +133,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { sortOrder={sortOrder} isSortingAllowed={isSortingAllowed} sortBy={sortBy} + shouldShowYear={shouldShowYear} /> } // To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling, diff --git a/src/components/Section/index.tsx b/src/components/Section/index.tsx index e7cadbe73b82..49ef1e89cff9 100644 --- a/src/components/Section/index.tsx +++ b/src/components/Section/index.tsx @@ -86,6 +86,9 @@ type SectionProps = Partial & { /** The height of the icon. */ iconHeight?: number; + + /** Banner to display at the top of the section */ + banner?: ReactNode; }; function isIllustrationLottieAnimation(illustration: DotLottieAnimation | IconAsset | undefined): illustration is DotLottieAnimation { @@ -119,6 +122,7 @@ function Section({ iconWidth, iconHeight, renderSubtitle, + banner = null, }: SectionProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -133,6 +137,7 @@ function Section({ return ( + {banner} {cardLayout === CARD_LAYOUT.ICON_ON_TOP && ( ({ wrapperStyle, containerStyle, isDisabled = false, - shouldPreventDefaultFocusOnSelectRow = false, shouldPreventEnterKeySubmit = false, canSelectMultiple = false, onSelectRow, @@ -88,7 +87,7 @@ function BaseListItem({ hoverDimmingValue={1} hoverStyle={[!item.isDisabled && styles.hoveredComponentBG, hoverStyle]} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} - onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + onMouseDown={(e) => e.preventDefault()} id={keyForList ?? ''} style={pressableStyle} onFocus={onFocus} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 6bbe69a1e2c6..ae092b8c2729 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -86,6 +86,7 @@ function BaseSelectionList( windowSize = 5, updateCellsBatchingPeriod = 50, removeClippedSubviews = true, + shouldDelayFocus = true, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -507,27 +508,28 @@ function BaseSelectionList( }; }, [debouncedSelectFocusedOption, shouldDebounceRowSelect]); + /** Function to focus text input */ + const focusTextInput = useCallback(() => { + if (!innerTextInputRef.current) { + return; + } + + innerTextInputRef.current.focus(); + }, []); + /** Focuses the text input when the component comes into focus and after any navigation animations finish. */ useFocusEffect( useCallback(() => { - if (!textInputAutoFocus) { - return; - } - if (shouldShowTextInput) { - focusTimeoutRef.current = setTimeout(() => { - if (!innerTextInputRef.current) { - return; - } - innerTextInputRef.current.focus(); - }, CONST.ANIMATED_TRANSITION); - } - return () => { - if (!focusTimeoutRef.current) { - return; + if (textInputAutoFocus && shouldShowTextInput) { + if (shouldDelayFocus) { + focusTimeoutRef.current = setTimeout(focusTextInput, CONST.ANIMATED_TRANSITION); + } else { + focusTextInput(); } - clearTimeout(focusTimeoutRef.current); - }; - }, [shouldShowTextInput, textInputAutoFocus]), + } + + return () => focusTimeoutRef.current && clearTimeout(focusTimeoutRef.current); + }, [shouldShowTextInput, textInputAutoFocus, shouldDelayFocus, focusTextInput]), ); const prevTextInputValue = usePrevious(textInputValue); diff --git a/src/components/SelectionList/InviteMemberListItem.tsx b/src/components/SelectionList/InviteMemberListItem.tsx index 13b0014efb2d..2b3c01c04a69 100644 --- a/src/components/SelectionList/InviteMemberListItem.tsx +++ b/src/components/SelectionList/InviteMemberListItem.tsx @@ -24,7 +24,6 @@ function InviteMemberListItem({ onSelectRow, onCheckboxPress, onDismissError, - shouldPreventDefaultFocusOnSelectRow, rightHandSideComponent, onFocus, shouldSyncFocus, @@ -56,7 +55,6 @@ function InviteMemberListItem({ canSelectMultiple={canSelectMultiple} onSelectRow={onSelectRow} onDismissError={onDismissError} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} errors={item.errors} pendingAction={item.pendingAction} diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx index c7884690c067..48ca474f6c60 100644 --- a/src/components/SelectionList/RadioListItem.tsx +++ b/src/components/SelectionList/RadioListItem.tsx @@ -13,7 +13,6 @@ function RadioListItem({ isDisabled, onSelectRow, onDismissError, - shouldPreventDefaultFocusOnSelectRow, shouldPreventEnterKeySubmit, rightHandSideComponent, isMultilineSupported = false, @@ -34,7 +33,6 @@ function RadioListItem({ showTooltip={showTooltip} onSelectRow={onSelectRow} onDismissError={onDismissError} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} shouldPreventEnterKeySubmit={shouldPreventEnterKeySubmit} rightHandSideComponent={rightHandSideComponent} keyForList={item.keyForList} diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx index 9adff46395e6..2273b80e529d 100644 --- a/src/components/SelectionList/Search/ReportListItem.tsx +++ b/src/components/SelectionList/Search/ReportListItem.tsx @@ -68,7 +68,6 @@ function ReportListItem({ canSelectMultiple, onSelectRow, onDismissError, - shouldPreventDefaultFocusOnSelectRow, onFocus, shouldSyncFocus, }: ReportListItemProps) { @@ -119,7 +118,6 @@ function ReportListItem({ canSelectMultiple={canSelectMultiple} onSelectRow={() => openReportInRHP(transactionItem)} onDismissError={onDismissError} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} onFocus={onFocus} shouldSyncFocus={shouldSyncFocus} /> @@ -138,7 +136,6 @@ function ReportListItem({ canSelectMultiple={canSelectMultiple} onSelectRow={onSelectRow} onDismissError={onDismissError} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} errors={item.errors} pendingAction={item.pendingAction} keyForList={item.keyForList} diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index ecf9264301c2..23ab549dd495 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -13,7 +13,6 @@ function TransactionListItem({ canSelectMultiple, onSelectRow, onDismissError, - shouldPreventDefaultFocusOnSelectRow, onFocus, shouldSyncFocus, }: TransactionListItemProps) { @@ -42,7 +41,6 @@ function TransactionListItem({ canSelectMultiple={canSelectMultiple} onSelectRow={onSelectRow} onDismissError={onDismissError} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} errors={item.errors} pendingAction={item.pendingAction} keyForList={item.keyForList} diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index d17d923a54e1..01a8a8bbb9c0 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -13,6 +13,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import DateUtils from '@libs/DateUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import variables from '@styles/variables'; @@ -87,7 +88,9 @@ function ReceiptCell({transactionItem}: TransactionCellProps) { function DateCell({transactionItem, showTooltip, isLargeScreenWidth}: TransactionCellProps) { const styles = useThemeStyles(); - const date = TransactionUtils.getCreated(transactionItem, CONST.DATE.MONTH_DAY_ABBR_FORMAT); + + const created = TransactionUtils.getCreated(transactionItem); + const date = DateUtils.formatWithUTCTimeZone(created, DateUtils.doesDateBelongToAPastYear(created) ? CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT : CONST.DATE.MONTH_DAY_ABBR_FORMAT); return ( - + void; + shouldShowYear: boolean; }; -function SearchTableHeader({data, sortBy, sortOrder, isSortingAllowed, onSortPress}: SearchTableHeaderProps) { +function SearchTableHeader({data, sortBy, sortOrder, isSortingAllowed, onSortPress, shouldShowYear}: SearchTableHeaderProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions(); @@ -123,7 +124,7 @@ function SearchTableHeader({data, sortBy, sortOrder, isSortingAllowed, onSortPre textStyle={textStyle} sortOrder={sortOrder ?? CONST.SORT_ORDER.ASC} isActive={isActive} - containerStyle={[StyleUtils.getSearchTableColumnStyles(columnName)]} + containerStyle={[StyleUtils.getSearchTableColumnStyles(columnName, shouldShowYear)]} isSortable={isSortable} onPress={(order: SortOrder) => onSortPress(columnName, order)} /> diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx index d07d658f6b12..9fc138254f8b 100644 --- a/src/components/SelectionList/TableListItem.tsx +++ b/src/components/SelectionList/TableListItem.tsx @@ -21,7 +21,6 @@ function TableListItem({ onSelectRow, onCheckboxPress, onDismissError, - shouldPreventDefaultFocusOnSelectRow, rightHandSideComponent, onFocus, shouldSyncFocus, @@ -53,7 +52,6 @@ function TableListItem({ canSelectMultiple={canSelectMultiple} onSelectRow={onSelectRow} onDismissError={onDismissError} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} errors={item.errors} pendingAction={item.pendingAction} diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx index d07ac03c00f5..104990cf479c 100644 --- a/src/components/SelectionList/UserListItem.tsx +++ b/src/components/SelectionList/UserListItem.tsx @@ -25,7 +25,6 @@ function UserListItem({ onSelectRow, onCheckboxPress, onDismissError, - shouldPreventDefaultFocusOnSelectRow, shouldPreventEnterKeySubmit, rightHandSideComponent, onFocus, @@ -59,7 +58,6 @@ function UserListItem({ canSelectMultiple={canSelectMultiple} onSelectRow={onSelectRow} onDismissError={onDismissError} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} shouldPreventEnterKeySubmit={shouldPreventEnterKeySubmit} rightHandSideComponent={rightHandSideComponent} errors={item.errors} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 5d5e7fc3891b..f70cac628498 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -168,6 +168,11 @@ type TransactionListItemType = ListItem & /** Whether we should show the tax column */ shouldShowTax: boolean; + + /** Whether we should show the transaction year. + * This is true if at least one transaction in the dataset was created in past years + */ + shouldShowYear: boolean; }; type ReportListItemType = ListItem & diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 85954e68c5a9..e1d9ffaeeef8 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -185,7 +185,7 @@ function SettlementButton({ // To achieve the one tap pay experience we need to choose the correct payment type as default. // If the user has previously chosen a specific payment option or paid for some expense, // let's use the last payment method or use default. - const paymentMethod = nvpLastPaymentMethod?.[policyID] ?? ''; + const paymentMethod = nvpLastPaymentMethod?.[policyID] ?? '-1'; if (canUseWallet) { buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.EXPENSIFY]); } diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index 3a996a8d2c64..7ae3ca4fb825 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -19,9 +19,9 @@ type ShowContextMenuContextProps = { const ShowContextMenuContext = createContext({ anchor: null, - report: null, - action: null, - transactionThreadReport: null, + report: undefined, + action: undefined, + transactionThreadReport: undefined, checkIfContextMenuActive: () => {}, }); diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 46a88c54219c..789b4b4957c3 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -27,8 +27,8 @@ type SubIcon = { }; type SubscriptAvatarProps = { - /** Avatar URL or icon */ - mainAvatar?: IconType; + /** Avatar icon */ + mainAvatar: IconType; /** Subscript avatar URL or icon */ secondaryAvatar?: IconType; diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 788734242f7b..0c7e603a4aa2 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -33,7 +33,7 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps)