diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml index 25fafcb7be7f..b86b68cc7d7d 100644 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml @@ -51,14 +51,13 @@ runs: - uses: Expensify/App/.github/actions/composite/setupNode@main - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "oracle" java-version: "17" - - uses: ruby/setup-ruby@v1.187.0 + - uses: ruby/setup-ruby@v1.190.0 with: - ruby-version: "2.7" bundler-cache: true - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh index 3d96ba17a799..05c402ad7766 100755 --- a/.github/scripts/verifyRedirect.sh +++ b/.github/scripts/verifyRedirect.sh @@ -32,6 +32,12 @@ while read -r line; do SOURCE_URL=${LINE_PARTS[0]} DEST_URL=${LINE_PARTS[1]} + # Make sure that the source and destination are not identical + if [[ "$SOURCE_URL" == "$DEST_URL" ]]; then + error "Source and destination URLs are identical: $SOURCE_URL" + exit 1 + fi + # Make sure the format of the line is as expected. if [[ "${#LINE_PARTS[@]}" -gt 2 ]]; then error "Found a line with more than one comma: $line" diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 90d81ada23c6..bacab79998f9 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -54,15 +54,14 @@ jobs: uses: ./.github/actions/composite/setupNode - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'oracle' java-version: '17' - name: Setup Ruby - uses: ruby/setup-ruby@v1.187.0 + uses: ruby/setup-ruby@v1.190.0 with: - ruby-version: '2.7' bundler-cache: true - name: Decrypt keystore @@ -186,9 +185,8 @@ jobs: uses: ./.github/actions/composite/setupNode - name: Setup Ruby - uses: ruby/setup-ruby@v1.187.0 + uses: ruby/setup-ruby@v1.190.0 with: - ruby-version: '2.7' bundler-cache: true - name: Cache Pod dependencies diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 95177e0bcefd..21f7fcedfe85 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -81,15 +81,14 @@ jobs: uses: ./.github/actions/composite/setupNode - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'oracle' java-version: '17' - name: Setup Ruby - uses: ruby/setup-ruby@v1.187.0 + uses: ruby/setup-ruby@v1.190.0 with: - ruby-version: '2.7' bundler-cache: true - name: Decrypt keystore @@ -161,9 +160,8 @@ jobs: run: sudo xcode-select -switch /Applications/Xcode_15.2.0.app - name: Setup Ruby - uses: ruby/setup-ruby@v1.187.0 + uses: ruby/setup-ruby@v1.190.0 with: - ruby-version: '2.7' bundler-cache: true - name: Cache Pod dependencies diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000000..a0891f563f38 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.4 diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index ec8e17dda4cf..d3829fe01779 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -9,7 +9,6 @@ import {LocaleContextProvider} from '@src/components/LocaleContextProvider'; import OnyxProvider from '@src/components/OnyxProvider'; import {EnvironmentProvider} from '@src/components/withEnvironment'; import {KeyboardStateProvider} from '@src/components/withKeyboardState'; -import {WindowDimensionsProvider} from '@src/components/withWindowDimensions'; import ONYXKEYS from '@src/ONYXKEYS'; import './fonts.css'; @@ -22,9 +21,7 @@ Onyx.init({ const decorators = [ (Story: React.ElementType) => ( - + ), diff --git a/Gemfile b/Gemfile index 2739d835b4e0..21947c0eb961 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version -ruby ">= 2.6.10" +ruby ">= 3.3.4" gem "cocoapods", "= 1.15.2" gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 10acc25586ad..00232570d5de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -299,7 +299,7 @@ DEPENDENCIES xcpretty (~> 0) RUBY VERSION - ruby 2.6.10p210 + ruby 3.3.4p94 BUNDLED WITH 2.4.19 diff --git a/README.md b/README.md index 529d6cb0ccfd..c8faff111bae 100644 --- a/README.md +++ b/README.md @@ -142,14 +142,14 @@ You create this certificate by following the instructions in [`Configuring HTTPS 3. Install the certificate as CA certificate from the settings. On the Android emulator, this option can be found in Settings > Security > Encryption & Credentials > Install a certificate > CA certificate. 4. Close the emulator. -Note - If you want to run app on `https://127.0.0.1:8082`, then just install the certificate and use `adb reverse tcp:8082 tcp:8082` on every startup. +**Note:** If you want to run app on `https://127.0.0.1:8082`, then just install the certificate and use `adb reverse tcp:8082 tcp:8082` on every startup. #### Android Flow 1. Run `npm run setupNewDotWebForEmulators android` 2. Select the emulator you want to run if prompted. (If single emulator is available, then it will open automatically) 3. Let the script execute till the message `🎉 Done!`. -Note - If you want to run app on `https://dev.new.expensify.com:8082`, then just do the Android flow and use `npm run startAndroidEmulator` to start the Android Emulator every time (It will configure the emulator). +**Note:** If you want to run app on `https://dev.new.expensify.com:8082`, then just do the Android flow and use `npm run startAndroidEmulator` to start the Android Emulator every time (It will configure the emulator). Possible Scenario: @@ -392,7 +392,7 @@ In most cases, the code written for this repo should be platform-independent. In - Web => `index.website.js` - Desktop => `index.desktop.js` -Note that `index.js` should be the default and only platform-specific implementations should be done in their respective files. i.e: If you have mobile-specific implementation in `index.native.js`, then the desktop/web implementation can be contained in a shared `index.js`. +**Note:** `index.js` should be the default and only platform-specific implementations should be done in their respective files. i.e: If you have mobile-specific implementation in `index.native.js`, then the desktop/web implementation can be contained in a shared `index.js`. `index.ios.js` and `index.android.js` are used when the app is running natively on respective platforms. These files are not used when users access the app through mobile browsers, but `index.website.js` is used instead. `index.native.js` are for both iOS and Android native apps. `index.native.js` should not be included in the same module as `index.ios.js` or `index.android.js`. diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 3deeabf6df2a..4c2a86818e9b 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -26,7 +26,6 @@ jest.doMock('react-native', () => { type ReactNativeMock = typeof ReactNative & { NativeModules: typeof ReactNative.NativeModules & { BootSplash: { - getVisibilityStatus: typeof BootSplash.getVisibilityStatus; hide: typeof BootSplash.hide; logoSizeRatio: number; navigationBarHeight: number; @@ -46,7 +45,6 @@ jest.doMock('react-native', () => { NativeModules: { ...ReactNative.NativeModules, BootSplash: { - getVisibilityStatus: jest.fn(), hide: jest.fn(), logoSizeRatio: 1, navigationBarHeight: 0, diff --git a/android/app/build.gradle b/android/app/build.gradle index 6a1c329980af..8b1a23b72e95 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009002800 - versionName "9.0.28-0" + versionCode 1009002905 + versionName "9.0.29-5" // 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/images/product-illustrations/broken-magnifying-glass.svg b/assets/images/product-illustrations/broken-magnifying-glass.svg new file mode 100644 index 000000000000..0b85744c1869 --- /dev/null +++ b/assets/images/product-illustrations/broken-magnifying-glass.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/spreadsheet-computer.svg b/assets/images/spreadsheet-computer.svg new file mode 100644 index 000000000000..74cac455537a --- /dev/null +++ b/assets/images/spreadsheet-computer.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/table.svg b/assets/images/table.svg new file mode 100644 index 000000000000..a9cfe68f339e --- /dev/null +++ b/assets/images/table.svg @@ -0,0 +1,3 @@ + + + diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index eab59a65d003..ca9ca23a8577 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -31,6 +31,15 @@ This project and everyone participating in it is governed by the Expensify [Code ## Restrictions At this time, we are not hiring contractors in Crimea, North Korea, Russia, Iran, Cuba, or Syria. +## Slack channels +All contributors should be a member of a shared Slack channel called [#expensify-open-source](https://expensify.slack.com/archives/C01GTK53T8Q) -- this channel is used to ask **general questions**, facilitate **discussions**, and make **feature requests**. + +Before requesting an invite to Slack, please ensure your Upwork account is active, since we only pay via Upwork (see [below](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#payment-for-contributions)). To request an invite to Slack, email contributors@expensify.com with the subject `Slack Channel Invites`. We'll send you an invite! + +Note: Do not send direct messages to the Expensify team in Slack or Expensify Chat, they will not be able to respond. + +Note: if you are hired for an Upwork job and have any job-specific questions, please ask in the GitHub issue or pull request. This will ensure that the person addressing your question has as much context as possible. + ## Reporting Vulnerabilities If you've found a vulnerability, please email security@expensify.com with the subject `Vulnerability Report` instead of creating an issue. diff --git a/desktop/package-lock.json b/desktop/package-lock.json index aeac3b7aca08..1187b3182187 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -9,7 +9,7 @@ "dependencies": { "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", - "electron-updater": "^6.3.2", + "electron-updater": "^6.3.3", "mime-types": "^2.1.35", "node-machine-id": "^1.1.12" }, @@ -154,9 +154,10 @@ "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==" }, "node_modules/electron-updater": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.2.tgz", - "integrity": "sha512-bEpuZ1IRnMtvZZaWeYi9ocX90Cnk+/impZ/08r6GQkfOMqECtKC2IjvxHcDk2VpWO8QZzK0+MUNaBiO81CGvQQ==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.3.tgz", + "integrity": "sha512-Kj1u6kfyxUyatnspvKa6qhGn82rMZfUD03WOvCGJ12PyRss/AC8kkYsN9IrJihKTlN8nRwTjZ1JM2UUXoD0KsA==", + "license": "MIT", "dependencies": { "builder-util-runtime": "9.2.5", "fs-extra": "^10.1.0", @@ -551,9 +552,9 @@ "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==" }, "electron-updater": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.2.tgz", - "integrity": "sha512-bEpuZ1IRnMtvZZaWeYi9ocX90Cnk+/impZ/08r6GQkfOMqECtKC2IjvxHcDk2VpWO8QZzK0+MUNaBiO81CGvQQ==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.3.tgz", + "integrity": "sha512-Kj1u6kfyxUyatnspvKa6qhGn82rMZfUD03WOvCGJ12PyRss/AC8kkYsN9IrJihKTlN8nRwTjZ1JM2UUXoD0KsA==", "requires": { "builder-util-runtime": "9.2.5", "fs-extra": "^10.1.0", diff --git a/desktop/package.json b/desktop/package.json index 7537ed7a75cf..cf3c3f4354b3 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -6,7 +6,7 @@ "dependencies": { "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", - "electron-updater": "^6.3.2", + "electron-updater": "^6.3.3", "mime-types": "^2.1.35", "node-machine-id": "^1.1.12" }, diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills.md new file mode 100644 index 000000000000..465f6742eaea --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills.md @@ -0,0 +1,79 @@ +--- +title: Pay Bills +description: Expensify bill management and payment methods. +--- +Streamline your operations by receiving and paying vendor or supplier bills directly in Expensify. Vendors can send bills even if they don't have an Expensify account, and you can manage payments seamlessly. + +## Receive Bills in Expensify +You can receive bills in three ways: +- Directly from Vendors: Provide your Expensify billing email to vendors. +- Forwarding Emails: Forward bills received in your email to Expensify. +- Manual Upload: For physical bills, create a Bill in Expensify from the Reports page. + +## Bill Pay Workflow +1. When a vendor or supplier sends a bill to Expensify, the document is automatically SmartScanned, and a Bill is created. This Bill is managed by the primary domain contact, who can view it on the Reports page within their default group policy. + +2. Once the Bill is ready for processing, it follows the established approval workflow. As each person approves it, the Bill appears in the next approver’s Inbox. The final approver will pay the Bill using one of the available payment methods. + +3. During this process, the Bill is coded with the appropriate GL codes from your connected accounting software. After completing the approval workflow, the Bill can be exported back to your accounting system. + +## Payment Methods +There are multiple ways to pay Bills in Expensify. Let’s go over each method below. + +### ACH bank-to-bank transfer + +To use this payment method, you must have a [business bank account connected to your Expensify account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account). + +**To pay with an ACH bank-to-bank transfer:** +1. Sign in to your [Expensify web account](www.expensify.com). +2. Go to the Home or Reports page and locate the Bill that needs to be paid. +3. Click the Pay button to be redirected to the Bill. +4. Choose the ACH option from the drop-down list. + +**Fees:** None + +### Credit or Debit Card +This option is available to all US and International customers receiving a bill from a US vendor with a US business bank account. + +**To pay with a credit or debit card:** +1. Sign in to your [Expensify web account](www.expensify.com). +2. Click on the Bill you’d like to pay to see the details. +3. Click the Pay button. +4. Enter your credit card or debit card details. + +**Fees:** 2.9% of the total amount paid. + +### Venmo +If both you and the vendor must have Venmo connected to Expensify, you can pay the bill by following the steps outlined [here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments#setting-up-third-party-payments). + +**Fees:** Venmo charges a 3% sender’s fee. + + +### Pay outside of Expensify +If you are unable to pay using one of the above methods, you can still mark the Bill as paid. This will update its status to indicate that the payment was made outside Expensify. + +**To mark a Bill as paid outside of Expensify:** +1. Sign in to your [Expensify web account](www.expensify.com). +2. Click on the Bill you’d like to pay to see the details. +3. Click the Reimburse button. +4. Choose **I’ll do it manually**. + +**Fees:** None. + +{% include faq-begin.md %} + +## Who receives vendor bills in Expensify? +Bills are sent to the Primary Contact listed under **Settings > Domains > [Domain Name] > Domain Admins**. + +## Who can view and pay a Bill? +Only the primary domain contact can view and pay a Bill. + +## How can others access Bills? +The primary contact can share Bills or grant Copilot access for others to manage payments. + +## Is Bill Pay supported internationally? +Currently, payments are only supported in USD. + +## What's the difference between a Bill and an Invoice in Expensify? +A Bill represents a payable amount owed to a vendor, while an Invoice is a receivable amount owed to you. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md index a998e279c3f6..b7357245ac84 100644 --- a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md +++ b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md @@ -21,7 +21,7 @@ The pay-per-use rate is the full rate per active member without any discounts. T ## How the Expensify Card can reduce your bill Bundling the Expensify Card with an annual subscription ensures you pay the lowest possible monthly price for Expensify. And the more you spend on Expensify Cards, the lower your bill will be. -If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per member. This additional 50% discount, when coupled with an annual subscription, brings the price per member to $5 on a Collect plan and $9 on a Control plan. +If at least 50% of your total settled US spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per member. This additional 50% discount, when coupled with an annual subscription, brings the price per member to $5 on a Collect plan and $9 on a Control plan. Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more (_applies to US purchases only_). Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. ## Savings calculator @@ -30,11 +30,11 @@ To see how much money you can save (and even earn!) by using the Expensify Card, {% include faq-begin.md %} ## What if we put less than 50% of our total spend on the Expensify Card? -If less than 50% of your total USD spend is on the Expensify Card, the bill is discounted on a sliding scale. +If less than 50% of your total settled US spend in a given month is on the Expensify Card, your bill is discounted on a sliding scale. **Example:** - Annual subscription discount: 50% -- % of Expensify Card spend (US purchases only) across all workspaces: 20% +- % of total settled Expensify Card spend (US purchases only) across all workspaces: 20% - Expensify Card discount: 20% In that case, you'd save 70% on the price per member for that month's bill. diff --git a/docs/articles/expensify-classic/expensify-billing/Out-of-date-Billing.md b/docs/articles/expensify-classic/expensify-billing/Out-of-date-Billing.md new file mode 100644 index 000000000000..d6529dfd8e1c --- /dev/null +++ b/docs/articles/expensify-classic/expensify-billing/Out-of-date-Billing.md @@ -0,0 +1,29 @@ +--- +title: Out-of-date Billing +description: What to do if you receive an out-of-date billing notification +--- + +A notification that your workspace has out-of-date billing will appear for one of the following reasons: + +- A workspace you’re an Admin for has an expired/invalid payment card or insufficient funds. +- Your company’s Expensify trial has ended and it’s time to [upgrade your subscription](https://help.expensify.com/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription). + +## Step 1: Determine who the billing owner is + +1. Hover over **Settings** and click **Workspaces**. +2. Click the name of the workspace that has the `!` symbol next to it. +3. Review who is listed as the Billing Owner. +4. Have this person complete the steps below, or you can click **Take Over Billing** if you will take over handling payments for the Expensify workspace. + +## Step 2: Retry payment or update the payment card + +{% include info.html %} +This step must be completed by the Billing Owner. +{% include end-info.html %} + +1. Ensure that the card or bank account has sufficient funds for the payment. +2. Hover over **Settings** and click **Account**. +3. Click the **Payments** tab on the left. +4. Click **Retry Billing** if there were originally insufficient funds in the payment account, or click **Add Payment Card** to add a payment method. + +Once the payment is processed, the out-of-date billing notification will disappear. diff --git a/docs/articles/expensify-classic/expensify-billing/Receipt-Breakdown.md b/docs/articles/expensify-classic/expensify-billing/Receipt-Breakdown.md index fd137aab62fb..21e2db5604f8 100644 --- a/docs/articles/expensify-classic/expensify-billing/Receipt-Breakdown.md +++ b/docs/articles/expensify-classic/expensify-billing/Receipt-Breakdown.md @@ -18,7 +18,7 @@ The top section will show the total amount you paid as the billing owner of Expe ## How-to reduce your bill and get paid to use Expensify Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. -_Note: Currently, we offer Expensify Cards to companies with USD bank accounts._ +_Note: Currently, we offer Expensify Cards to companies with US bank accounts._ ## How-to understand your billing breakdown Your receipt will have a detailed breakdown of activity and discounts across all workspaces. Here's a description of items that may appear on your bill: @@ -34,12 +34,10 @@ Your receipt will have a detailed breakdown of activity and discounts across all - Any members included in your annual subscription on the Collect plan. - [Number of] Pay-per-use Collect members @ $20.00 - Any members above your annual subscription size on the Collect plan. These members are billed at the pay-per-use rate. -- [Number of] Free members @ $0.00 - - All members across any of your Free workspaces. - X% Expensify Card discount with $Y spend - - This shows the % discount you're getting based on total approved spend across your Expensify Cards. This is only available in the US. + - The % discount you're getting based on total settled US purchases across your Expensify Cards. - X% Expensify Card cash back credit for $Y spend - - The amount of cash back you've earned based on total approved spend across your Expensify Cards. This is only available in the US. + - The amount of cash back you've earned based on total settled US purchases across your Expensify Cards. - 50% ExpensifyApproved! partner discount - If you're part of an accounting firm, you get an additional discount for being our partner. [Learn more about our ExpensifyApproved! accountants program.](https://use.expensify.com/accountants-program) - Total diff --git a/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md index 922bf9d07056..e55a26dfacff 100644 --- a/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md +++ b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md @@ -7,7 +7,7 @@ description: An overview of all the various perks the Expensify Card offers ### Cashback Get 1% cash back with every swipe — no minimums necessary — and 2% back if you spend $250k+/month across cards. -This applies to USD purchases only. +This applies to US purchases only. ### Discounts on Monthly Expensify Bill Get the Expensify Visa® Commercial Card and use it for at least half of your organization's monthly expenses to save 50% on your monthly Expensify bill. diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md index 720badf1dbc6..bded231d1daa 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md @@ -59,7 +59,7 @@ Here’s how to set it up: Once this is done, you are all set to begin the process of enabling the Expensify Card. Not just for you, but if you have a co-founder, you can also issue them a card. ## Step 5: Get the Expensify Card -If you linked your business bank account in the previous step, you are immediately eligible for the Expensify Card. The Expensify Card is included with your Free Plan workspace, it earns you up to 2% cash back, and they are all stored in your team’s workspace. It’s easy to apply, and it takes minutes! +If you linked your business bank account in the previous step, you are immediately eligible for the Expensify Card. The Expensify Card is included with your Free Plan workspace, earns you 1% cash back on all US purchases (2% if you spend $250k+/mo across cards), and they are all stored in your team’s workspace. It’s easy to apply, and it takes minutes! Here’s how to enable the Expensify Card: diff --git a/docs/articles/expensify-classic/reports/Automatic-Receipt-Audit.md b/docs/articles/expensify-classic/reports/Automatic-Receipt-Audit.md new file mode 100644 index 000000000000..f0d112b86e9f --- /dev/null +++ b/docs/articles/expensify-classic/reports/Automatic-Receipt-Audit.md @@ -0,0 +1,21 @@ +--- +title: Automatic Receipt Audit +description: How Concierge Receipt Audit works +--- + +Concierge Receipt Audit is a real-time, automatic audit and compliance check for receipts submitted by employees and workspace members. Concierge checks every receipt for accuracy and compliance, flagging any expenses that are inaccurate or noncompliant. Flagged expenses are highlighted for manual review, leaving you with more control and visibility into employee expenses. + +All Expensify Control plans automatically come with Concierge Receipt Audit. If you have an Expensify Collect plan, you can [upgrade your workspace](https://help.expensify.com/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription) to get access to this feature. + +## How Concierge Receipt Audit works + +1. Concierge SmartScans every receipt to verify that the data entered by the workspace member matches the currency, date, and amount on the physical receipt. +2. After the report is submitted for approval, Concierge highlights any differences between the SmartScanned values and the employee's chosen input. +3. Each receipt that has been verified is labeled as "Verified." + +{% include faq-begin.md %} + +**Can I disable Concierge Receipt Audit?** + +All Control plan policies automatically include Concierge Receipt Audit. At this time, it cannot be disabled. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspaces/Create-tags.md b/docs/articles/expensify-classic/workspaces/Create-tags.md index 74967ee04c7a..ad3f51bc8c58 100644 --- a/docs/articles/expensify-classic/workspaces/Create-tags.md +++ b/docs/articles/expensify-classic/workspaces/Create-tags.md @@ -55,10 +55,10 @@ When you first connect your accounting integration (for example, QuickBooks Onli You can add a list of single tags by importing them in a .csv, .txt, .xls, or .xlsx spreadsheet. 1. Determine whether you will use independent (a separate tag for department and project) or dependent tags (the project tags populate different options based on the department selected), and whether you will capture general ledge (GL) codes. Then use one of the following templates to build your tags list: - - [Dependent tags with GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fus.v-cdn.net%2F6030147%2Fuploads%2FO7G7UWJCCFXC%2Fdependant-tag-with-gl-code-template.xlsx) - - [Dependent tags without GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fus.v-cdn.net%2F6030147%2Fuploads%2FY7DCMUVLSHEO%2Fdependant-tag-without-gl-code-template.xlsx) - - [Independent tags with GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1618929581886-Independent%2Bwith%2BGL%2Bcodes%2Bformat%2B-%2BSheet1.csv) - - [Independent tags without GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1618929575401-Independent%2Bwithout%2BGL%2Bcodes%2Bformat%2B-%2BSheet1.csv) + - [Dependent tags with GL codes]({{site.url}}/assets/Files/Dependent+with+GL+codes+format.csv) + - [Dependent tags without GL codes]({{site.url}}/assets/Files/Dependent+without+GL+codes+format.csv) + - [Independent tags with GL codes]({{site.url}}/assets/Files/Independent+with+GL+codes+format.csv) + - [Independent tags without GL codes]({{site.url}}/assets/Files/Independent+without+GL+codes+format.csv) {% include info.html %} If you have more than 50,000 tags, divide them into two separate files. diff --git a/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md index 397200ce83ef..54cbcfcb52c3 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md @@ -7,11 +7,12 @@ description: Approve, hold, or pay expenses submitted to you As a workspace admin, you can set an approval workflow for the expenses submitted to you. Expenses can be, - Instantly submitted without needing approval. - - Submitted at a desired frequency (daily, weekly, monthly) and follow an approval workflow. **Setting approval workflow and submission frequencies** +Approval workflow settings and submission frequencies can be set in the Workflow settings of your workspace. + # Manually approve expense When someone sends an expense or a group of expenses to you for approval, you’ll receive the expense in Expensify Chat for the related workspace. Chats with new updates appear with a green dot to the right of the chat message. Concierge also sends you an email notification for the new expense. diff --git a/docs/articles/new-expensify/expenses-&-payments/pay-an-invoice.md b/docs/articles/new-expensify/expenses-&-payments/Pay-an-invoice.md similarity index 100% rename from docs/articles/new-expensify/expenses-&-payments/pay-an-invoice.md rename to docs/articles/new-expensify/expenses-&-payments/Pay-an-invoice.md index 45b62c5d7892..f9491892693a 100644 --- a/docs/articles/new-expensify/expenses-&-payments/pay-an-invoice.md +++ b/docs/articles/new-expensify/expenses-&-payments/Pay-an-invoice.md @@ -21,8 +21,6 @@ To pay an invoice, 4. Click **Add Bank Account** or **Add debit or credit card** to issue payment. {% include end-option.html %} -You can also view all unpaid invoices by searching for the sender’s email or phone number on the left-hand side of the app. The invoices waiting for your payment will have a green dot. - {% include option.html value="mobile" %} 1. Tap the link in the email or text notification they receive from Expensify. 2. Tap **Pay**. @@ -32,6 +30,8 @@ You can also view all unpaid invoices by searching for the sender’s email or p {% include end-selector.html %} +You can also view all unpaid invoices by searching for the sender’s email or phone number on the left-hand side of the app. The invoices waiting for your payment will have a green dot. + # FAQ **Can someone else pay an invoice besides the person who received it?** diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 71f6fd1ab0f3..c01a583354d8 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.28 + 9.0.29 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.28.0 + 9.0.29.5 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 854bccb1b90d..e0afb4af6447 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.28 + 9.0.29 CFBundleSignature ???? CFBundleVersion - 9.0.28.0 + 9.0.29.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 00065808afb9..14b7c13c7bd6 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.28 + 9.0.29 CFBundleVersion - 9.0.28.0 + 9.0.29.5 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d688213fafb4..8b34d0e61eba 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1733,7 +1733,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-pager-view (6.3.4): + - react-native-pager-view (6.4.1): - DoubleConversion - glog - hermes-engine @@ -1746,7 +1746,7 @@ PODS: - React-featureflags - React-graphics - React-ImageManager - - react-native-pager-view/common (= 6.3.4) + - react-native-pager-view/common (= 6.4.1) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1755,7 +1755,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-pager-view/common (6.3.4): + - react-native-pager-view/common (6.4.1): - DoubleConversion - glog - hermes-engine @@ -2362,7 +2362,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.117): + - RNLiveMarkdown (0.1.120): - DoubleConversion - glog - hermes-engine @@ -2382,9 +2382,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.117) + - RNLiveMarkdown/common (= 0.1.120) - Yoga - - RNLiveMarkdown/common (0.1.117): + - RNLiveMarkdown/common (0.1.120): - DoubleConversion - glog - hermes-engine @@ -2610,7 +2610,7 @@ PODS: - RNSound/Core (= 0.11.2) - RNSound/Core (0.11.2): - React-Core - - RNSVG (15.4.0): + - RNSVG (15.6.0): - DoubleConversion - glog - hermes-engine @@ -2630,9 +2630,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.4.0) + - RNSVG/common (= 15.6.0) - Yoga - - RNSVG/common (15.4.0): + - RNSVG/common (15.6.0): - DoubleConversion - glog - hermes-engine @@ -3187,7 +3187,7 @@ SPEC CHECKSUMS: react-native-keyboard-controller: 5075321af7b1c834cfb9582230659d032c963278 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: fb5112b1fa754975485884ae85a3fb6a684f49d5 - react-native-pager-view: 6bff9b0883b902571530ddd1b2ea9dc570f321f6 + react-native-pager-view: 94195f1bf32e7f78359fa20057c97e632364a08b react-native-pdf: dd6ae39a93607a80919bef9f3499e840c693989d react-native-performance: 3c608307be10964f8a97d3af462f37125b6d8fa5 react-native-plaid-link-sdk: f91a22b45b7c3d4cd6c47273200dc57df35068b0 @@ -3235,7 +3235,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 919ab2853030c16732ad1d3339b0516264f63783 + RNLiveMarkdown: cfc927fc0b1182e364237c72692e079107c6f5f1 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 5ab6bfd249cd67262615153c648f8d809aab781c RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 @@ -3244,7 +3244,7 @@ SPEC CHECKSUMS: RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 RNShare: a3c2fbbca5682530b65ff405b34c91dad1e22442 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 - RNSVG: 6b65086b51556fd9723d5570a3455e865e1304a3 + RNSVG: 1079f96b39a35753d481a20e30603fd6fc4f6fa9 SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c diff --git a/package-lock.json b/package-lock.json index 3504515bd4ed..792969184480 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "9.0.28-0", + "version": "9.0.29-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.28-0", + "version": "9.0.29-5", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.117", + "@expensify/react-native-live-markdown": "0.1.120", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.12.5", "@formatjs/intl-listformat": "^7.5.7", @@ -104,7 +104,7 @@ "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", "react-native-onyx": "2.0.66", - "react-native-pager-view": "6.3.4", + "react-native-pager-view": "6.4.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.10.0", @@ -119,7 +119,7 @@ "react-native-screens": "3.34.0", "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", - "react-native-svg": "15.4.0", + "react-native-svg": "15.6.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", @@ -133,7 +133,8 @@ "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", "react-window": "^1.8.9", - "semver": "^7.5.2" + "semver": "^7.5.2", + "xlsx": "file:vendor/xlsx-0.20.3.tgz" }, "devDependencies": { "@actions/core": "1.10.0", @@ -3584,9 +3585,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.117", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.117.tgz", - "integrity": "sha512-MMs8U7HRNilTc5PaCODpWL89/+fo61Np1tUBjVaiA4QQw2h5Qta8V5/YexUA4wG29M0N7gcGkxapVhfUoEB0vQ==", + "version": "0.1.120", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.120.tgz", + "integrity": "sha512-MQ8/gPb2u8U1HPClwKhrf2sqjCpi56g5aEhonYOejMPd7kUKpV0nlccSJgy5UEwJFhtxL+cl7SgnXq8xJNwxng==", "workspaces": [ "parser", "example", @@ -8224,8 +8225,9 @@ } }, "node_modules/@react-native-community/cli-server-api/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "peer": true, "engines": { "node": ">=8.3.0" @@ -10010,8 +10012,9 @@ } }, "node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "6.2.2", - "license": "MIT", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "dependencies": { "async-limiter": "~1.0.0" } @@ -13831,23 +13834,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@storybook/core-common/node_modules/glob/node_modules/jackspeak": { - "version": "2.3.6", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/@storybook/core-common/node_modules/has-flag": { "version": "4.0.0", "dev": true, @@ -28383,7 +28369,6 @@ }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^2.0.0" @@ -30682,16 +30667,6 @@ "node": ">= 6" } }, - "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/jest-environment-jsdom/node_modules/jsdom": { "version": "20.0.3", "license": "MIT", @@ -33922,8 +33897,9 @@ } }, "node_modules/metro/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, @@ -38073,8 +38049,9 @@ } }, "node_modules/react-native-macos/node_modules/react-devtools-core/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "peer": true, "engines": { "node": ">=8.3.0" @@ -38144,8 +38121,9 @@ } }, "node_modules/react-native-macos/node_modules/ws": { - "version": "6.2.2", - "license": "MIT", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "peer": true, "dependencies": { "async-limiter": "~1.0.0" @@ -38200,9 +38178,9 @@ } }, "node_modules/react-native-pager-view": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.3.4.tgz", - "integrity": "sha512-4PEQd52EOwWcfiFJZ4VGSlY+GZfumvUzNbAozsMEgJaLvOHOMMl+Arfsc0txgtGdS49uEiHFLLZthZQzxx/Mog==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.4.1.tgz", + "integrity": "sha512-HnDxXTRHnR6WJ/vnOitv0C32KG9MJjxLnxswuQlBJmQ7RxF2GWOHSPIRAdZ9fLxdLstV38z9Oz1C95+t+yXkcg==", "peerDependencies": { "react": "*", "react-native": "*" @@ -38420,8 +38398,9 @@ } }, "node_modules/react-native-svg": { - "version": "15.4.0", - "license": "MIT", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.6.0.tgz", + "integrity": "sha512-TUtR+h+yi1ODsd8FHdom1TpjfWOmnaK5pri5rnSBXnMqpzq8o2zZfonHTjPX+nS3wb/Pu2XsoARgYaHNjVWXhQ==", "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -39223,8 +39202,9 @@ } }, "node_modules/react-native-windows/node_modules/react-devtools-core/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "peer": true, "engines": { "node": ">=8.3.0" @@ -39294,8 +39274,9 @@ } }, "node_modules/react-native-windows/node_modules/ws": { - "version": "6.2.2", - "license": "MIT", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "peer": true, "dependencies": { "async-limiter": "~1.0.0" @@ -39454,8 +39435,9 @@ } }, "node_modules/react-native/node_modules/ws": { - "version": "6.2.2", - "license": "MIT", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "dependencies": { "async-limiter": "~1.0.0" } @@ -44864,9 +44846,10 @@ } }, "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.9", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -45615,8 +45598,9 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "license": "MIT", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -45651,6 +45635,17 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/xlsx": { + "version": "0.20.3", + "resolved": "file:vendor/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-formatter": { "version": "2.6.1", "license": "MIT", diff --git a/package.json b/package.json index 3f86d843ae52..c5dd5b52354a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.28-0", + "version": "9.0.29-5", "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.", @@ -35,7 +35,7 @@ "android-build-e2e": "bundle exec fastlane android build_e2e", "android-build-e2edelta": "bundle exec fastlane android build_e2edelta", "test": "TZ=utc NODE_OPTIONS=--experimental-vm-modules jest", - "typecheck": "tsc", + "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "eslint --fix $(git diff --diff-filter=AM --name-only main -- \"*.js\" \"*.ts\" \"*.tsx\")", "lint-watch": "npx eslint-watch --watch --changed", @@ -70,7 +70,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.117", + "@expensify/react-native-live-markdown": "0.1.120", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.12.5", "@formatjs/intl-listformat": "^7.5.7", @@ -161,7 +161,7 @@ "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", "react-native-onyx": "2.0.66", - "react-native-pager-view": "6.3.4", + "react-native-pager-view": "6.4.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.10.0", @@ -176,7 +176,7 @@ "react-native-screens": "3.34.0", "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", - "react-native-svg": "15.4.0", + "react-native-svg": "15.6.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", @@ -190,7 +190,8 @@ "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", "react-window": "^1.8.9", - "semver": "^7.5.2" + "semver": "^7.5.2", + "xlsx": "file:vendor/xlsx-0.20.3.tgz" }, "devDependencies": { "@actions/core": "1.10.0", diff --git a/patches/@expensify+react-native-live-markdown+0.1.117+001+intial.patch b/patches/@expensify+react-native-live-markdown+0.1.120+001+intial.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.117+001+intial.patch rename to patches/@expensify+react-native-live-markdown+0.1.120+001+intial.patch diff --git a/patches/@expensify+react-native-live-markdown+0.1.117+002+text-layout-manager.patch b/patches/@expensify+react-native-live-markdown+0.1.120+002+text-layout-manager.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.117+002+text-layout-manager.patch rename to patches/@expensify+react-native-live-markdown+0.1.120+002+text-layout-manager.patch diff --git a/patches/@expensify+react-native-live-markdown+0.1.117+003+shadow-node.patch b/patches/@expensify+react-native-live-markdown+0.1.120+003+shadow-node.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.117+003+shadow-node.patch rename to patches/@expensify+react-native-live-markdown+0.1.120+003+shadow-node.patch diff --git a/patches/@expensify+react-native-live-markdown+0.1.117+004+hybrid-app.patch b/patches/@expensify+react-native-live-markdown+0.1.120+004+hybrid-app.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.117+004+hybrid-app.patch rename to patches/@expensify+react-native-live-markdown+0.1.120+004+hybrid-app.patch diff --git a/patches/@react-navigation+core+6.4.11+001+getStateFromPath-getPathFromState-configs-caching.patch b/patches/@react-navigation+core+6.4.11+001+getStateFromPath-getPathFromState-configs-caching.patch new file mode 100644 index 000000000000..7461a576af13 --- /dev/null +++ b/patches/@react-navigation+core+6.4.11+001+getStateFromPath-getPathFromState-configs-caching.patch @@ -0,0 +1,259 @@ +diff --git a/node_modules/@react-navigation/core/src/getPathFromState.tsx b/node_modules/@react-navigation/core/src/getPathFromState.tsx +index 26a6213..bdbb056 100644 +--- a/node_modules/@react-navigation/core/src/getPathFromState.tsx ++++ b/node_modules/@react-navigation/core/src/getPathFromState.tsx +@@ -37,6 +37,11 @@ const getActiveRoute = (state: State): { name: string; params?: object } => { + return route; + }; + ++let cachedNormalizedConfigs: [ ++ PathConfigMap<{}> | undefined, ++ Record, ++] = [undefined, {}]; ++ + /** + * Utility to serialize a navigation state object to a path string. + * +@@ -81,9 +86,13 @@ export default function getPathFromState( + } + + // Create a normalized configs object which will be easier to use +- const configs: Record = options?.screens +- ? createNormalizedConfigs(options?.screens) +- : {}; ++ if (cachedNormalizedConfigs[0] !== options?.screens) { ++ cachedNormalizedConfigs = [ ++ options?.screens, ++ options?.screens ? createNormalizedConfigs(options.screens) : {}, ++ ]; ++ } ++ const configs: Record = cachedNormalizedConfigs[1]; + + let path = '/'; + let current: State | undefined = state; +diff --git a/node_modules/@react-navigation/core/src/getStateFromPath.tsx b/node_modules/@react-navigation/core/src/getStateFromPath.tsx +index b61e1e5..d244bef 100644 +--- a/node_modules/@react-navigation/core/src/getStateFromPath.tsx ++++ b/node_modules/@react-navigation/core/src/getStateFromPath.tsx +@@ -41,6 +41,12 @@ type ParsedRoute = { + params?: Record | undefined; + }; + ++type ConfigResources = { ++ initialRoutes: InitialRouteConfig[]; ++ configs: RouteConfig[]; ++ configWithRegexes: RouteConfig[]; ++}; ++ + /** + * Utility to parse a path string to initial state object accepted by the container. + * This is useful for deep linking when we need to handle the incoming URL. +@@ -66,18 +72,8 @@ export default function getStateFromPath( + path: string, + options?: Options + ): ResultState | undefined { +- if (options) { +- validatePathConfig(options); +- } +- +- let initialRoutes: InitialRouteConfig[] = []; +- +- if (options?.initialRouteName) { +- initialRoutes.push({ +- initialRouteName: options.initialRouteName, +- parentScreens: [], +- }); +- } ++ const { initialRoutes, configs, configWithRegexes } = ++ getConfigResources(options); + + const screens = options?.screens; + +@@ -106,8 +102,111 @@ export default function getStateFromPath( + return undefined; + } + ++ if (remaining === '/') { ++ // We need to add special handling of empty path so navigation to empty path also works ++ // When handling empty path, we should only look at the root level config ++ const match = configs.find( ++ (config) => ++ config.path === '' && ++ config.routeNames.every( ++ // Make sure that none of the parent configs have a non-empty path defined ++ (name) => !configs.find((c) => c.screen === name)?.path ++ ) ++ ); ++ ++ if (match) { ++ return createNestedStateObject( ++ path, ++ match.routeNames.map((name) => ({ name })), ++ initialRoutes, ++ configs ++ ); ++ } ++ ++ return undefined; ++ } ++ ++ let result: PartialState | undefined; ++ let current: PartialState | undefined; ++ ++ // We match the whole path against the regex instead of segments ++ // This makes sure matches such as wildcard will catch any unmatched routes, even if nested ++ const { routes, remainingPath } = matchAgainstConfigs( ++ remaining, ++ configWithRegexes ++ ); ++ ++ if (routes !== undefined) { ++ // This will always be empty if full path matched ++ current = createNestedStateObject(path, routes, initialRoutes, configs); ++ remaining = remainingPath; ++ result = current; ++ } ++ ++ if (current == null || result == null) { ++ return undefined; ++ } ++ ++ return result; ++} ++ ++/** ++ * Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same. ++ */ ++let cachedConfigResources: [Options<{}> | undefined, ConfigResources] = [ ++ undefined, ++ prepareConfigResources(), ++]; ++ ++function getConfigResources( ++ options?: Options ++) { ++ if (cachedConfigResources[0] !== options) { ++ cachedConfigResources = [options, prepareConfigResources(options)]; ++ } ++ ++ return cachedConfigResources[1]; ++} ++ ++function prepareConfigResources(options?: Options<{}>) { ++ if (options) { ++ validatePathConfig(options); ++ } ++ ++ const initialRoutes = getInitialRoutes(options); ++ ++ const configs = getNormalizedConfigs(initialRoutes, options?.screens); ++ ++ checkForDuplicatedConfigs(configs); ++ ++ const configWithRegexes = getConfigsWithRegexes(configs); ++ ++ return { ++ initialRoutes, ++ configs, ++ configWithRegexes, ++ }; ++} ++ ++function getInitialRoutes(options?: Options<{}>) { ++ const initialRoutes: InitialRouteConfig[] = []; ++ ++ if (options?.initialRouteName) { ++ initialRoutes.push({ ++ initialRouteName: options.initialRouteName, ++ parentScreens: [], ++ }); ++ } ++ ++ return initialRoutes; ++} ++ ++function getNormalizedConfigs( ++ initialRoutes: InitialRouteConfig[], ++ screens: PathConfigMap = {} ++) { + // Create a normalized configs array which will be easier to use +- const configs = ([] as RouteConfig[]) ++ return ([] as RouteConfig[]) + .concat( + ...Object.keys(screens).map((key) => + createNormalizedConfigs( +@@ -169,7 +268,9 @@ export default function getStateFromPath( + } + return bParts.length - aParts.length; + }); ++} + ++function checkForDuplicatedConfigs(configs: RouteConfig[]) { + // Check for duplicate patterns in the config + configs.reduce>((acc, config) => { + if (acc[config.pattern]) { +@@ -198,57 +299,14 @@ export default function getStateFromPath( + [config.pattern]: config, + }); + }, {}); ++} + +- if (remaining === '/') { +- // We need to add special handling of empty path so navigation to empty path also works +- // When handling empty path, we should only look at the root level config +- const match = configs.find( +- (config) => +- config.path === '' && +- config.routeNames.every( +- // Make sure that none of the parent configs have a non-empty path defined +- (name) => !configs.find((c) => c.screen === name)?.path +- ) +- ); +- +- if (match) { +- return createNestedStateObject( +- path, +- match.routeNames.map((name) => ({ name })), +- initialRoutes, +- configs +- ); +- } +- +- return undefined; +- } +- +- let result: PartialState | undefined; +- let current: PartialState | undefined; +- +- // We match the whole path against the regex instead of segments +- // This makes sure matches such as wildcard will catch any unmatched routes, even if nested +- const { routes, remainingPath } = matchAgainstConfigs( +- remaining, +- configs.map((c) => ({ +- ...c, +- // Add `$` to the regex to make sure it matches till end of the path and not just beginning +- regex: c.regex ? new RegExp(c.regex.source + '$') : undefined, +- })) +- ); +- +- if (routes !== undefined) { +- // This will always be empty if full path matched +- current = createNestedStateObject(path, routes, initialRoutes, configs); +- remaining = remainingPath; +- result = current; +- } +- +- if (current == null || result == null) { +- return undefined; +- } +- +- return result; ++function getConfigsWithRegexes(configs: RouteConfig[]) { ++ return configs.map((c) => ({ ++ ...c, ++ // Add `$` to the regex to make sure it matches till end of the path and not just beginning ++ regex: c.regex ? new RegExp(c.regex.source + '$') : undefined, ++ })); + } + + const joinPaths = (...paths: string[]): string => diff --git a/patches/react-native+0.75.2+017+redactAppParameters.patch b/patches/react-native+0.75.2+017+redactAppParameters.patch new file mode 100644 index 000000000000..7c4273c6d0c1 --- /dev/null +++ b/patches/react-native+0.75.2+017+redactAppParameters.patch @@ -0,0 +1,40 @@ +diff --git a/node_modules/react-native/Libraries/ReactNative/AppRegistry.js b/node_modules/react-native/Libraries/ReactNative/AppRegistry.js +index 68bd389..be9b5bf 100644 +--- a/node_modules/react-native/Libraries/ReactNative/AppRegistry.js ++++ b/node_modules/react-native/Libraries/ReactNative/AppRegistry.js +@@ -232,12 +232,34 @@ const AppRegistry = { + appParameters: Object, + displayMode?: number, + ): void { ++ const redactAppParameters = (parameters) => { ++ const initialProps = parameters['initialProps']; ++ const url = initialProps['url']; ++ ++ if(!url) { ++ return parameters; ++ } ++ ++ const sensitiveParams = ['authToken', 'autoGeneratedPassword', 'autoGeneratedLogin']; ++ const [urlBase, queryString] = url.split('?'); ++ ++ if (!queryString) { ++ return parameters; ++ } ++ ++ const redactedSearchParams = queryString.split('&').map((param) => { ++ const [key, value] = param.split('='); ++ return `${key}=${sensitiveParams.includes(key) ? '' : value}` ++ }); ++ return {...parameters, initialProps: {...initialProps, url: `${urlBase}?${redactedSearchParams.join('&')}`}}; ++ } ++ + if (appKey !== 'LogBox') { + const msg = + 'Updating props for Surface "' + + appKey + + '" with ' + +- JSON.stringify(appParameters); ++ JSON.stringify(redactAppParameters(appParameters)); + infoLog(msg); + BugReporting.addSource( + 'AppRegistry.setSurfaceProps' + runCount++, diff --git a/patches/react-native-pager-view+6.3.4+001+add-view.patch b/patches/react-native-pager-view+6.3.4+001+add-view.patch deleted file mode 100644 index 9d3b645700f9..000000000000 --- a/patches/react-native-pager-view+6.3.4+001+add-view.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/react-native-pager-view/android/src/fabric/java/com/reactnativepagerview/PagerViewViewManager.kt b/node_modules/react-native-pager-view/android/src/fabric/java/com/reactnativepagerview/PagerViewViewManager.kt -index aa974a6..e9c4a52 100644 ---- a/node_modules/react-native-pager-view/android/src/fabric/java/com/reactnativepagerview/PagerViewViewManager.kt -+++ b/node_modules/react-native-pager-view/android/src/fabric/java/com/reactnativepagerview/PagerViewViewManager.kt -@@ -88,7 +88,7 @@ class PagerViewViewManager : ViewGroupManager(), RNCViewPa - return host - } - -- override fun addView(host: NestedScrollableHost, child: View?, index: Int) { -+ override fun addView(host: NestedScrollableHost, child: View, index: Int) { - PagerViewViewManagerImpl.addView(host, child, index) - } - diff --git a/patches/react-native-pager-view+6.3.4+002+rn-75-fixes.patch b/patches/react-native-pager-view+6.3.4+002+rn-75-fixes.patch deleted file mode 100644 index 694da9da1049..000000000000 --- a/patches/react-native-pager-view+6.3.4+002+rn-75-fixes.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/node_modules/react-native-pager-view/src/PagerView.tsx b/node_modules/react-native-pager-view/src/PagerView.tsx -index 62faa74..e3fb020 100644 ---- a/node_modules/react-native-pager-view/src/PagerView.tsx -+++ b/node_modules/react-native-pager-view/src/PagerView.tsx -@@ -20,17 +20,6 @@ import LEGACY_PagerViewNativeComponent, { - Commands as LEGACY_PagerViewNativeCommands, - } from './specs/LEGACY_PagerViewNativeComponent'; - --// The Fabric component for PagerView uses a work around present also in ScrollView: --// https://github.com/callstack/react-native-pager-view/blob/master/ios/Fabric/RNCPagerViewComponentView.mm#L362-L368 --// That workaround works only if we add these lines in to make sure that the RCTEventEmitter is registered properly --// in the JS callable modules. --// NOTE: This is a workaround as we would like to get rid of these lines below. But for the time being, as the cut date for --// 0.74 approaches, we need to keep these lines. --// As soon as we figure out how to move forward, we will provide guidance and/or submit a PR to fix this. --if (Platform.OS === 'ios') { -- require('react-native/Libraries/Renderer/shims/ReactNative'); // Force side effects to prevent T55744311 --} -- - /** - * Container that allows to flip left and right between child views. Each - * child view of the `PagerView` will be treated as a separate page diff --git a/patches/react-native-pager-view+6.3.4+003+position-absolute.patch b/patches/react-native-pager-view+6.3.4+003+position-absolute.patch deleted file mode 100644 index 466e040f0816..000000000000 --- a/patches/react-native-pager-view+6.3.4+003+position-absolute.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/node_modules/react-native-pager-view/src/utils.tsx b/node_modules/react-native-pager-view/src/utils.tsx -index 0d7d265..2f022a4 100644 ---- a/node_modules/react-native-pager-view/src/utils.tsx -+++ b/node_modules/react-native-pager-view/src/utils.tsx -@@ -1,5 +1,5 @@ - import React, { Children, ReactNode } from 'react'; --import { StyleSheet, View } from 'react-native'; -+import { Platform, StyleSheet, View } from 'react-native'; - - export const LEGACY_childrenWithOverriddenStyle = (children?: ReactNode) => { - return Children.map(children, (child) => { -@@ -29,6 +29,7 @@ export const childrenWithOverriddenStyle = ( - height: '100%', - width: '100%', - paddingHorizontal: pageMargin / 2, -+ position: Platform.OS === 'android' ? 'absolute' : undefined, - }} - collapsable={false} - > diff --git a/patches/react-native-pager-view+6.3.4+004+hybrid-app.patch b/patches/react-native-pager-view+6.3.4+004+hybrid-app.patch deleted file mode 100644 index 4b8f45e87f38..000000000000 --- a/patches/react-native-pager-view+6.3.4+004+hybrid-app.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/react-native-pager-view/ios/Fabric/RNCPagerViewComponentView.mm b/node_modules/react-native-pager-view/ios/Fabric/RNCPagerViewComponentView.mm -index 83fa497..a8f792a 100644 ---- a/node_modules/react-native-pager-view/ios/Fabric/RNCPagerViewComponentView.mm -+++ b/node_modules/react-native-pager-view/ios/Fabric/RNCPagerViewComponentView.mm -@@ -2,7 +2,7 @@ - - #import - #import "RNCPagerViewComponentView.h" --#import -+#import - #import - #import - #import diff --git a/patches/react-native-svg+15.4.0+001+rn75-delegate.patch b/patches/react-native-svg+15.4.0+001+rn75-delegate.patch deleted file mode 100644 index 7b4bd64bef04..000000000000 --- a/patches/react-native-svg+15.4.0+001+rn75-delegate.patch +++ /dev/null @@ -1,45 +0,0 @@ -diff --git a/node_modules/react-native-svg/apple/Elements/RNSVGImage.mm b/node_modules/react-native-svg/apple/Elements/RNSVGImage.mm -index 62b961f..4898760 100644 ---- a/node_modules/react-native-svg/apple/Elements/RNSVGImage.mm -+++ b/node_modules/react-native-svg/apple/Elements/RNSVGImage.mm -@@ -151,6 +151,23 @@ - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(voi - }); - } - -+- (void)didReceiveFailure:(nonnull NSError *)error fromObserver:(nonnull const void *)observer -+{ -+ if (_image) { -+ CGImageRelease(_image); -+ } -+ _image = nil; -+} -+ -+- (void)didReceiveProgress:(float)progress -+ loaded:(int64_t)loaded -+ total:(int64_t)total -+ fromObserver:(nonnull const void *)observer -+{ -+} -+ -+#pragma mark - RCTImageResponseDelegate - < RN 0.75 -+ - - (void)didReceiveProgress:(float)progress fromObserver:(void const *)observer - { - } -@@ -183,6 +200,7 @@ - (void)prepareForRecycle - _imageSize = CGSizeZero; - _reloadImageCancellationBlock = nil; - } -+ - #endif // RCT_NEW_ARCH_ENABLED - - - (void)setSrc:(RCTImageSource *)src -@@ -218,7 +236,7 @@ - (void)setSrc:(RCTImageSource *)src - #if TARGET_OS_OSX // [macOS] - sourceLoaded = [src imageSourceWithSize:image.size scale:1]; - #else -- sourceLoaded = [src imageSourceWithSize:image.size scale:image.scale]; -+ sourceLoaded = [src imageSourceWithSize:image.size scale:image.scale]; - #endif - NSDictionary *dict = @{ - @"uri" : sourceLoaded.request.URL.absoluteString, diff --git a/src/App.tsx b/src/App.tsx index 2480d169980b..cf0fd5528eec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,6 @@ import {VolumeContextProvider} from './components/VideoPlayerContexts/VolumeCont import {CurrentReportIDContextProvider} from './components/withCurrentReportID'; import {EnvironmentProvider} from './components/withEnvironment'; import {KeyboardStateProvider} from './components/withKeyboardState'; -import {WindowDimensionsProvider} from './components/withWindowDimensions'; import CONFIG from './CONFIG'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; @@ -38,6 +37,7 @@ import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; +import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; type AppProps = { /** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ @@ -64,47 +64,48 @@ function App({url}: AppProps) { return ( - - - - - - - - - - - - + + + + + + + + + + + + + + ); } diff --git a/src/CONST.ts b/src/CONST.ts index 92ab86d93f4d..8528ae57c32a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -127,6 +127,9 @@ const CONST = { ALLOWED_RECEIPT_EXTENSIONS: ['jpg', 'jpeg', 'gif', 'png', 'pdf', 'htm', 'html', 'text', 'rtf', 'doc', 'tif', 'tiff', 'msword', 'zip', 'xml', 'message'], }, + // Allowed extensions for spreadsheets import + ALLOWED_SPREADSHEET_EXTENSIONS: ['xls', 'xlsx', 'csv', 'txt'], + // This is limit set on servers, do not update without wider internal discussion API_TRANSACTION_CATEGORY_MAX_LENGTH: 255, @@ -386,6 +389,7 @@ const CONST = { SPOTNANA_TRAVEL: 'spotnanaTravel', REPORT_FIELDS_FEATURE: 'reportFieldsFeature', WORKSPACE_FEEDS: 'workspaceFeeds', + COMPANY_CARD_FEEDS: 'companyCardFeeds', NETSUITE_USA_TAX: 'netsuiteUsaTax', NEW_DOT_COPILOT: 'newDotCopilot', WORKSPACE_RULES: 'workspaceRules', @@ -632,6 +636,8 @@ const CONST = { EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT_FILE_NAME: 'ExpensifyPackageForSageIntacct', SAGE_INTACCT_INSTRUCTIONS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct', HOW_TO_CONNECT_TO_SAGE_INTACCT: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct#how-to-connect-to-sage-intacct', + SAGE_INTACCT_HELP_LINK: + "https://help.expensify.com/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting#:~:text=First%20make%20sure%20that%20you,your%20company's%20Web%20Services%20authorizations.", PRICING: `https://www.expensify.com/pricing`, // Use Environment.getEnvironmentURL to get the complete URL with port number @@ -1520,6 +1526,7 @@ const CONST = { }, CUSTOM_SEGMENT_FIELDS: ['segmentName', 'internalID', 'scriptID', 'mapping'], CUSTOM_LIST_FIELDS: ['listName', 'internalID', 'transactionFieldID', 'mapping'], + CUSTOM_FORM_ID_ENABLED: 'enabled', CUSTOM_FORM_ID_TYPE: { REIMBURSABLE: 'reimbursable', NON_REIMBURSABLE: 'nonReimbursable', @@ -2287,6 +2294,17 @@ const CONST = { VISA: 'vcf', AMEX: 'gl1025', }, + STEP_NAMES: ['1', '2', '3', '4'], + STEP: { + ASSIGNEE: 'Assignee', + CARD: 'Card', + TRANSACTION_START_DATE: 'TransactionStartDate', + CONFIRMATION: 'Confirmation', + }, + TRANSACTION_START_DATE_OPTIONS: { + FROM_BEGINNING: 'fromBeginning', + CUSTOM: 'custom', + }, }, EXPENSIFY_CARD: { BANK: 'Expensify Card', @@ -3799,6 +3817,71 @@ const CONST = { EXPENSIFY_LOGO_SIZE_RATIO: 0.22, EXPENSIFY_LOGO_MARGIN_RATIO: 0.03, }, + + /** + * Acceptable values for the `accessibilityRole` prop on react native components. + * + * **IMPORTANT:** Do not use with the `role` prop as it can cause errors. + * + * @deprecated ACCESSIBILITY_ROLE is deprecated. Please use CONST.ROLE instead. + */ + ACCESSIBILITY_ROLE: { + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + BUTTON: 'button', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + LINK: 'link', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + MENUITEM: 'menuitem', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + TEXT: 'text', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + RADIO: 'radio', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + IMAGEBUTTON: 'imagebutton', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + CHECKBOX: 'checkbox', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + SWITCH: 'switch', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + ADJUSTABLE: 'adjustable', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + IMAGE: 'image', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + TEXTBOX: 'textbox', + }, + /** * Acceptable values for the `role` attribute on react native components. * @@ -3916,6 +3999,7 @@ const CONST = { DROPDOWN_BUTTON_SIZE: { LARGE: 'large', MEDIUM: 'medium', + SMALL: 'small', }, SF_COORDINATES: [-122.4194, 37.7749], @@ -4466,7 +4550,7 @@ const CONST = { REPORT_FIELD_TITLE_FIELD_ID: 'text_title', MOBILE_PAGINATION_SIZE: 15, - WEB_PAGINATION_SIZE: 50, + WEB_PAGINATION_SIZE: 30, /** Dimensions for illustration shown in Confirmation Modal */ CONFIRM_CONTENT_SVG_SIZE: { @@ -5489,6 +5573,38 @@ const CONST = { REMOVE: 'remove', }, }, + + BOOT_SPLASH_STATE: { + VISIBLE: 'visible', + READY_TO_BE_HIDDEN: 'readyToBeHidden', + HIDDEN: `hidden`, + }, + + CSV_IMPORT_COLUMNS: { + EMAIL: 'email', + NAME: 'name', + GL_CODE: 'glCode', + SUBMIT_TO: 'submitTo', + APPROVE_TO: 'approveTo', + CUSTOM_FIELD_1: 'customField1', + CUSTOM_FIELD_2: 'customField2', + ROLE: 'role', + REPORT_THRESHHOLD: 'reportThreshold', + APPROVE_TO_ALTERNATE: 'approveToAlternate', + SUBRATE: 'subRate', + AMOUNT: 'amount', + CURRENCY: 'currency', + RATE_ID: 'rateID', + ENABLED: 'enabled', + IGNORE: 'ignore', + }, + + IMPORT_SPREADSHEET: { + ICON_WIDTH: 180, + ICON_HEIGHT: 160, + + CATEGORIES_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories#import-custom-categories', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 10389f69a44c..62e7839b21f0 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -1,5 +1,5 @@ import {Audio} from 'expo-av'; -import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {NativeEventSubscription} from 'react-native'; import {AppState, Linking, NativeModules, Platform} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -20,8 +20,8 @@ import {updateLastRoute} from './libs/actions/App'; import * as EmojiPickerAction from './libs/actions/EmojiPickerAction'; import * as Report from './libs/actions/Report'; import * as User from './libs/actions/User'; +import {handleHybridAppOnboarding} from './libs/actions/Welcome'; import * as ActiveClientManager from './libs/ActiveClientManager'; -import BootSplash from './libs/BootSplash'; import FS from './libs/Fullstory'; import * as Growl from './libs/Growl'; import Log from './libs/Log'; @@ -42,6 +42,7 @@ import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/Popo import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu'; import type {Route} from './ROUTES'; import ROUTES from './ROUTES'; +import SplashScreenStateContext from './SplashScreenStateContext'; import type {ScreenShareRequest} from './types/onyx'; Onyx.registerLogger(({level, message}) => { @@ -80,13 +81,6 @@ type ExpensifyOnyxProps = { type ExpensifyProps = ExpensifyOnyxProps; -// HybridApp needs access to SetStateAction in order to properly hide SplashScreen when React Native was booted before. -type SplashScreenHiddenContextType = {isSplashHidden?: boolean; setIsSplashHidden: React.Dispatch>}; - -const SplashScreenHiddenContext = React.createContext({ - setIsSplashHidden: () => {}, -}); - function Expensify({ isCheckingPublicRoom = true, updateAvailable, @@ -99,12 +93,13 @@ function Expensify({ const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); - const [isSplashHidden, setIsSplashHidden] = useState(false); + const {splashScreenState, setSplashScreenState} = useContext(SplashScreenStateContext); const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false); const {translate} = useLocalize(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [session] = useOnyx(ONYXKEYS.SESSION); const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE); + const [tryNewDotData] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT); const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false); useEffect(() => { @@ -123,11 +118,21 @@ function Expensify({ setAttemptedToOpenPublicRoom(true); }, [isCheckingPublicRoom]); + useEffect(() => { + if (splashScreenState !== CONST.BOOT_SPLASH_STATE.HIDDEN || tryNewDotData === undefined) { + return; + } + + handleHybridAppOnboarding(); + }, [splashScreenState, tryNewDotData]); + const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]); const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]); const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom; - const shouldHideSplash = shouldInit && !isSplashHidden; + const shouldHideSplash = + shouldInit && + (NativeModules.HybridAppModule ? splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && isAuthenticated : splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE); const initializeClient = () => { if (!Visibility.isVisible()) { @@ -145,17 +150,9 @@ function Expensify({ }, []); const onSplashHide = useCallback(() => { - setIsSplashHidden(true); + setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED); - }, []); - - const contextValue = useMemo( - () => ({ - isSplashHidden, - setIsSplashHidden, - }), - [isSplashHidden, setIsSplashHidden], - ); + }, [setSplashScreenState]); useLayoutEffect(() => { // Initialize this client as being an active client @@ -177,24 +174,22 @@ function Expensify({ useEffect(() => { setTimeout(() => { - BootSplash.getVisibilityStatus().then((status) => { - const appState = AppState.currentState; - Log.info('[BootSplash] splash screen status', false, {appState, status}); - - if (status === 'visible') { - const propsToLog: Omit = { - isCheckingPublicRoom, - updateRequired, - updateAvailable, - isSidebarLoaded, - screenShareRequest, - focusModeNotification, - isAuthenticated, - lastVisitedPath, - }; - Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false); - } - }); + const appState = AppState.currentState; + Log.info('[BootSplash] splash screen status', false, {appState, splashScreenState}); + + if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) { + const propsToLog: Omit = { + isCheckingPublicRoom, + updateRequired, + updateAvailable, + isSidebarLoaded, + screenShareRequest, + focusModeNotification, + isAuthenticated, + lastVisitedPath, + }; + Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false); + } }, 30 * 1000); // This timer is set in the native layer when launching the app and we stop it here so we can measure how long @@ -304,18 +299,15 @@ function Expensify({ {hasAttemptedToOpenPublicRoom && ( - - - + )} - {/* HybridApp has own middleware to hide SplashScreen */} - {!NativeModules.HybridAppModule && shouldHideSplash && } + {shouldHideSplash && } ); } @@ -349,5 +341,3 @@ export default withOnyx({ key: ONYXKEYS.LAST_VISITED_PATH, }, })(Expensify); - -export {SplashScreenHiddenContext}; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 19439ddd1f40..d2a0372fd9c7 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -392,6 +392,9 @@ const ONYXKEYS = { /** Stores the information about the state of issuing a new card */ ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard', + /** Stores the information about the state of assigning a company card */ + ASSIGN_CARD: 'assignCard', + /** Stores the information if mobile selection mode is active */ MOBILE_SELECTION_MODE: 'mobileSelectionMode', @@ -400,6 +403,9 @@ const ONYXKEYS = { /** Stores the information about currently edited advanced approval workflow */ APPROVAL_WORKFLOW: 'approvalWorkflow', + /** Stores information about recently uploaded spreadsheet file */ + IMPORTED_SPREADSHEET: 'importedSpreadsheet', + /** Stores the route to open after changing app permission from settings */ LAST_ROUTE: 'lastRoute', @@ -612,6 +618,8 @@ const ONYXKEYS = { SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', + ASSIGN_CARD_FORM: 'assignCard', + ASSIGN_CARD_FORM_DRAFT: 'assignCardDraft', EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName', EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft', EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit', @@ -709,6 +717,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; + [ONYXKEYS.FORMS.ASSIGN_CARD_FORM]: FormTypes.AssignCardForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; @@ -905,6 +914,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings; [ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates; [ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; + [ONYXKEYS.ASSIGN_CARD]: OnyxTypes.AssignCard; [ONYXKEYS.MOBILE_SELECTION_MODE]: OnyxTypes.MobileSelectionMode; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; @@ -914,6 +924,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; + [ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet; [ONYXKEYS.LAST_ROUTE]: string; }; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 17451e5a4d99..89023063ad8f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -35,7 +35,7 @@ const ROUTES = { SEARCH_CENTRAL_PANE: { route: 'search', - getRoute: ({query, isCustomQuery = false}: {query: SearchQueryString; isCustomQuery?: boolean}) => `search?q=${query}&isCustomQuery=${isCustomQuery}` as const, + getRoute: ({query}: {query: SearchQueryString}) => `search?q=${query}` as const, }, SEARCH_ADVANCED_FILTERS: 'search/filters', SEARCH_ADVANCED_FILTERS_DATE: 'search/filters/date', @@ -744,6 +744,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const, }, + WORKSPACE_CATEGORIES_IMPORT: { + route: 'settings/workspaces/:policyID/categories/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/import` as const, + }, + WORKSPACE_CATEGORIES_IMPORTED: { + route: 'settings/workspaces/:policyID/categories/imported', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/imported` as const, + }, WORKSPACE_CATEGORY_CREATE: { route: 'settings/workspaces/:policyID/categories/new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const, @@ -902,6 +910,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/company-cards', getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards` as const, }, + WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: { + route: 'settings/workspaces/:policyID/company-cards/:feed/assign-card', + getRoute: (policyID: string, feed: string) => `settings/workspaces/${policyID}/company-cards/${feed}/assign-card` as const, + }, WORKSPACE_EXPENSIFY_CARD_DETAILS: { route: 'settings/workspaces/:policyID/expensify-card/:cardID', getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a072307c133b..db790dd389c3 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -368,6 +368,7 @@ const SCREENS = { RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate', RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit', COMPANY_CARDS: 'Workspace_CompanyCards', + COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard', COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed', COMPANY_CARDS_SETTINGS: 'Workspace_CompanyCards_Settings', COMPANY_CARDS_SETTINGS_FEED_NAME: 'Workspace_CompanyCards_Settings_Feed_Name', @@ -435,6 +436,8 @@ const SCREENS = { CATEGORY_GL_CODE: 'Category_GL_Code', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', + CATEGORIES_IMPORT: 'Categories_Import', + CATEGORIES_IMPORTED: 'Categories_Imported', MORE_FEATURES: 'Workspace_More_Features', MEMBER_DETAILS: 'Workspace_Member_Details', OWNER_CHANGE_CHECK: 'Workspace_Owner_Change_Check', diff --git a/src/SplashScreenStateContext.tsx b/src/SplashScreenStateContext.tsx new file mode 100644 index 000000000000..90a858f70c42 --- /dev/null +++ b/src/SplashScreenStateContext.tsx @@ -0,0 +1,34 @@ +import React, {useContext, useMemo, useState} from 'react'; +import type {ValueOf} from 'type-fest'; +import CONST from './CONST'; +import type ChildrenProps from './types/utils/ChildrenProps'; + +type SplashScreenStateContextType = { + splashScreenState: ValueOf; + setSplashScreenState: React.Dispatch>>; +}; + +const SplashScreenStateContext = React.createContext({ + splashScreenState: CONST.BOOT_SPLASH_STATE.VISIBLE, + setSplashScreenState: () => {}, +}); + +function SplashScreenStateContextProvider({children}: ChildrenProps) { + const [splashScreenState, setSplashScreenState] = useState>(CONST.BOOT_SPLASH_STATE.VISIBLE); + const splashScreenStateContext = useMemo( + () => ({ + splashScreenState, + setSplashScreenState, + }), + [splashScreenState], + ); + + return {children}; +} + +function useSplashScreenStateContext() { + return useContext(SplashScreenStateContext); +} + +export default SplashScreenStateContext; +export {SplashScreenStateContextProvider, useSplashScreenStateContext}; diff --git a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx index dc27f115dc05..f7c3ca4e2b5e 100644 --- a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx +++ b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx @@ -6,8 +6,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -29,7 +29,7 @@ type PaymentCardCurrencyModalProps = { }; function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.PAYMENT_CARD_CURRENCY.USD, onCurrencyChange, onClose}: PaymentCardCurrencyModalProps) { - const {isSmallScreenWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const {translate} = useLocalize(); const {sections} = useMemo( @@ -55,7 +55,7 @@ function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONS onClose={() => onClose?.()} onModalHide={onClose} hideModalContentWhileAnimating - innerContainerStyle={styles.RHPNavigatorContainer(isSmallScreenWidth)} + innerContainerStyle={styles.RHPNavigatorContainer(shouldUseNarrowLayout)} onBackdropPress={Navigation.dismissModal} useNativeDriver > diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx index 615f039cf533..fd28595e7436 100644 --- a/src/components/ApprovalWorkflowSection.tsx +++ b/src/components/ApprovalWorkflowSection.tsx @@ -1,9 +1,9 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; import Icon from './Icon'; @@ -24,7 +24,7 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSe const styles = useThemeStyles(); const theme = useTheme(); const {translate, toLocaleOrdinal} = useLocalize(); - const {isSmallScreenWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const approverTitle = useCallback( (index: number) => @@ -45,7 +45,7 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSe return ( diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index 26d117697888..c4979f544080 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -1,5 +1,6 @@ import React, {useRef} from 'react'; import type {ValueOf} from 'type-fest'; +import * as Browser from '@libs/Browser'; import Visibility from '@libs/Visibility'; import CONST from '@src/CONST'; import type AttachmentPickerProps from './types'; @@ -8,7 +9,7 @@ import type AttachmentPickerProps from './types'; * Returns acceptable FileTypes based on ATTACHMENT_PICKER_TYPE */ function getAcceptableFileTypes(type: string): string | undefined { - if (type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE) { + if (type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE || Browser.isMobileChrome()) { return; } diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index f7ef2c6529ce..72e0f17aa310 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -12,6 +12,7 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import * as Illustrations from '@components/Icon/Illustrations'; import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -40,7 +41,8 @@ const MIN_FLING_VELOCITY = 500; function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); - const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); + const {windowWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const {isFullScreenRef} = useFullScreenContext(); const scrollRef = useAnimatedRef>>(); @@ -49,7 +51,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true); + const modalStyles = styles.centeredModalStyles(shouldUseNarrowLayout, true); const cellWidth = useMemo( () => PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2), [modalStyles.borderWidth, modalStyles.marginHorizontal, windowWidth], diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 41a01fa27c46..3d1b91dce4b5 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -1,5 +1,6 @@ import React, {useEffect} from 'react'; import useKeyboardState from '@hooks/useKeyboardState'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -53,7 +54,8 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu const isSuggestionMenuAboveRef = React.useRef(false); const leftValue = React.useRef(0); const prevLeftValue = React.useRef(0); - const {windowHeight, windowWidth, isSmallScreenWidth} = useWindowDimensions(); + const {windowHeight, windowWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [suggestionHeight, setSuggestionHeight] = React.useState(0); const [containerState, setContainerState] = React.useState(initialContainerState); const StyleUtils = useStyleUtils(); @@ -96,12 +98,12 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu const contentMaxHeight = measureHeightOfSuggestionRows(suggestionsLength, true); const contentMinHeight = measureHeightOfSuggestionRows(suggestionsLength, false); let bottomValue = windowHeight - (cursorCoordinates.y - scrollValue + y) - keyboardHeight; - const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; + const widthValue = shouldUseNarrowLayout ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; const isEnoughSpaceToRenderMenuAboveForBig = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMaxHeight, topInset}); const isEnoughSpaceToRenderMenuAboveForSmall = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMinHeight, topInset}); - const newLeftOffset = isSmallScreenWidth ? x : bigScreenLeftOffset; + const newLeftOffset = shouldUseNarrowLayout ? x : bigScreenLeftOffset; // If the suggested word is longer than 150 (approximately half the width of the suggestion popup), then adjust a new position of popup const isAdjustmentNeeded = Math.abs(prevLeftValue.current - bigScreenLeftOffset) > 150; if (isInitialRender.current || isAdjustmentNeeded) { @@ -131,7 +133,7 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu cursorCoordinates, }); }); - }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset, topInset]); + }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, shouldUseNarrowLayout, suggestionsLength, bottomInset, topInset]); if ((containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) || (containerState.cursorCoordinates.x === 0 && containerState.cursorCoordinates.y === 0)) { return null; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index c6d93f93ee23..698591d22bfd 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -137,13 +137,13 @@ type ButtonProps = Partial & { type KeyboardShortcutComponentProps = Pick; -const accessibilityRoles: string[] = Object.values(CONST.ROLE); +const accessibilityRoles: string[] = Object.values(CONST.ACCESSIBILITY_ROLE); function KeyboardShortcutComponent({isDisabled = false, isLoading = false, onPress = () => {}, pressOnEnter, allowBubble, enterKeyEventListenerPriority}: KeyboardShortcutComponentProps) { const isFocused = useIsFocused(); const activeElementRole = useActiveElementRole(); - const shouldDisableEnterShortcut = useMemo(() => accessibilityRoles.includes(activeElementRole ?? '') && activeElementRole !== CONST.ROLE.PRESENTATION, [activeElementRole]); + const shouldDisableEnterShortcut = useMemo(() => accessibilityRoles.includes(activeElementRole ?? '') && activeElementRole !== CONST.ACCESSIBILITY_ROLE.TEXT, [activeElementRole]); const keyboardShortcutCallback = useCallback( (event?: GestureResponderEvent | KeyboardEvent) => { @@ -289,8 +289,9 @@ function Button( ) : ( ({ enterKeyEventListenerPriority = 0, wrapperStyle, useKeyboardShortcuts = false, + defaultSelectedIndex = 0, + shouldShowSelectedItemCheck = false, }: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const [selectedItemIndex, setSelectedItemIndex] = useState(0); + const [selectedItemIndex, setSelectedItemIndex] = useState(defaultSelectedIndex); const [isMenuVisible, setIsMenuVisible] = useState(false); const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); @@ -64,7 +66,7 @@ function ButtonWithDropdownMenu({ if ('measureInWindow' in dropdownAnchor.current) { dropdownAnchor.current.measureInWindow((x, y, w, h) => { setPopoverAnchorPosition({ - horizontal: x + w, + horizontal: x + w + h, vertical: anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP ? y + h + CONST.MODAL.POPOVER_MENU_PADDING // if vertical anchorAlignment is TOP, menu will open below the button and we need to add the height of button and padding @@ -93,11 +95,12 @@ function ButtonWithDropdownMenu({ isActive: useKeyboardShortcuts, }, ); + const splitButtonWrapperStyle = isSplitButton ? [styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter] : {}; return ( {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( - +