diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js
index ae82e38070e7..1f17c73f5813 100644
--- a/.eslintrc.changed.js
+++ b/.eslintrc.changed.js
@@ -21,7 +21,7 @@ module.exports = {
},
overrides: [
{
- files: ['src/pages/workspace/WorkspaceInitialPage.tsx', 'src/pages/home/report/PureReportActionItem.tsx', 'src/libs/SidebarUtils.ts'],
+ files: ['src/pages/workspace/WorkspaceInitialPage.tsx', 'src/libs/SidebarUtils.ts'],
rules: {
'rulesdir/no-default-id-values': 'off',
},
diff --git a/.eslintrc.js b/.eslintrc.js
index 650692c54eb7..aa98b7bdc464 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -87,6 +87,10 @@ const restrictedImportPaths = [
importNames: ['memoize'],
message: "Please use '@src/libs/memoize' instead.",
},
+ {
+ name: 'react-native-animatable',
+ message: "Please use 'react-native-reanimated' instead.",
+ },
];
const restrictedImportPatterns = [
diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml
index 086a2a383d28..cfa3f9fc191e 100644
--- a/.github/actions/composite/setupNode/action.yml
+++ b/.github/actions/composite/setupNode/action.yml
@@ -31,8 +31,8 @@ runs:
uses: actions/cache@v4
with:
path: node_modules
- key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json', 'patches/**') }}
-
+ key: ${{ inputs.IS_HYBRID_BUILD == 'true' && format('{0}-node-modules-{1}', runner.os, hashFiles('package-lock.json', 'patches/**', 'Mobile-Expensify/patches/**')) || format('{0}-node-modules-{1}', runner.os, hashFiles('package-lock.json', 'patches/**'))}}
+
- id: cache-old-dot-node-modules
if: inputs.IS_HYBRID_BUILD == 'true'
uses: actions/cache@v4
diff --git a/Mobile-Expensify b/Mobile-Expensify
index 33f71a1b24e1..10beb38304d3 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 33f71a1b24e15527a8207281344ed9fa57203c69
+Subproject commit 10beb38304d35ee86b948f64afcdf49d51ab3d4a
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 23de9fc95273..bc4558882674 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 1009008902
- versionName "9.0.89-2"
+ versionCode 1009008906
+ versionName "9.0.89-6"
// 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/arrows-leftright.svg b/assets/images/arrows-leftright.svg
new file mode 100644
index 000000000000..53c75d411734
--- /dev/null
+++ b/assets/images/arrows-leftright.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/assets/images/chatbubble-slash.svg b/assets/images/chatbubble-slash.svg
new file mode 100644
index 000000000000..09d2b5bd3149
--- /dev/null
+++ b/assets/images/chatbubble-slash.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts
index 1992a27c40c3..d50fa927fa95 100644
--- a/config/webpack/webpack.common.ts
+++ b/config/webpack/webpack.common.ts
@@ -179,7 +179,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment):
// We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later.
// This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server.
{
- test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs'),
+ test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'),
type: 'asset/source',
},
diff --git a/docs/articles/expensify-classic/connect-credit-cards/Reconcile-Company-Card-Expenses.md b/docs/articles/expensify-classic/connect-credit-cards/Reconcile-Company-Card-Expenses.md
index 7243bdd5f470..c812071166c3 100644
--- a/docs/articles/expensify-classic/connect-credit-cards/Reconcile-Company-Card-Expenses.md
+++ b/docs/articles/expensify-classic/connect-credit-cards/Reconcile-Company-Card-Expenses.md
@@ -1,74 +1,92 @@
---
title: Reconcile Company Card Expenses
-description: How to reconcile company card transactions
+description: Learn how to reconcile company card expenses in Expensify, including troubleshooting discrepancies, managing approvals, and preparing accruals
---
-If your company imports corporate card transactions into Expensify, you can reconcile them by using the Reconciliation dashboard.
+This guide explains how to reconcile corporate card transactions imported into Expensify using the reconciliation dashboard feature.
-1. Hover over **Settings** and click **Domains**.
+# Steps to Reconcile Transactions
+
+## Access the Reconciliation Dashboard
+1. Hover over **Settings** and click **Domains**.
2. Select the desired domain.
3. Click the **Reconciliation** tab near the top of the page.
-4. Enter the statement dates and click **Run**.
-
-# Confirm statement total
-
-To confirm the total of transactions imported into Expensify against a credit card statement:
-1. Review the **Imported Total**, which shows the sum of all expenses imported into Expensify for that statement period. This should match the total on your credit card statement.
-2. If there is a discrepancy, refresh the feed to import missing expenses. Click **Update all cards** for commercial card feeds, or update individual cards by clicking the blue cog icon and choosing **Update** for other feed types.
-3. After updating, click **Run** to update the transaction totals.
-
-# Confirm card totals
-
-If there is a discrepancy between the totals on the credit card statement and the Reconciliation dashboard, then review each card’s total to find the source of the missing transactions.
-
-1. Sort the cards by clicking the heading for **Card Name/Number**, **Assignee** or **Total** and compare each card's total to the statement to determine which card(s) don't match the statement total.
-2. Click on the **Total** amount for a card to view the imported expenses and identify any that are missing from the statement. Confirm that all cards have been assigned to cardholders, as this could be another reason that the Imported Total doesn't match the statement.
-3. If there is still a discrepancy after updating and re-calculating the totals, contact concierge@expensify.com and provide the details of the expenses that are showing on your statement but are missing in Expensify. To investigate, we’ll need the cardholder email, expense date, and amount. Keep in mind sorting by column heading also makes locating expenses easier.
-
-# Identify outstanding unapproved expenses
-
-Use the **Unapproved total** and **Approved Total** to identify expenses that have not yet been approved and/or exported.
+4. Enter the statement dates and click **Run**.
-# View expenses
+## Confirm Statement Total
+To verify the total transactions imported into Expensify match your credit card statement:
-- Click the **Unapproved Total** heading to sort cards by those with outstanding expenses.
-- Click the **Unapproved** amount for a card to view the expenses which are in the Unreported, Open, Processing, or Deleted states.
+1. Review the **Imported Total**, which shows the sum of all imported expenses for the selected statement period.
+ - This total should match the amount on your credit card statement.
+2. If there’s a discrepancy:
+ - Refresh the feed to import missing expenses:
+ - Click **Update All Cards** for commercial card feeds.
+ - For other feeds, click the blue cog icon next to individual cards and select **Update**.
+ - After updating, click **Run** to recalculate the totals.
-*Note: You must be both a Domain Admin and a Workspace Admin to access expenses.*
+## Confirm Card Totals
+If the totals on the credit card statement and the Reconciliation dashboard still don’t match, follow these steps:
-# Add unreported and/or deleted expenses to a report
+1. Sort the cards by clicking the column heading for **Card Name/Number**, **Assignee**, or **Total**.
+2. Compare each card’s total to the credit card statement to find discrepancies.
+3. Click the **Total** amount for a card to view its imported expenses. Check for:
+ - Missing transactions.
+ - Unassigned cards (all cards must be assigned to cardholders).
+4. If discrepancies persist, contact **concierge@expensify.com** with details of the missing expenses:
+ - Cardholder email
+ - Expense date
+ - Expense amount
-1. Change the filters so that only Unreported and/or Deleted expenses are showing.
-2. Select all expenses, then click **Add to a Report,** then **Auto Report**.
-3. If there is an open report in the cardholder's account, the expense(s) will be added to it. If not, a new report will be created with the expenses.
+## Identify Outstanding, Unapproved Expenses
+Use the **Unapproved Total** and **Approved Total** columns to locate expenses that haven’t been approved or exported:
-# Process reports
+1. Click the **Unapproved Total** heading to sort cards by those with outstanding expenses.
+2. Click the **Unapproved** amount for a card to view expenses in the Unreported, Open, Processing, or Deleted states.
-- Workspace admins have the ability to code (categorize or tag an expense or add a receipt or comment to it) unsubmitted expenses, submit Open reports, and approve Processing reports. Any changes made by an admin are tracked under Report History and Comments at the bottom of each report.
-- You can remind members to submit and approve reports via the Report History and Comments. An email notification will be sent to all members who have taken action on the report.
+**Note: You must be both a Domain Admin and Workspace Admin to access expenses.**
-# Prepare accrual
+## Add Unreported or Deleted Expenses to a Report
+1. Filter the expenses to display only Unreported or Deleted expenses.
+2. Select all relevant expenses and click **Add to a Report** > **Auto Report**.
+3. If an open report exists in the cardholder’s account, the expenses will be added to it. Otherwise, a new report will be created.
-If there are still unapproved expenses when you want to close your books for the month, then you can use the feed’s Imported, Approved, and Unapproved totals to create an accrual entry.
-- Match the Imported Total to the Statement amount.
-- Match the Approved Total to the Company Card Liability account in your accounting system.
-- The Unapproved Total becomes the Accrual amount (if the two amounts above are correct).
-
-{% include faq-begin.md %}
-
-**Who can view and access the Reconciliation tab?**
-
-Only Domain admins have access to the Reconciliation tool.
-
-**Who can view and process company card transactions?**
-
-- Domain admins can view all company card transactions using the Reconciliation tool, even if they are unreported.
-- Workspace admins can only view reported expenses on a workspace. So if a workspace admin does not have access to the domain, they will be unable to see any transaction that hasn’t been placed on a report.
+---
+# Process and Edit Reports
-**What do I do if company card expenses are missing?**
+Workspace Admins can do the following via the Reconciliation Dashboard:
+ - Code (categorize or tag expenses, add receipts or comments) expenses.
+ - Submit Open reports.
+ - Approve Processing reports.
+- All changes made by admins are tracked in the **Report History and Comments** section at the bottom of each report.
+- You can remind members to submit or approve reports via Report History, which sends email notifications to users.
-If a cardholder reports expenses as missing, we first recommend using the Reconciliation tool to try and locate the expense. Select the date range the expense falls under, and once the report is available, select the specific card to view the data. If the expense is not listed, you will want to click **Update** next to the card under the Card List tab. This will pull in any missing expenses.
+---
+# Prepare Accrual
-If after updating, the expense still hasn’t appeared, you should reach out to Concierge with the missing expense specifics (merchant, date, amount and last four digits of the card number). Please note, only posted transactions will import.
+To close your books for the month with unapproved expenses:
+1. Match the **Imported Total** to the statement amount.
+2. Match the **Approved Total** to the Company Card Liability account in your accounting system.
+3. Use the **Unapproved Total** as the accrual amount if the above totals are correct.
-{% include faq-end.md %}
+---
+# FAQ
+
+## Who can access the Reconciliation tab?
+Only Domain Admins can access the Reconciliation tool.
+
+## Who can view and process company card transactions?
+- **Domain Admins** can view all company card transactions, including unreported ones, via the Reconciliation tool.
+- **Workspace Admins** can only view reported expenses in a workspace. If they lack domain access, they cannot see transactions that haven’t been added to a report.
+
+## What do I do if company card expenses are missing?
+1. Use the Reconciliation tool to locate the missing expense:
+ - Select the date range for the expense.
+ - View the specific card to check the data.
+2. If the expense isn’t listed, click **Update** next to the card under the Card List tab to pull in missing transactions.
+3. If the expense still doesn’t appear, contact Concierge with these details:
+ - Merchant name
+ - Date
+ - Amount
+ - Last four digits of the card number
+
+**Note: Only posted transactions will be imported.**
diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
deleted file mode 100644
index 1272cbd1f117..000000000000
--- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-title: Request the Card
-description: Details on requesting the Expensify Card as an employee
----
-_Note: The Expensify Card is currently only available to companies that have:_
-_- A US Bank Account_
-_- US documentation_
-_- A private email domain i.e. we cannot provision Expensify cards for users with gmail.com, hotmail.com, yahoo.com etc_
-
-To start using the Expensify Card, do the following:
-1. **Enable Expensify Cards:** An admin must first enable the cards. Then, an admin can assign you a card by setting a limit, which allows access to the card.
-2. **Request the Card:**
- - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!”
- - Completing that task will send an in-product notification to your admin team that you requested the card.
- - Once you’re assigned a card limit, you’ll receive an email notification. Click the link in the email to provide your shipping address on your account’s homepage.
- - Enter your address, and the physical card will be shipped within 3-5 business days.
-3. **Activate the Card:** When your physical card arrives, activate it in Expensify by entering the last four digits of the card in the activation task on your homepage.
-
-### Virtual Cards
-Once you've been assigned a limit, a virtual card is available immediately. You can view its details via _**Settings > Account > Credit Card Import > Show Details**_.
-
-To protect your account and card spend, enable two-factor authentication under _**Settings > Account > Account Details**_.
-
-### Notifications
-- Download the Expensify mobile app and enable push notifications to stay updated on your card’s limit and spending.
-- Each transaction triggers a push notification.
-- You’ll also get notifications for potentially fraudulent activity, allowing you to confirm or dispute charges.
-
-## Request a Replacement Expensify Card
-### If the card is lost, stolen, or damaged Card:
- - Go to _**Settings > Account > Credit Card Import** and click **Request a New Card**_.
- - Confirm your shipping information and complete the prompts. The new card will arrive in 2-3 business days.
- - Selecting “lost” or “stolen” deactivates your current card to prevent fraud. Choosing “damaged” keeps the current card active until the new one arrives.
- - If you can’t access the website or app, call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally) to cancel your card.
-
-### If the card is expiring
-- If your card is about to expire, Expensify will notify you via your account’s Home (Inbox) tab.
-- Enter your address if it has changed; otherwise, do nothing, and the new card will ship to your address on file.
-- The new card will have a unique number and will not be linked to the old one.
-
-{% include faq-begin.md %}
-
-## What if I haven’t received my card after multiple weeks?
-
-Reach out to support, and we can locate a tracking number for the card. If the card shows as delivered, but you still haven’t received it, you’ll need to confirm your address and order a new one.
-
-## I’m self-employed. Can I set up the Expensify Card as an individual?
-
-Yep! As long as you have a business bank account and have registered your company with the IRS, you are eligible to use the Expensify Card as an individual business owner.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Expensify-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Expensify-Card.md
new file mode 100644
index 000000000000..52ffec9f716e
--- /dev/null
+++ b/docs/articles/expensify-classic/expensify-card/Request-the-Expensify-Card.md
@@ -0,0 +1,88 @@
+---
+title: Request the Expensify Card
+description: Learn how to request, activate, and manage the Expensify Card, including virtual card setup, replacement procedures, and eligibility requirements.
+---
+
+This guide provides details on how you and your employees can request and use the Expensify Card.
+
+# Requirements for the Expensify Card
+
+The Expensify Card is currently available only to companies that meet the following criteria:
+- **US Bank Account**
+- **US Documentation**
+- **Private Email Domain**: We cannot provision Expensify Cards for users with public domains like gmail.com, hotmail.com, yahoo.com, etc.
+
+---
+# Steps to Request the Expensify Card
+
+## 1. Enable Expensify Cards (Admin Action)
+- An admin must first enable the cards.
+- The admin will assign you a card by setting a spending limit and granting access to the card.
+
+## 2. Request the Card
+If a card limit hasn’t been assigned to you, look for the task on your account homepage that says: **"Ask your admin for the card!"**
+- Completing this task sends an in-product notification to your admin team requesting the card.
+- Once assigned a card limit, you’ll receive an email notification. Follow these steps:
+ 1. Click the link in the email.
+ 2. Provide your shipping address on your account homepage.
+ 3. Submit the address to have your physical card shipped within **3-5 business days**.
+
+## 3. Activate the Card
+When the physical card arrives, activate it by:
+ - Entering the last four digits of the card in the activation task on your account homepage.
+
+---
+# Virtual Cards
+
+- Virtual cards are available immediately once a spending limit is assigned.
+- To view your virtual card details, go to:
+ **Settings > Account > Credit Card Import > Show Details**.
+
+## Security Tip
+[Enable two-factor authentication](https://help.expensify.com/articles/expensify-classic/settings/Enable-two-factor-authentication) to secure your account and card spend:
+1. Navigate to **Settings > Account > Account Details**
+2. Under the Account Details tab, find the Two-Factor Authentication section, and switch on the toggle
+
+---
+# Notifications
+Download the Expensify mobile app and enable push notifications to stay updated on:
+ - Card spending limits
+ - Transactions
+ - Potentially fraudulent activity
+
+---
+# Request a Replacement Expensify Card
+
+## Lost, Stolen, or Damaged Cards
+1. Go to **Settings > Account > Credit Card Import** and click **Request a New Card**.
+2. Confirm your shipping information and complete the prompts.
+3. Replacement Timeline:
+ - **Lost or Stolen**: The current card is deactivated immediately to prevent fraud.
+ - **Damaged**: The current card remains active until the replacement arrives.
+4. A new card will arrive within **2-3 business days**.
+
+**Alternative: Contact Support**
+If you can’t access the website or app:
+- Call **1-877-751-5848** (US) or **+44 808 196 0632** (International) to cancel the card.
+
+## Expiring Cards
+- Expensify notifies you via the **Home (Inbox)** tab when your card is nearing expiration.
+- If your address has changed, update it to receive the new card.
+- Otherwise, the card will ship automatically to your address on file.
+
+**Important**: The new card will have a unique number and won’t be linked to the old one.
+
+---
+# FAQ
+
+## What if I haven’t received my card after multiple weeks?
+- Reach out to support for a tracking number.
+
+- If the card is marked as delivered but not received:
+ - Confirm your address.
+ - Order a replacement card.
+
+## I’m self-employed. Can I set up the Expensify Card as an individual?
+Yes! If you have a business bank account and IRS registration for your company, you can use the Expensify Card as an individual business owner.
+
+---
diff --git a/docs/articles/expensify-classic/settings/Change-or-add-email-address.md b/docs/articles/expensify-classic/settings/Change-or-add-email-address.md
deleted file mode 100644
index f6fe3d8e13b4..000000000000
--- a/docs/articles/expensify-classic/settings/Change-or-add-email-address.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-title: Change or add email address
-description: Update your Expensify email address or add a secondary email
----
-
-
-The primary email address on your Expensify account is the email that receives email updates and notifications for your account. You can add a secondary email address in order to
-- Change your primary email to a new one.
-- Connect your personal email address as a secondary login if your primary email address is one from your employer. This allows you to always have access to your Expensify account, even if your employer changes.
-
-{% include info.html %}
-Before you can remove a primary email address, you must add a new one to your Expensify account and make it the primary using the steps below. Email addresses must be added as a secondary login before they can be made the primary.
-{% include end-info.html %}
-
-# Adding a new Secondary Login
-*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.*
-
-1. Hover over Settings, then click **Account**.
-2. Under the Account Details > Secondary Logins > click **Add Secondary Login**.
-3. Enter the email address or phone number you wish to use as a secondary login. For phone numbers, be sure to include the international code, if applicable.
-4. Find the email or text message from Expensify containing the Magic Code and enter it into the field.
-
-# Changing your Primary Login
-If you already have multiple email addresses linked to your account, you can change which one is listed as the Primary Login.
-
-1. Settings > Account > Secondary Logins.
-2. Click **Make Primary** next to the email address you want to appear on your account.
-
-You can keep both logins, or you can click **Remove** next to the old email address to delete it from your account.
-
-# Unlinking an email from your old account
-If you at one point added your personal email address as a Secondary Login to your account, and then the account was closed - for example if you had a company account and then left the company - you may want to unlink your personal email to use it with a new Expensify account. You can do this with the following steps:
-
-1. Navigate to the sign in page at expensify.com.
-2. Enter your personal email address into the email field.
-3. Click **Unlink Accounts**.
-4. You will recieve a verification email to complete the unlinking of your personal address.
-
-# FAQ
-**What does changing the primary login do?**
-When you change your primary login this will update the email address that appears on your reports (old and new), in workspace account settings, and on your account.
-
-**Can I have multiple Seconary Logins?**
-Yes, you can have an unlimited number of logins attached to your account.
-
diff --git a/docs/articles/expensify-classic/settings/Managing-Primary-and-Secondary-Logins-in-Expensify.md b/docs/articles/expensify-classic/settings/Managing-Primary-and-Secondary-Logins-in-Expensify.md
new file mode 100644
index 000000000000..788d0ff94eb9
--- /dev/null
+++ b/docs/articles/expensify-classic/settings/Managing-Primary-and-Secondary-Logins-in-Expensify.md
@@ -0,0 +1,61 @@
+---
+title: Managing Primary and Secondary Logins in Expensify
+description: Learn how to update or add an email address to your Expensify Classic account with this step-by-step guide.
+---
+
+
+Your **primary email address** on Expensify is used for receiving all notifications and updates. Adding a **secondary email address** enables you to:
+- Change your primary email to a new email.
+- Connect a personal email as a secondary login when your primary email is employer-provided. This ensures continued account access if your employment changes.
+- Log in to your Expensify account using either your primary or secondary email address.
+- SmartScan receipts by sending them to receipts@expensify.com from your secondary login.
+
+**Important:** Before removing your primary email, add and make another email the primary. Emails must be added as a secondary login first.
+
+---
+
+# Adding a Secondary Login
+
+⚠️ **This process is only available on the Expensify website, not the mobile app.**
+
+1. Go to **Settings** > **Account**.
+2. Under **Account Details**, find **Secondary Logins**, and click **Add Secondary Login**.
+3. Enter the email address or phone number you want to use.
+ - For phone numbers, include the international code if applicable.
+4. Check your email or text messages for a verification Magic Code and enter it in the required field.
+
+---
+
+# Changing Your Primary Login
+
+If you have multiple email addresses linked to your account, follow these steps to change your primary login:
+
+1. Navigate to **Settings** > **Account** > **Secondary Logins**.
+2. Click **Make Primary** next to the desired email address.
+3. *(Optional)* To remove the old email address, click **Remove** next to it.
+
+---
+
+# Unlinking an Email from a Closed Account
+
+If you previously added your personal email as a Secondary Login on a company account and the account has been closed, you can unlink your email to use it with a new Expensify account:
+
+1. Go to the **Expensify Sign-In** page.
+2. Enter your personal email address.
+3. Click **Unlink Accounts**.
+4. Follow the steps in the verification email to complete the unlinking process.
+
+---
+
+# FAQ
+
+## What does changing the primary login do?
+Changing your primary login updates:
+- The email address displayed on reports (old and new).
+- Workspace account settings.
+- Your account’s default email.
+
+## Can I have multiple secondary logins?
+Yes, you can add an unlimited number of secondary logins to your account.
+
+
diff --git a/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md b/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md
deleted file mode 100644
index 782e939e991e..000000000000
--- a/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-title: Upgrade to the new Expensify Card from Visa
-description: Get the new Expensify Visa® Commercial Card
----
-
-
-When you upgrade the Expensify Cards to the new program, you'll have access to even more tools to manage employee spending, including:
-- Unlimited [virtual cards](https://use.expensify.com/unlimited-virtual-cards)
-- Controlled spending amounts on virtual cards to manage subscriptions
-- Tighter controls for managing spend across employees and merchants
-- Fixed or monthly spending limits for each card
-- Unique naming for each virtual card for simplified expense categorization
-
-{% include info.html %}
-The Expensify Card upgrade must be completed by December 1, 2024.
-{% include end-info.html %}
-
-# Upgrade your company’s Expensify Card program
-This process must be completed by a Domain Admin. Any domain Admin can complete the upgrade, but only one admin needs to complete these steps.
-
-**Before updating the card program:**
-- Make sure your employees' address is up-to-date in their Expensify account
-- Confirm the employees who should be receiving a new Expensify Card have a card limit set that's greater than $0
-
-## Steps to upgrade the Expensify Cards
-1. On your Home page, click the task titled _Upgrade to the new and improved Expensify Card._
-2. Review and agree to the Terms of Service.
-3. Click **Get the new card**. All existing cardholders with a limit greater than $0 will be automatically mailed a new physical card to the address they have on file. Virtual cards will be automatically issued and available for immediate use.
-4. If you have Positive Pay enabled for your settlement account, contact your bank as soon as possible to whitelist the new ACH ID: 2270239450.
-5. Remind your employees to update their payment information for recurring charges to their virtual card information.
-
-New cards will have the same limit as the existing cards. Each cardholder’s current physical and virtual cards will remain active until a Domain Admin or the cardholder deactivates it.
-
-{% include info.html %}
-Cards won’t be issued to any employees who don’t currently have them. In this case, you’ll need to [issue a new card](https://help.expensify.com/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa%C2%AE-Commercial-Card-for-your-Company)
-{% include end-info.html %}
-
-{% include faq-begin.md %}
-
-## Why don’t I see the task to agree to new terms on my Home page?
-
-There are a few reasons why you might not see the task on your Home page:
-- You may not be a Domain Admin
-- Another domain admin has already accepted the terms
-- The task may be hidden. To find hidden tasks, scroll to the bottom of the Home page and click **Show Hidden Tasks** to see all of your available tasks.
-
-## Will this affect the continuous reconciliation process?
-
-No. During the transition period, you may have some employees with old cards and some with new cards, so you’ll have two different debits (settlements) made to your settlement account for each settlement period. Once all spending has transitioned to the new cards, you’ll only see one debit/settlement.
-
-## Do I have to upgrade to the new Expensify Visa® Commercial Card?
-
-Yes, the Expensify Cards will not work on the old program. This must be completed by November 1, 2024.
-{% include faq-end.md %}
-
diff --git a/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md b/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md
index 237aad83169a..d7d9c656a612 100644
--- a/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md
+++ b/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md
@@ -158,8 +158,16 @@ Flight preferences include multiple sections with different settings:
# FAQ
-How do travel policy rules interact with Expensify’s [approval flows](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses)?
+## How do travel policy rules interact with Expensify’s [approval flows](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses)?
Travel policy rules define what can and can’t be booked by your employees while they’re making the booking. Once a booking is placed and the travel itself is [approved](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses), the expense will appear in Expensify. It will then be coded, submitted, pushed through the existing expense approval process as defined by your workspace, and exported to your preferred accounting platform (if applicable).
+## Why are some policies not selectable?
+
+If the travel policy you want to configure is greyed out, it might be linked to a parent policy. To unlink it from the default policy:
+
+1. Look for the “link” icon next to the dropdown menu.
+2. Click the icon to unlink the policy from the parent policy.
+3. Once unlinked, you’ll be able to make your selection.
+
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 40e8b8d0ca61..81dc9a9a3129 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -61,7 +61,7 @@ https://community.expensify.com/discussion/4343/expensify-anz-partnership-announ
https://community.expensify.com/discussion/7318/deep-dive-company-credit-card-import-options,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards
https://community.expensify.com/discussion/2673/personalize-your-commercial-card-feed-name,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds
https://community.expensify.com/discussion/6569/how-to-import-and-assign-company-cards-from-csv-file,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import
-https://community.expensify.com/discussion/4714/how-to-set-up-a-direct-bank-connection-for-company-cards,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections
+https://community.expensify.com/discussion/4714/how-to-set-up-a--connection-for-company-cards,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections
https://community.expensify.com/discussion/5366/deep-dive-troubleshooting-credit-card-issues-in-expensify,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting
https://community.expensify.com/discussion/9554/how-to-set-up-global-reimbursemen,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursements
https://community.expensify.com/discussion/4463/how-to-remove-or-manage-settings-for-imported-personal-cards,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards
@@ -612,11 +612,14 @@ https://help.expensify.com/articles/expensify-classic/expenses/Apply-Tax,https:/
https://help.expensify.com/articles/expensify-classic/workspaces/Tax-Tracking,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes
https://help.expensify.com/articles/new-expensify/travel/manage-travel-member-roles,https://help.expensify.com/articles/new-expensify/travel/Manage-Travel-Member-Roles
https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Personal-Credit-Cards
-https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/
https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards/
https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards/
https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings/
https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Connect-ANZ,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards/
https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Reconcile-Company-Card-Expenses/
https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards/
+https://help.expensify.com/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa,https://help.expensify.com/new-expensify/hubs/expensify-card/
https://help.expensify.com/articles/new-expensify/expensify-card/Dispute-Expensify-Card-transaction,https://help.expensify.com/articles/new-expensify/expensify-card/Disputing-Expensify-Card-Transactions
+https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the-Card,https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the-Expensify-Card
+https://help.expensify.com/articles/expensify-classic/settings/Change-or-add-email-address,https://help.expensify.com/articles/expensify-classic/settings/Managing-Primary-and-Secondary-Logins-in-Expensify
+https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index b740755740ed..184498800897 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.0.89.2
+ 9.0.89.6FullStoryOrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 4bf6d22ef364..1c17b3df0e3d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature????CFBundleVersion
- 9.0.89.2
+ 9.0.89.6
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 5af1c9a4914b..99c7550ee5f2 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString9.0.89CFBundleVersion
- 9.0.89.2
+ 9.0.89.6NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 82299434a3b1..184c7101b6b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.89-2",
+ "version": "9.0.89-6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.89-2",
+ "version": "9.0.89-6",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 393f670098fa..43c38ed9b904 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.89-2",
+ "version": "9.0.89-6",
"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.",
diff --git a/src/CONST.ts b/src/CONST.ts
index ac4b9562672d..4ddde915155e 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -328,6 +328,9 @@ const CONST = {
ANIMATED_HIGHLIGHT_END_DURATION: 2000,
ANIMATED_TRANSITION: 300,
ANIMATED_TRANSITION_FROM_VALUE: 100,
+ ANIMATED_PROGRESS_BAR_DELAY: 300,
+ ANIMATED_PROGRESS_BAR_OPACITY_DURATION: 300,
+ ANIMATED_PROGRESS_BAR_DURATION: 750,
ANIMATION_IN_TIMING: 100,
ANIMATION_DIRECTION: {
IN: 'in',
@@ -446,6 +449,9 @@ const CONST = {
MAX_LENGTH: 83,
},
+ REVERSED_TRANSACTION_ATTRIBUTE: 'is-reversed-transaction',
+ HIDDEN_MESSAGE_ATTRIBUTE: 'is-hidden-message',
+
CALENDAR_PICKER: {
// Numbers were arbitrarily picked.
MIN_YEAR: CURRENT_YEAR - 100,
@@ -904,7 +910,6 @@ const CONST = {
CLOUDFRONT_URL,
EMPTY_ARRAY,
EMPTY_OBJECT,
- EMPTY_STRING: '',
DEFAULT_NUMBER_ID: 0,
USE_EXPENSIFY_URL,
EXPENSIFY_URL,
@@ -1707,6 +1712,10 @@ const CONST = {
CONCIERGE: 'concierge',
OTHER: 'other',
WEB_PROP_ATTR: 'data-testid',
+ SHUTDOWN: 'shutdown',
+ RESTART: 'restart',
+ SET_IDENTITY: 'setIdentity',
+ OBSERVE: 'observe',
},
CONCIERGE_DISPLAY_NAME: 'Concierge',
@@ -2393,6 +2402,7 @@ const CONST = {
TRACK: 'track',
},
AMOUNT_MAX_LENGTH: 8,
+ DISTANCE_REQUEST_AMOUNT_MAX_LENGTH: 14,
RECEIPT_STATE: {
SCANREADY: 'SCANREADY',
OPEN: 'OPEN',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 4a9bbe12203d..7750e795e9c7 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -418,9 +418,6 @@ const ONYXKEYS = {
/** Stores the last export method for policy */
LAST_EXPORT_METHOD: 'lastExportMethod',
- /** Stores the information about the state of issuing a new card */
- ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard',
-
/** Stores the information about the state of addint a new company card */
ADD_NEW_COMPANY_CARD: 'addNewCompanyCard',
@@ -556,6 +553,9 @@ const ONYXKEYS = {
/** Whether the bank account chosen for Expensify Card in on verification waitlist */
NVP_EXPENSIFY_ON_CARD_WAITLIST: 'nvp_expensify_onCardWaitlist_',
+
+ /** Stores the information about the state of issuing a new card */
+ ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_',
},
/** List of Form ids */
@@ -897,6 +897,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean;
[ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeed;
[ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist;
+ [ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
};
type OnyxValuesMapping = {
@@ -1036,7 +1037,6 @@ type OnyxValuesMapping = {
[ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING]: boolean;
[ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings;
[ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates;
- [ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
[ONYXKEYS.ADD_NEW_COMPANY_CARD]: OnyxTypes.AddNewCompanyCardFeed;
[ONYXKEYS.ASSIGN_CARD]: OnyxTypes.AssignCard;
[ONYXKEYS.MOBILE_SELECTION_MODE]: OnyxTypes.MobileSelectionMode;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 33c94e343568..aa6e54f82dd3 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -322,8 +322,12 @@ const ROUTES = {
},
EDIT_REPORT_FIELD_REQUEST: {
route: 'r/:reportID/edit/policyField/:policyID/:fieldID',
- getRoute: (reportID: string, policyID: string, fieldID: string, backTo?: string) =>
- getUrlWithBackToParam(`r/${reportID}/edit/policyField/${policyID}/${encodeURIComponent(fieldID)}` as const, backTo),
+ getRoute: (reportID: string | undefined, policyID: string | undefined, fieldID: string, backTo?: string) => {
+ if (!policyID || !reportID) {
+ Log.warn('Invalid policyID or reportID is used to build the EDIT_REPORT_FIELD_REQUEST route', {policyID, reportID});
+ }
+ return getUrlWithBackToParam(`r/${reportID}/edit/policyField/${policyID}/${encodeURIComponent(fieldID)}` as const, backTo);
+ },
},
REPORT_WITH_ID_DETAILS_SHARE_CODE: {
route: 'r/:reportID/details/shareCode',
@@ -400,11 +404,21 @@ const ROUTES = {
},
SPLIT_BILL_DETAILS: {
route: 'r/:reportID/split/:reportActionID',
- getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/split/${reportActionID}` as const, backTo),
+ getRoute: (reportID: string | undefined, reportActionID: string, backTo?: string) => {
+ if (!reportID) {
+ Log.warn('Invalid reportID is used to build the SPLIT_BILL_DETAILS route');
+ }
+ return getUrlWithBackToParam(`r/${reportID}/split/${reportActionID}` as const, backTo);
+ },
},
TASK_TITLE: {
route: 'r/:reportID/title',
- getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/title` as const, backTo),
+ getRoute: (reportID: string | undefined, backTo?: string) => {
+ if (!reportID) {
+ Log.warn('Invalid reportID is used to build the TASK_TITLE route');
+ }
+ return getUrlWithBackToParam(`r/${reportID}/title` as const, backTo);
+ },
},
REPORT_DESCRIPTION: {
route: 'r/:reportID/description',
@@ -417,7 +431,12 @@ const ROUTES = {
},
TASK_ASSIGNEE: {
route: 'r/:reportID/assignee',
- getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/assignee` as const, backTo),
+ getRoute: (reportID: string | undefined, backTo?: string) => {
+ if (!reportID) {
+ Log.warn('Invalid reportID is used to build the TASK_ASSIGNEE route');
+ }
+ return getUrlWithBackToParam(`r/${reportID}/assignee` as const, backTo);
+ },
},
PRIVATE_NOTES_LIST: {
route: 'r/:reportID/notes',
@@ -534,7 +553,12 @@ const ROUTES = {
},
SETTINGS_TAGS_ROOT: {
route: 'settings/:policyID/tags',
- getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags`, backTo),
+ getRoute: (policyID: string | undefined, backTo = '') => {
+ if (!policyID) {
+ Log.warn('Invalid policyID while building route SETTINGS_TAGS_ROOT');
+ }
+ return getUrlWithBackToParam(`settings/${policyID}/tags`, backTo);
+ },
},
SETTINGS_TAGS_SETTINGS: {
route: 'settings/:policyID/tags/settings',
@@ -669,8 +693,11 @@ const ROUTES = {
},
MONEY_REQUEST_STEP_TAG: {
route: ':action/:iouType/tag/:orderWeight/:transactionID/:reportID/:reportActionID?',
- getRoute: (action: IOUAction, iouType: IOUType, orderWeight: number, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
- getUrlWithBackToParam(`${action as string}/${iouType as string}/tag/${orderWeight}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, orderWeight: number, transactionID: string, reportID?: string, backTo = '', reportActionID?: string) =>
+ getUrlWithBackToParam(
+ `${action as string}/${iouType as string}/tag/${orderWeight}/${transactionID}${reportID ? `/${reportID}` : ''}${reportActionID ? `/${reportActionID}` : ''}`,
+ backTo,
+ ),
},
MONEY_REQUEST_STEP_WAYPOINT: {
route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
@@ -1283,6 +1310,15 @@ const ROUTES = {
return `settings/workspaces/${policyID}/company-cards` as const;
},
},
+ WORKSPACE_COMPANY_CARDS_BANK_CONNECTION: {
+ route: 'settings/workspaces/:policyID/company-cards/:bankName/bank-connection',
+ getRoute: (policyID: string | undefined, bankName: string, backTo: string) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_COMPANY_CARDS_BANK_CONNECTION route');
+ }
+ return getUrlWithBackToParam(`settings/workspaces/${policyID}/company-cards/${bankName}/bank-connection`, backTo);
+ },
+ },
WORKSPACE_COMPANY_CARDS_ADD_NEW: {
route: 'settings/workspaces/:policyID/company-cards/add-card-feed',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards/add-card-feed` as const,
@@ -1310,7 +1346,7 @@ const ROUTES = {
},
WORKSPACE_EXPENSIFY_CARD: {
route: 'settings/workspaces/:policyID/expensify-card',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const,
+ getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/expensify-card` as const,
},
WORKSPACE_EXPENSIFY_CARD_DETAILS: {
route: 'settings/workspaces/:policyID/expensify-card/:cardID',
@@ -1909,7 +1945,7 @@ const ROUTES = {
},
DEBUG_REPORT: {
route: 'debug/report/:reportID',
- getRoute: (reportID: string) => `debug/report/${reportID}` as const,
+ getRoute: (reportID: string | undefined) => `debug/report/${reportID}` as const,
},
DEBUG_REPORT_TAB_DETAILS: {
route: 'debug/report/:reportID/details',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 3d85cd907f2a..76456485a3a4 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -456,6 +456,7 @@ const SCREENS = {
COMPANY_CARDS: 'Workspace_CompanyCards',
COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard',
COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed',
+ COMPANY_CARDS_BANK_CONNECTION: 'Workspace_CompanyCards_BankConnection',
COMPANY_CARDS_ADD_NEW: 'Workspace_CompanyCards_New',
COMPANY_CARDS_TYPE: 'Workspace_CompanyCards_Type',
COMPANY_CARDS_INSTRUCTIONS: 'Workspace_CompanyCards_Instructions',
diff --git a/src/components/AnimatedStep/index.tsx b/src/components/AnimatedStep/index.tsx
index 6de7d0c2b013..e9f99b6fc17c 100644
--- a/src/components/AnimatedStep/index.tsx
+++ b/src/components/AnimatedStep/index.tsx
@@ -1,8 +1,8 @@
import React, {useMemo} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
+// eslint-disable-next-line no-restricted-imports -- will be removed in the future PR
import * as Animatable from 'react-native-animatable';
import useThemeStyles from '@hooks/useThemeStyles';
-import useNativeDriver from '@libs/useNativeDriver';
import CONST from '@src/CONST';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type {AnimationDirection} from './AnimatedStepContext';
@@ -37,8 +37,6 @@ function AnimatedStep({onAnimationEnd, direction = CONST.ANIMATION_DIRECTION.IN,
}}
duration={CONST.ANIMATED_TRANSITION}
animation={animationStyle}
- // eslint-disable-next-line react-compiler/react-compiler
- useNativeDriver={useNativeDriver}
style={style}
>
{children}
diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx
index d831fca562c3..c998c38e96ca 100644
--- a/src/components/DotIndicatorMessage.tsx
+++ b/src/components/DotIndicatorMessage.tsx
@@ -7,8 +7,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {isReceiptError} from '@libs/ErrorUtils';
import fileDownload from '@libs/fileDownload';
-import * as Localize from '@libs/Localize';
-import CONST from '@src/CONST';
+import {translateLocal} from '@libs/Localize';
import type {ReceiptError} from '@src/types/onyx/Transaction';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
@@ -61,38 +60,17 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica
key={index}
style={styles.offlineFeedback.text}
>
- {Localize.translateLocal('iou.error.receiptFailureMessage')}
+ {translateLocal('iou.error.receiptFailureMessage')} {
fileDownload(message.source, message.filename);
}}
>
- {Localize.translateLocal('iou.error.saveFileMessage')}
+ {translateLocal('iou.error.saveFileMessage')}
- {Localize.translateLocal('iou.error.loseFileMessage')}
-
- );
- }
-
- if (message === CONST.COMPANY_CARDS.CONNECTION_ERROR) {
- return (
-
- {Localize.translateLocal('workspace.companyCards.brokenConnectionErrorFirstPart')}
- {
- // TODO: re-navigate the user to the bank’s website to re-authenticate https://github.com/Expensify/App/issues/50448
- }}
- >
- {Localize.translateLocal('workspace.companyCards.brokenConnectionErrorLink')}
-
-
- {Localize.translateLocal('workspace.companyCards.brokenConnectionErrorSecondPart')}
+ {translateLocal('iou.error.loseFileMessage')}
);
}
diff --git a/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts b/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts
index 3189eebf2f04..a1e21f07a8ee 100644
--- a/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts
+++ b/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts
@@ -6,6 +6,7 @@ type FocusTrapForModalProps = {
children: React.ReactNode;
active: boolean;
initialFocus?: FocusTrapOptions['initialFocus'];
+ shouldPreventScroll?: boolean;
};
export default FocusTrapForModalProps;
diff --git a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
index c47b7086bbd8..1be3f06224f2 100644
--- a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
+++ b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
@@ -5,12 +5,13 @@ import blurActiveElement from '@libs/Accessibility/blurActiveElement';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import type FocusTrapForModalProps from './FocusTrapForModalProps';
-function FocusTrapForModal({children, active, initialFocus = false}: FocusTrapForModalProps) {
+function FocusTrapForModal({children, active, initialFocus = false, shouldPreventScroll = false}: FocusTrapForModalProps) {
return (
) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const htmlAttribs = tnode.attributes;
+
+ const reversedTransactionValue = htmlAttribs[CONST.REVERSED_TRANSACTION_ATTRIBUTE];
+ const hiddenMessageValue = htmlAttribs[CONST.HIDDEN_MESSAGE_ATTRIBUTE];
+
+ const getIcon = () => {
+ if (reversedTransactionValue === 'true') {
+ return Expensicons.ArrowsLeftRight;
+ }
+ if (hiddenMessageValue === 'true') {
+ return Expensicons.EyeDisabled;
+ }
+ return Expensicons.Trashcan;
+ };
+
+ return (
+
+
+ {
+ const firstChild = props?.childTnode?.children?.at(0);
+ const data = firstChild && 'data' in firstChild ? firstChild.data : null;
+
+ if (typeof data === 'string') {
+ return {data};
+ }
+ return props.childElement;
+ }}
+ />
+
+ );
+}
+
+DeletedActionRenderer.displayName = 'DeletedActionRenderer';
+
+export default DeletedActionRenderer;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx
index e44d3ef97df6..ddaab1a55994 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext.tsx
@@ -1,12 +1,12 @@
import {createContext} from 'react';
type MentionReportContextProps = {
- currentReportID: string;
+ currentReportID: string | undefined;
exactlyMatch?: boolean;
};
const MentionReportContext = createContext({
- currentReportID: '',
+ currentReportID: undefined,
});
export default MentionReportContext;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
index 89a9fb21d48f..fcae31dd7d2f 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
@@ -60,9 +60,9 @@ function MentionReportRenderer({style, tnode, TDefaultRenderer, ...defaultRender
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const currentReportID = useCurrentReportID();
- const currentReportIDValue = currentReportIDContext || currentReportID?.currentReportID;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const [currentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentReportIDValue || -1}`);
+ const currentReportIDValue = currentReportIDContext || currentReportID?.currentReportID;
+ const [currentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentReportIDValue}`);
// When we invite someone to a room they don't have the policy object, but we still want them to be able to see and click on report mentions, so we only check if the policyID in the report is from a workspace
const isGroupPolicyReport = useMemo(() => currentReport && !isEmptyObject(currentReport) && !!currentReport.policyID && currentReport.policyID !== CONST.POLICY.ID_FAKE, [currentReport]);
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
index ce24584048b0..91ed66f8b931 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
@@ -1,6 +1,7 @@
import type {CustomTagRendererRecord} from 'react-native-render-html';
import AnchorRenderer from './AnchorRenderer';
import CodeRenderer from './CodeRenderer';
+import DeletedActionRenderer from './DeletedActionRenderer';
import EditedRenderer from './EditedRenderer';
import EmojiRenderer from './EmojiRenderer';
import ImageRenderer from './ImageRenderer';
@@ -30,6 +31,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = {
'mention-here': MentionHereRenderer,
emoji: EmojiRenderer,
'next-step-email': NextStepEmailRenderer,
+ 'deleted-action': DeletedActionRenderer,
/* eslint-enable @typescript-eslint/naming-convention */
};
diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx
index eda42a703d65..3ec7bb8f25d4 100644
--- a/src/components/Hoverable/ActiveHoverable.tsx
+++ b/src/components/Hoverable/ActiveHoverable.tsx
@@ -15,35 +15,35 @@ type OnMouseEvents = Record void>;
function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreezeCapture, children}: ActiveHoverableProps, outerRef: Ref) {
const [isHovered, setIsHovered] = useState(false);
-
const elementRef = useRef(null);
const isScrollingRef = useRef(false);
const isHoveredRef = useRef(false);
- const isVisibiltyHidden = useRef(false);
+ const isVisibilityHidden = useRef(false);
const updateIsHovered = useCallback(
(hovered: boolean) => {
+ if (shouldFreezeCapture) {
+ return;
+ }
+
isHoveredRef.current = hovered;
- // Nullish coalescing operator (`??`) wouldn't be appropriate here because
- // it's not a matter of providing a default when encountering `null` or `undefined`
- // but rather making a decision based on the truthy nature of the complete expressions.
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- if ((shouldHandleScroll && isScrollingRef.current) || shouldFreezeCapture) {
+ isVisibilityHidden.current = false;
+
+ if (shouldHandleScroll && isScrollingRef.current) {
return;
}
+
setIsHovered(hovered);
+
+ if (hovered) {
+ onHoverIn?.();
+ } else {
+ onHoverOut?.();
+ }
},
- [shouldHandleScroll, shouldFreezeCapture],
+ [shouldHandleScroll, shouldFreezeCapture, onHoverIn, onHoverOut],
);
- useEffect(() => {
- if (isHovered) {
- onHoverIn?.();
- } else {
- onHoverOut?.();
- }
- }, [isHovered, onHoverIn, onHoverOut]);
-
useEffect(() => {
if (!shouldHandleScroll) {
return;
@@ -51,105 +51,65 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez
const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling: boolean) => {
isScrollingRef.current = scrolling;
- if (!isScrollingRef.current) {
- setIsHovered(isHoveredRef.current);
+ if (scrolling && isHovered) {
+ setIsHovered(false);
+ onHoverOut?.();
+ } else if (!scrolling && elementRef.current?.matches(':hover')) {
+ setIsHovered(true);
+ onHoverIn?.();
}
});
return () => scrollingListener.remove();
- }, [shouldHandleScroll]);
+ }, [shouldHandleScroll, isHovered, onHoverIn, onHoverOut]);
useEffect(() => {
- // Do not mount a listener if the component is not hovered
- if (!isHovered) {
- return;
- }
-
- /**
- * Checks the hover state of a component and updates it based on the event target.
- * This is necessary to handle cases where the hover state might get stuck due to an unreliable mouseleave trigger,
- * such as when an element is removed before the mouseleave event is triggered.
- * @param event The hover event object.
- */
- const unsetHoveredIfOutside = (event: MouseEvent) => {
- // We're also returning early if shouldFreezeCapture is true in order
- // to not update the hover state but keep it frozen.
- if (!elementRef.current || elementRef.current.contains(event.target as Node) || shouldFreezeCapture) {
- return;
+ const handleVisibilityChange = () => {
+ if (document.visibilityState === 'hidden') {
+ isVisibilityHidden.current = true;
+ setIsHovered(false);
+ } else {
+ isVisibilityHidden.current = false;
}
-
- setIsHovered(false);
};
- document.addEventListener('mouseover', unsetHoveredIfOutside, true);
-
- return () => document.removeEventListener('mouseover', unsetHoveredIfOutside);
- }, [isHovered, elementRef, shouldFreezeCapture]);
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+ return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
+ }, []);
- useEffect(() => {
- const unsetHoveredWhenDocumentIsHidden = () => {
- if (document.visibilityState !== 'hidden') {
+ const handleMouseEvents = useCallback(
+ (type: 'enter' | 'leave' | 'blur') => () => {
+ if (shouldFreezeCapture) {
return;
}
- isVisibiltyHidden.current = true;
- setIsHovered(false);
- };
-
- document.addEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden);
+ const newHoverState = type === 'enter';
+ isHoveredRef.current = newHoverState;
+ isVisibilityHidden.current = false;
- return () => document.removeEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden);
- }, []);
+ updateIsHovered(newHoverState);
+ },
+ [shouldFreezeCapture, updateIsHovered],
+ );
- const child = useMemo(() => getReturnValue(children, !isScrollingRef.current && isHovered), [children, isHovered]);
+ const child = useMemo(() => getReturnValue(children, isHovered), [children, isHovered]);
- const {onMouseEnter, onMouseLeave, onMouseMove, onBlur} = child.props as OnMouseEvents;
+ const {onMouseEnter, onMouseLeave, onBlur} = child.props as OnMouseEvents;
- const hoverAndForwardOnMouseEnter = useCallback(
- (e: MouseEvent) => {
- isVisibiltyHidden.current = false;
- updateIsHovered(true);
+ return cloneElement(child, {
+ ref: mergeRefs(elementRef, outerRef, child.ref),
+ onMouseEnter: (e: MouseEvent) => {
+ handleMouseEvents('enter')();
onMouseEnter?.(e);
},
- [updateIsHovered, onMouseEnter],
- );
-
- const unhoverAndForwardOnMouseLeave = useCallback(
- (e: MouseEvent) => {
- updateIsHovered(false);
+ onMouseLeave: (e: MouseEvent) => {
+ handleMouseEvents('leave')();
onMouseLeave?.(e);
},
- [updateIsHovered, onMouseLeave],
- );
-
- const unhoverAndForwardOnBlur = useCallback(
- (event: MouseEvent) => {
- // Check if the blur event occurred due to clicking outside the element
- // and the wrapperView contains the element that caused the blur and reset isHovered
- if (!elementRef.current?.contains(event.target as Node) && !elementRef.current?.contains(event.relatedTarget as Node) && !shouldFreezeCapture) {
- setIsHovered(false);
- }
-
- onBlur?.(event);
- },
- [onBlur, shouldFreezeCapture],
- );
-
- const handleAndForwardOnMouseMove = useCallback(
- (e: MouseEvent) => {
- isVisibiltyHidden.current = false;
- updateIsHovered(true);
- onMouseMove?.(e);
+ onBlur: (e: MouseEvent) => {
+ handleMouseEvents('blur')();
+ onBlur?.(e);
},
- [updateIsHovered, onMouseMove],
- );
-
- return cloneElement(child, {
- ref: mergeRefs(elementRef, outerRef, child.ref),
- onMouseEnter: hoverAndForwardOnMouseEnter,
- onMouseLeave: unhoverAndForwardOnMouseLeave,
- onBlur: unhoverAndForwardOnBlur,
- ...(isVisibiltyHidden.current ? {onMouseMove: handleAndForwardOnMouseMove} : {}),
});
}
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 02a6843dc11f..e4072504f3d6 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -7,6 +7,7 @@ import ArrowRightLong from '@assets/images/arrow-right-long.svg';
import ArrowRight from '@assets/images/arrow-right.svg';
import ArrowUpLong from '@assets/images/arrow-up-long.svg';
import UpArrow from '@assets/images/arrow-up.svg';
+import ArrowsLeftRight from '@assets/images/arrows-leftright.svg';
import ArrowsUpDown from '@assets/images/arrows-updown.svg';
import AttachmentNotFound from '@assets/images/attachment-not-found.svg';
import AdminRoomAvatar from '@assets/images/avatars/admin-room.svg';
@@ -43,6 +44,7 @@ import Cash from '@assets/images/cash.svg';
import Chair from '@assets/images/chair.svg';
import ChatBubbleAdd from '@assets/images/chatbubble-add.svg';
import ChatBubbleReply from '@assets/images/chatbubble-reply.svg';
+import ChatBubbleSlash from '@assets/images/chatbubble-slash.svg';
import ChatBubbleUnread from '@assets/images/chatbubble-unread.svg';
import ChatBubble from '@assets/images/chatbubble.svg';
import ChatBubbles from '@assets/images/chatbubbles.svg';
@@ -220,6 +222,7 @@ export {
ArrowRight,
ArrowRightLong,
ArrowsUpDown,
+ ArrowsLeftRight,
ArrowUpLong,
ArrowDownLong,
AttachmentNotFound,
@@ -390,6 +393,7 @@ export {
Linkedin,
Instagram,
ChatBubbleAdd,
+ ChatBubbleSlash,
ChatBubbleUnread,
ChatBubbleReply,
Lightbulb,
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 3bf8e11d4ad6..604e2b3065fd 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -55,7 +55,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
const [isScreenFocused, setIsScreenFocused] = useState(false);
const {shouldUseNarrowLayout} = useResponsiveLayout();
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID}`);
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
diff --git a/src/components/LoadingBar.tsx b/src/components/LoadingBar.tsx
new file mode 100644
index 000000000000..e6d1ec0cd66d
--- /dev/null
+++ b/src/components/LoadingBar.tsx
@@ -0,0 +1,83 @@
+import React, {useEffect} from 'react';
+import Animated, {cancelAnimation, Easing, useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming} from 'react-native-reanimated';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+type LoadingBarProps = {
+ // Whether or not to show the loading bar
+ shouldShow: boolean;
+};
+
+function LoadingBar({shouldShow}: LoadingBarProps) {
+ const left = useSharedValue(0);
+ const width = useSharedValue(0);
+ const opacity = useSharedValue(0);
+ const styles = useThemeStyles();
+
+ useEffect(() => {
+ if (shouldShow) {
+ // eslint-disable-next-line react-compiler/react-compiler
+ left.set(0);
+ width.set(0);
+ opacity.set(withTiming(1, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}));
+
+ left.set(
+ withDelay(
+ CONST.ANIMATED_PROGRESS_BAR_DELAY,
+ withRepeat(
+ withSequence(
+ withTiming(0, {duration: 0}),
+ withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}),
+ withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}),
+ ),
+ -1,
+ false,
+ ),
+ ),
+ );
+
+ width.set(
+ withDelay(
+ CONST.ANIMATED_PROGRESS_BAR_DELAY,
+ withRepeat(
+ withSequence(
+ withTiming(0, {duration: 0}),
+ withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}),
+ withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}),
+ ),
+ -1,
+ false,
+ ),
+ ),
+ );
+ } else {
+ opacity.set(
+ withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}, () => {
+ cancelAnimation(left);
+ cancelAnimation(width);
+ }),
+ );
+ }
+ // we want to update only when shouldShow changes
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, [shouldShow]);
+
+ const animatedIndicatorStyle = useAnimatedStyle(() => ({
+ left: `${left.get()}%`,
+ width: `${width.get()}%`,
+ }));
+
+ const animatedContainerStyle = useAnimatedStyle(() => ({
+ opacity: opacity.get(),
+ }));
+
+ return (
+
+
+
+ );
+}
+
+LoadingBar.displayName = 'ProgressBar';
+
+export default LoadingBar;
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index e1c5a7c48b86..e59f8f6453fd 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -14,9 +14,8 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import ComposerFocusManager from '@libs/ComposerFocusManager';
import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay';
-import useNativeDriver from '@libs/useNativeDriver';
import variables from '@styles/variables';
-import * as Modal from '@userActions/Modal';
+import {areAllModalsHidden, closeTop, onModalDidClose, setCloseModal, setModalVisibility, willAlertModalBecomeVisible} from '@userActions/Modal';
import CONST from '@src/CONST';
import ModalContent from './ModalContent';
import ModalContext from './ModalContext';
@@ -37,7 +36,7 @@ function BaseModal(
fullscreen = true,
animationIn,
animationOut,
- useNativeDriver: useNativeDriverProp,
+ useNativeDriver,
useNativeDriverForBackdrop,
hideModalContentWhileAnimating = false,
animationInTiming,
@@ -54,6 +53,7 @@ function BaseModal(
restoreFocusType,
shouldUseModalPaddingStyle = true,
initialFocus = false,
+ shouldPreventScrollOnFocus = false,
}: BaseModalProps,
ref: React.ForwardedRef,
) {
@@ -85,16 +85,16 @@ function BaseModal(
*/
const hideModal = useCallback(
(callHideCallback = true) => {
- if (Modal.areAllModalsHidden()) {
- Modal.willAlertModalBecomeVisible(false);
+ if (areAllModalsHidden()) {
+ willAlertModalBecomeVisible(false);
if (shouldSetModalVisibility) {
- Modal.setModalVisibility(false);
+ setModalVisibility(false);
}
}
if (callHideCallback) {
onModalHide();
}
- Modal.onModalDidClose();
+ onModalDidClose();
ComposerFocusManager.refocusAfterModalFullyClosed(uniqueModalId, restoreFocusType);
},
[shouldSetModalVisibility, onModalHide, restoreFocusType, uniqueModalId],
@@ -104,9 +104,9 @@ function BaseModal(
isVisibleRef.current = isVisible;
let removeOnCloseListener: () => void;
if (isVisible) {
- Modal.willAlertModalBecomeVisible(true, type === CONST.MODAL.MODAL_TYPE.POPOVER || type === CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED);
+ willAlertModalBecomeVisible(true, type === CONST.MODAL.MODAL_TYPE.POPOVER || type === CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED);
// To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu
- removeOnCloseListener = Modal.setCloseModal(onClose);
+ removeOnCloseListener = setCloseModal(onClose);
}
return () => {
@@ -131,7 +131,7 @@ function BaseModal(
const handleShowModal = () => {
if (shouldSetModalVisibility) {
- Modal.setModalVisibility(true);
+ setModalVisibility(true);
}
onModalShow();
};
@@ -231,7 +231,7 @@ function BaseModal(
onBackdropPress={handleBackdropPress}
// Note: Escape key on web/desktop will trigger onBackButtonPress callback
// eslint-disable-next-line react/jsx-props-no-multi-spaces
- onBackButtonPress={Modal.closeTop}
+ onBackButtonPress={closeTop}
onModalShow={handleShowModal}
propagateSwipe={propagateSwipe}
onModalHide={hideModal}
@@ -250,10 +250,8 @@ function BaseModal(
deviceWidth={windowWidth}
animationIn={animationIn ?? modalStyleAnimationIn}
animationOut={animationOut ?? modalStyleAnimationOut}
- // eslint-disable-next-line react-compiler/react-compiler
- useNativeDriver={useNativeDriverProp && useNativeDriver}
- // eslint-disable-next-line react-compiler/react-compiler
- useNativeDriverForBackdrop={useNativeDriverForBackdrop && useNativeDriver}
+ useNativeDriver={useNativeDriver}
+ useNativeDriverForBackdrop={useNativeDriverForBackdrop}
hideModalContentWhileAnimating={hideModalContentWhileAnimating}
animationInTiming={animationInTiming}
animationOutTiming={animationOutTiming}
@@ -271,6 +269,7 @@ function BaseModal(
{}, type, onModalShow = (
onModalShow={showModal}
avoidKeyboard={false}
fullscreen={fullscreen}
+ useNativeDriver={false}
+ useNativeDriverForBackdrop={false}
type={type}
>
{children}
diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts
index aee2045dbe47..6e8648630156 100644
--- a/src/components/Modal/types.ts
+++ b/src/components/Modal/types.ts
@@ -93,6 +93,9 @@ type BaseModalProps = Partial & {
/** Used to set the element that should receive the initial focus */
initialFocus?: FocusTrapOptions['initialFocus'];
+
+ /** Whether to prevent the focus trap from scrolling the element into view. */
+ shouldPreventScrollOnFocus?: boolean;
};
export default BaseModalProps;
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index aaa7c5c99ab5..9b030f9bfede 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -41,6 +41,7 @@ import {
isPending,
isReceiptBeingScanned,
shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils,
+ shouldShowRTERViolationMessage,
} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import {
@@ -72,6 +73,7 @@ import DelegateNoAccessModal from './DelegateNoAccessModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
+import LoadingBar from './LoadingBar';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar';
import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
@@ -183,7 +185,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldShowSettlementButton =
!shouldShowSubmitButton &&
(shouldShowPayButton || shouldShowApproveButton) &&
- !hasAllPendingRTERViolations &&
+ !shouldShowRTERViolationMessage(transactions) &&
!shouldShowExportIntegrationButton &&
!shouldShowBrokenConnectionViolation;
@@ -208,6 +210,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout);
const {isDelegateAccessRestricted} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
+ const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA);
const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP;
const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
@@ -347,7 +350,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
}, [canDeleteRequest]);
return (
-
+
{isDuplicate && !shouldUseNarrowLayout && (
@@ -427,7 +429,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
)}
{!!isMoreContentShown && (
-
+
{isDuplicate && shouldUseNarrowLayout && (