diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js
index ae82e38070e7..356ad936e1fd 100644
--- a/.eslintrc.changed.js
+++ b/.eslintrc.changed.js
@@ -20,12 +20,6 @@ module.exports = {
],
},
overrides: [
- {
- files: ['src/pages/workspace/WorkspaceInitialPage.tsx', 'src/pages/home/report/PureReportActionItem.tsx', 'src/libs/SidebarUtils.ts'],
- rules: {
- 'rulesdir/no-default-id-values': 'off',
- },
- },
{
files: ['**/libs/**/*.{ts,tsx}'],
rules: {
diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml
new file mode 100644
index 000000000000..b3db2c37d4d7
--- /dev/null
+++ b/.github/workflows/verifyHybridApp.yml
@@ -0,0 +1,141 @@
+name: Verify HybridApp build
+
+on:
+ workflow_call:
+ pull_request:
+ types: [opened, synchronize]
+ branches-ignore: [staging, production]
+ paths:
+ - '**.kt'
+ - '**.java'
+ - '**.swift'
+ - '**.mm'
+ - '**.h'
+ - '**.cpp'
+ - 'package.json'
+ - 'patches/**'
+ - 'android/build.gradle'
+ - 'android/AndroidManifest.xml'
+ - 'ios/Podfile.lock'
+ - 'ios/project.pbxproj'
+
+concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main
+ cancel-in-progress: true
+
+jobs:
+ verify_android:
+ name: Verify Android HybridApp builds on main
+ runs-on: ubuntu-latest-xl
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
+ ref: ${{ github.event.pull_request.head.sha }}
+ token: ${{ secrets.OS_BOTIFY_TOKEN }}
+ # fetch-depth: 0 is required in order to fetch the correct submodule branch
+ fetch-depth: 0
+
+ - name: Update submodule to match main
+ run: |
+ git submodule update --init --remote
+ git fetch
+ git checkout main
+
+ - name: Configure MapBox SDK
+ run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
+
+ - name: Setup Node
+ id: setup-node
+ uses: ./.github/actions/composite/setupNode
+ with:
+ IS_HYBRID_BUILD: 'true'
+
+ - name: Build Android Debug
+ working-directory: Mobile-Expensify/Android
+ run: |
+ if ! ./gradlew assembleDebug
+ then
+ echo "❌ Android HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve."
+ exit 1
+ fi
+
+ verify_ios:
+ name: Verify iOS HybridApp builds on main
+ runs-on: macos-15-xlarge
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
+ ref: ${{ github.event.pull_request.head.sha }}
+ token: ${{ secrets.OS_BOTIFY_TOKEN }}
+ # fetch-depth: 0 is required in order to fetch the correct submodule branch
+ fetch-depth: 0
+
+ - name: Update submodule to match main
+ run: |
+ git submodule update --init --remote
+ git fetch
+ git checkout main
+
+ - name: Configure MapBox SDK
+ run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
+
+ - name: Setup Node
+ id: setup-node
+ uses: ./.github/actions/composite/setupNode
+ with:
+ IS_HYBRID_BUILD: 'true'
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1.204.0
+ with:
+ bundler-cache: true
+
+ - name: Install New Expensify Gems
+ run: bundle install
+
+ - name: Cache Pod dependencies
+ uses: actions/cache@v4
+ id: pods-cache
+ with:
+ path: Mobile-Expensify/iOS/Pods
+ key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }}
+
+ - name: Compare Podfile.lock and Manifest.lock
+ id: compare-podfile-and-manifest
+ run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/Manifest.lock') }}" >> "$GITHUB_OUTPUT"
+
+ - name: Install cocoapods
+ uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
+ if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true'
+ with:
+ timeout_minutes: 10
+ max_attempts: 5
+ command: npm run pod-install
+
+ - name: Build iOS HybridApp
+ run: |
+ # Let us know if the builds fails
+ set -o pipefail
+
+ # Do not start metro
+ export RCT_NO_LAUNCH_PACKAGER=1
+
+ # Build iOS using xcodebuild
+ if ! xcodebuild \
+ -workspace Mobile-Expensify/iOS/Expensify.xcworkspace \
+ -scheme Expensify \
+ -configuration Debug \
+ -sdk iphonesimulator \
+ -arch x86_64 \
+ CODE_SIGN_IDENTITY="" \
+ CODE_SIGNING_REQUIRED=NO \
+ CODE_SIGNING_ALLOWED=NO \
+ build | xcpretty
+ then
+ echo "❌ iOS HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve."
+ exit 1
+ fi
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5abcd8191b15..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 1009008905
- versionName "9.0.89-5"
+ 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/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-billing/Billing-Overview.md b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md
index b7357245ac84..ae2c790bb3ae 100644
--- a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md
+++ b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md
@@ -1,42 +1,86 @@
---
title: Billing Overview
-description: An overview of how billing works in Expensify.
+description: Learn about Expensify billing, including active member charges, annual subscription savings, and pay-per-use options. Discover how the Expensify Card can reduce costs and maximize value.
---
-# Overview
-Expensify’s billing is based on monthly member activity. At the beginning of each month, you’ll be billed for the previous month’s activity. Your Expensify bill ultimately depends on your plan type, whether you're on an annual subscription or pay-per-use billing, and whether you’re using the Expensify Visa® Commercial Card.
-# How billing works in Expensify
-Expensify bills the owners of Group Workspaces on the first of each month for the previous month's usage. You can find billing receipts in **Settings > Account > Payments > Billing History**. We recommend that businesses have one billing owner for all of their Group Workspaces.
-## Active members
-An **active member** is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automated settings.
-## Annual subscription
-With an annual subscription, you set your monthly active member count at the beginning and get a 50% discount on your monthly active member cost. That means an annual subscription paired with the Collect plan will cost $10 per monthly active member instead of $20, and the Control plan will cost $18 instead of $36.
-Each month, you’ll be billed for the amount of members you originally set in your subscription size. Any active members in a given month above this subscription size will be billed at the pay-per-use rate.
+Expensify’s billing is based on monthly member activity. You’ll be charged for the previous month’s usage at the beginning of each month. Your bill depends on:
+- Plan type: Annual subscription or pay-per-use.
+- Expensify Visa® Commercial Card usage: Discounts are available based on your card spending.
-For example, let’s say you set your annual subscription size at 10 members and you’re on the Control plan. You’ll be billed $18/member for 10 members each month. However, let’s say in one particular month you had 12 active members, you’d be billed at $18/member for the 10 members in your subscription size + $36/member (pay-per-use rate) for the additional 2 active members.
+---
+# How Billing Works
+- Billing occurs on the first of each month for the previous month’s usage.
+- Only Group Workspace owners are billed.
+- View billing receipts in:
+ Settings > Account > Payments > Billing History.
-You can always increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. If you have any questions about this, contact Concierge or your account manager.
-## Pay-per-use
-The pay-per-use rate is the full rate per active member without any discounts. The pay-per-use rate for each member on the Collect plan is $20, and on the Control plan, it is $36.
-## 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.
+**Tip: Designate one billing owner for all Group Workspaces to streamline billing management.**
-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.
+---
+## What is an Active Member?
+An active member is anyone who performs any of these actions in Expensify during a month:
+- Chats
+- Creates, submits, approves, reimburses, or exports a report
+- Uses the Copilot feature to take an action in another user's account
-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
-To see how much money you can save (and even earn!) by using the Expensify Card, check out our [savings calculator](https://use.expensify.com/price-savings-calculator). Just enter a few details and see how much you’ll save!
+---
+# Annual Subscription
-{% include faq-begin.md %}
+## Key Benefits:
+- Save 50% per active member compared to pay-per-use billing.
+ - Collect plan: $10 per member (vs. $20).
+ - Control plan: $18 per member (vs. $36).
+- Set your monthly active member count upfront and pay a fixed rate.
-## What if we put less than 50% of our total spend on the Expensify Card?
-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.
+## How It Works:
+- You’ll be billed for the number of members set in your subscription.
+- Extra active members beyond your subscription size are charged at the pay-per-use rate.
**Example:**
-- Annual subscription discount: 50%
-- % of total settled Expensify Card spend (US purchases only) across all workspaces: 20%
-- Expensify Card discount: 20%
+- Plan: Control
+- Subscription size: 10 members
+ - Cost: $18/member x 10 members = $180/month
+- Scenario: 12 active members in one month
+ - Cost for additional two members: $36/member = $72
+ - Total bill: $252
+
+**Adjustments:**
+- You can increase your subscription size by extending your subscription period.
+- Reductions are only allowed after your current subscription ends.
+---
+# Pay-Per-Use Billing
+- Charges apply at full rates with no discounts.
+ - Collect plan: $20 per active member.
+ - Control plan: $36 per active member.
+
+---
+# How the Expensify Card Reduces Your Bill
+
+## Bundling Benefits:
+- Combine an Expensify Card with an annual subscription for the lowest price per member.
+- Spending at least 50% of your total settled US spend on Expensify Cards earns a further 50% discount.
-In that case, you'd save 70% on the price per member for that month's bill.
+## Discount Breakdown:
+- Collect plan: $5/member.
+- Control plan: $9/member.
+
+## Additional Savings:
+- Earn 1% cash back on Expensify Card purchases.
+ - 2% cash back if total card spend exceeds $250k (US purchases only).
+ - Cashback is first applied to your bill, reducing costs further. Any surplus is deposited into your bank account.
+
+## Savings Calculator
+Use our [savings calculator](https://use.expensify.com/price-savings-calculator) to estimate potential savings and earnings with the Expensify Card. Enter your details to see the results!
+
+---
+# FAQ
+
+## What if less than 50% of the spend is on Expensify Cards?
+Discounts are applied on a sliding scale based on your Expensify Card spend percentage.
+
+**Example:**
+- Annual subscription discount: 50%
+- Expensify Card spend (US purchases): 20% of total spend
+- Expensify Card discount: 20%
+- Total savings: 70% discount on the per-member price for that month.
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
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/new-expensify/expensify-card/Expensify-Card-Perks.md b/docs/articles/new-expensify/expensify-card/Expensify-Card-Perks.md
new file mode 100644
index 000000000000..e3de671982a1
--- /dev/null
+++ b/docs/articles/new-expensify/expensify-card/Expensify-Card-Perks.md
@@ -0,0 +1,174 @@
+---
+title: Expensify Card Perks
+description: An overview of all the perks the Expensify Card offers
+---
+From cash-back rewards to discounts on popular services, the Expensify Card offers benefits that make it more than just a corporate card. In this article, we’ll highlight the various perks available to cardholders, how to access them, and how they can benefit your business operations. Whether you're looking to cut costs, streamline workflows, or unlock valuable savings, the Expensify Card perks are tailored to meet your needs.
+
+# Expensify Perks
+
+## Cashback
+Get 1% cash back with every swipe — no minimums necessary — and 2% back if you spend $250k+/month across cards.
+
+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.
+
+# Perks with Expensify's Partners
+
+## Amazon AWS
+Whether you're a two-person startup or a venture-backed company, AWS Activate provides access to the resources you need to get started on AWS quickly, including free credits, technical support, and training. All Expensify Cardholders qualify when they add their Expensify Card for AWS billing!
+
+**How to redeem:** Apply [here](https://aws.amazon.com/activate/portfolio-signup) using OrgID: 0qyIA (Case Sensitive).
+
+For more details, refer to the [AWS Activate terms and conditions](https://aws.amazon.com/activate/terms/) and the [Activate FAQs](https://aws.amazon.com/activate/faq/).
+
+## Stripe
+Stripe’s integrated payments platform helps you build and scale your business globally, whether you're creating a subscription service, an on-demand marketplace, or an e-commerce store.
+
+Stripe will waive the processing fees for the first $5,000 in payments for Expensify Cardholders.
+
+**How to redeem:** Sign up for Stripe using your Expensify Card.
+
+## Lamar Advertising
+Lamar offers out-of-home advertising on billboards, digital displays, airport signage, transit, and highway logo signs. Expensify Cardholders receive a minimum 10% discount on their first campaign.
+
+**How to redeem:** Contact Expensify’s dedicated account manager, Lisa Kane (email: lkane@lamar.com), and mention that you’re an Expensify Cardholder.
+
+## Carta
+Simplify equity management with Carta. Expensify Cardholders receive a 20% discount on the first year and waived implementation fees.
+
+**How to redeem:** Sign up using your Expensify Card.
+
+## Pilot
+Pilot specializes in bookkeeping and tax preparation for startups and e-commerce providers. When you use Pilot, you're paired with a dedicated finance expert who handles the work for you and answers your questions.
+
+**Offer:** 20% off the first six months of Pilot Core.
+
+**How to redeem:** Sign up using your Expensify Card.
+
+## Spotlight Reporting
+Spotlight Reporting is a cloud-based reporting and forecasting tool designed by accountants, for accountants. Expensify Cardholders receive a 20% discount on their subscription for the first six months, plus one free seat for Spotlight Certification.
+
+**How to redeem:** Sign up using your Expensify Card.
+
+## Guideline
+Guideline's full-service 401(k) plans make it easier and more affordable to offer your employees the retirement benefits they deserve.
+
+**Offer:** Three months free.
+
+**How to redeem:** Sign up using your Expensify Card.
+
+## Gusto
+Gusto's platform helps businesses onboard, pay, insure, and support their teams. Expensify Cardholders receive three months of free service.
+
+**How to redeem:** Sign up using your Expensify Card.
+
+## QuickBooks Online
+QuickBooks accounting software keeps your books accurate and up to date with features like invoicing, cash flow management, expense tracking, and more.
+
+**Offer:** 30% off QuickBooks Online for the first 12 months.
+
+**How to redeem:** Sign up using your Expensify Card.
+
+## Highfive
+Highfive improves the ease and quality of in-room video conferencing. Expensify Cardholders receive 50% off the Highfive Select Starter Package and 10% off the Highfive Premium Package.
+
+**How to redeem:** Sign up using your Expensify Card.
+
+## Zendesk
+Expensify Cardholders receive $436 in credits for Zendesk Suite products per month for the first year.
+
+**How to redeem:**
+- Reach out to startups@zendesk.com with the message: "Expensify asked me to send an email regarding the Zendesk promotion.” You'll receive a promo code to use.
+- Start a Zendesk trial (Suite or another plan) in USD (if your trial is not in USD, contact Zendesk).
+- Click the "Buy Now" button inside your trial.
+- Select your plan with monthly billing.
+ - The $436 monthly credit applies to up to four licenses of the Suite but can also be applied to any other monthly plan.
+- Enter the promo code you receive.
+- Complete the checkout process.
+ - After 12 monthly billing periods, your free credit will expire, and you'll be charged for the next month.
+
+## Xero
+Xero offers accounting software with everything you need to run your business seamlessly.
+
+**Offer:** U.S. residents receive 50% off Xero for six months.
+
+**How to redeem:** Visit [this page](https://apps.xero.com/us/app/expensify?xtid=x30expensify&utm_source=expensify&utm_medium=web&utm_campaign=cardoffer) and sign up using your Expensify Card.
+
+## Freshworks
+Boost your startup journey with customer and employee engagement solutions from Freshworks, including CRM, live chat, support, marketing automation, ITSM, and HRMS.
+
+**Offer:** $4,000 in credits on Freshworks products.
+
+**How to redeem:** Click [here](https://www.freshworks.com/partners/startup-program/expensify-card/) and fill out the form; Freshworks will automatically recognize your company as an Expensify Card customer.
+
+## Slack
+Get 25% off your first year with Slack, enjoying premium features like unlimited messaging, apps, Slack Connect channels, group video calls, priority support, and more.
+
+**How to redeem:** Click [here](https://slack.com/promo/partner?remote_promo=ead919f5) to redeem the offer using your Expensify Card.
+
+## Deel
+Deel simplifies onboarding international team members in 150 different countries. Expensify Cardholders receive three months free, followed by 30% off for the rest of the year.
+
+**How to redeem:** Click [here](https://www.deel.com/partners/expensify) and sign up using your Expensify Card.
+
+## Snap
+Expensify Cardholders receive $1,000 in Snap credits when they spend $1,000 in Snapchat's Ads Manager.
+
+**How to redeem:**
+- Click "Create Ad" or "Request a Call" by clicking [here](https://forbusiness.snapchat.com/).
+- Enter your details to set up your account if you don't already have one.
+- Add the Expensify Card as your payment option for your Snap Business account.
+- Credits will automatically be placed in your account once you've reached $1,000 in spend.
+
+## Aircall
+Aircall is a cloud-based phone system for sales and support teams. Expensify Cardholders receive two months free, with discounts ranging from $270 to $9,000+ depending on the number of users.
+
+**How to redeem:**
+- Click [here](http://pages.aircall.io/Expensify-RewardsPartnerReferral.html) to sign up for a demo.
+- Let the Aircall team know you're an Expensify customer.
+
+## NetSuite
+NetSuite helps companies manage core business processes with a cloud-based ERP and accounting software. Expensify has a direct integration with NetSuite to synchronize data and customize expense coding.
+
+**Offer:** 10% off for the first year.
+
+**How to redeem:**
+- Fill out this [Google form](https://docs.google.com/forms/d/e/1FAIpQLSeiOzLrdO-MgqeEMwEqgdQoh4SJBi42MZME9ycHp4SQjlk3bQ/viewform?usp=sf_link).
+- An Expensify rep will introduce you to a NetSuite sales rep to start the process.
+- Once set up, pay for your first year with NetSuite, and Expensify will send you a payment equal to 10% of your first-year contract within three months of your first NetSuite invoice.
+
+**Note:** This offer is only for prospective NetSuite customers.
+
+## PagerDuty
+PagerDuty's platform integrates machine data and human intelligence to improve visibility and agility across organizations.
+
+**Offer:** 25% off.
+
+**How to redeem:** Sign up using your Expensify Card and enter the discount code EXPENSIFYPDTEAM for the Team plan or EXPENSIFYPDBUSINESS for the Business plan at checkout.
+
+## Typeform
+Typeform makes collecting and sharing information easy and conversational, allowing you to create anything from surveys to apps without writing a single line of code.
+
+**Offer:** 30% off annual Premium and Professional plans.
+
+**How to redeem:**
+- Click [here](https://try.typeform.com/expensify/?utm_source=expensify&utm_medium=referral&utm_campaign=expensify_integration&utm_content=directory) to get Typeform.
+- Enter your details and set up your free account.
+- Verify your email by clicking on the link Typeform sends you.
+- Complete the onboarding flow within Typeform.
+- Click the "Upgrade" button in your workspace.
+- Select your plan.
+- Enter the coupon code EXPENSIFY30 on the checkout page.
+- Fill out all payment details using your Expensify Card and click "Upgrade now."
+
+## Intercom
+Intercom offers a suite of messaging-first products to help businesses accelerate growth across the customer lifecycle.
+
+**Offer:** Three months of free service.
+
+**How to redeem:** Sign up using your Expensify Card.
+
+## Talkspace
+Talkspace provides prescription management and personalized treatment through a network of licensed prescribers trained in mental healthcare. Expensify Cardholders receive $125 off Talk
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 839aa19531fe..8884b8dad876 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
@@ -123,7 +123,7 @@ https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-c
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Connect-ANZ
-https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections
+https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting
https://help.expensify.com/articles/expensify-classic/reports/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules
@@ -517,7 +517,7 @@ https://community.expensify.com/discussion/5554/deep-dive-what-are-domains-and-h
https://community.expensify.com/discussion/5587/deep-dive-understanding-policy-types-and-billing-options,https://help.expensify.com/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription
https://community.expensify.com/discussion/5643/deep-dive-submit-and-approve,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses
https://community.expensify.com/discussion/5668/deep-dive-how-trips-work-and-benefits,https://help.expensify.com/articles/expensify-classic/expenses/Trips
-https://community.expensify.com/discussion/5689/deep-dive-is-a-direct-connection-the-best-option/,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections
+https://community.expensify.com/discussion/5689/deep-dive-is-a-direct-connection-the-best-option/,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards
https://community.expensify.com/discussion/6592/deep-dive-ach-reimbursement-timing,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments#faq
https://community.expensify.com/discussion/7914/how-to-pay-an-invoice-and-bill-in-expensify,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills
https://community.expensify.com/discussion/8229/connecting-kayak-for-business,https://help.expensify.com/articles/expensify-classic/connections/Additional-Travel-Integrations#how-to-connect-to-kayak-for-business
@@ -612,7 +612,6 @@ 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/
@@ -621,4 +620,6 @@ https://help.expensify.com/articles/expensify-classic/connect-credit-cards/compa
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 d2ba9b4ee5f3..184498800897 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.0.89.5
+ 9.0.89.6FullStoryOrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 5c437d85b17a..1c17b3df0e3d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature????CFBundleVersion
- 9.0.89.5
+ 9.0.89.6
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 90d30b3c3bb7..99c7550ee5f2 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString9.0.89CFBundleVersion
- 9.0.89.5
+ 9.0.89.6NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 0e4f36fa00d2..184c7101b6b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.89-5",
+ "version": "9.0.89-6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.89-5",
+ "version": "9.0.89-6",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index f988b3ac6541..43c38ed9b904 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.89-5",
+ "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 7192e0548b38..b318dde9d9c1 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1063,6 +1063,7 @@ const CONST = {
CREATED: 'CREATED',
DELEGATE_SUBMIT: 'DELEGATESUBMIT', // OldDot Action
DELETED_ACCOUNT: 'DELETEDACCOUNT', // Deprecated OldDot Action
+ DELETED_TRANSACTION: 'DELETEDTRANSACTION',
DISMISSED_VIOLATION: 'DISMISSEDVIOLATION',
DONATION: 'DONATION', // Deprecated OldDot Action
EXPORTED_TO_CSV: 'EXPORTCSV', // OldDot Action
@@ -2402,6 +2403,7 @@ const CONST = {
TRACK: 'track',
},
AMOUNT_MAX_LENGTH: 8,
+ DISTANCE_REQUEST_AMOUNT_MAX_LENGTH: 14,
RECEIPT_STATE: {
SCANREADY: 'SCANREADY',
OPEN: 'OPEN',
@@ -3150,7 +3152,7 @@ const CONST = {
* Group 2: Optional email group between \s+....\s* start rule with @+valid email or short mention
* Group 3: Title is remaining characters
*/
- TASK_TITLE_WITH_OPTONAL_SHORT_MENTION: `^\\[\\]\\s+(?:@(?:${EMAIL_WITH_OPTIONAL_DOMAIN}))?\\s*([\\s\\S]*)`,
+ TASK_TITLE_WITH_OPTONAL_SHORT_MENTION: `^\\[\\]\\s+(?:@(?:${EMAIL_WITH_OPTIONAL_DOMAIN.source}))?\\s*([\\s\\S]*)`,
},
PRONOUNS: {
@@ -6536,6 +6538,7 @@ const CONST = {
BOTTOM_NAV_INBOX_TOOLTIP: 'bottomNavInboxTooltip',
LHN_WORKSPACE_CHAT_TOOLTIP: 'workspaceChatLHNTooltip',
GLOBAL_CREATE_TOOLTIP: 'globalCreateTooltip',
+ SCAN_TEST_TOOLTIP: 'scanTestTooltip',
},
SMART_BANNER_HEIGHT: 152,
} as const;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 7750e795e9c7..1a8caaa04408 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -228,9 +228,6 @@ const ONYXKEYS = {
/** The NVP containing the target url to navigate to when deleting a transaction */
NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL: 'nvp_deleteTransactionNavigateBackURL',
- /** Does this user have push notifications enabled for this device? */
- PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled',
-
/** Plaid data (access tokens, bank accounts ...) */
PLAID_DATA: 'plaidData',
@@ -964,7 +961,6 @@ type OnyxValuesMapping = {
[ONYXKEYS.HAS_NON_PERSONAL_POLICY]: boolean;
[ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates;
[ONYXKEYS.NVP_SEEN_NEW_USER_MODAL]: boolean;
- [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean;
[ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData;
[ONYXKEYS.IS_PLAID_DISABLED]: boolean;
[ONYXKEYS.PLAID_LINK_TOKEN]: string;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index b150e6841ec6..e642d2594039 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -4,6 +4,7 @@ import type CONST from './CONST';
import type {IOUAction, IOUType} from './CONST';
import type {IOURequestType} from './libs/actions/IOU';
import Log from './libs/Log';
+import type {ReimbursementAccountStepToOpen} from './libs/ReimbursementAccountUtils';
import type {ExitReason} from './types/form/ExitSurveyReasonForm';
import type {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy';
import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
@@ -112,7 +113,12 @@ const ROUTES = {
BANK_ACCOUNT_PERSONAL: 'bank-account/personal',
BANK_ACCOUNT_WITH_STEP_TO_OPEN: {
route: 'bank-account/:stepToOpen?',
- getRoute: (stepToOpen = '', policyID = '', backTo?: string) => getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}`, backTo),
+ getRoute: (policyID: string | undefined, stepToOpen: ReimbursementAccountStepToOpen = '', backTo?: string) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the BANK_ACCOUNT_WITH_STEP_TO_OPEN route');
+ }
+ return getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}`, backTo);
+ },
},
WORKSPACE_SWITCHER: 'workspace-switcher',
SETTINGS: 'settings',
@@ -322,8 +328,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 +410,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 +437,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',
@@ -763,7 +788,7 @@ const ROUTES = {
route: 'settings/workspaces/:policyID',
getRoute: (policyID: string | undefined, backTo?: string) => {
if (!policyID) {
- Log.warn('Invalid policyID while building route WORKSPACE_INITIAL');
+ Log.warn('Invalid policyID is used to build the WORKSPACE_INITIAL route');
}
return `${getUrlWithBackToParam(`settings/workspaces/${policyID}`, backTo)}` as const;
},
@@ -778,7 +803,12 @@ const ROUTES = {
},
WORKSPACE_PROFILE: {
route: 'settings/workspaces/:policyID/profile',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_PROFILE route');
+ }
+ return `settings/workspaces/${policyID}/profile` as const;
+ },
},
WORKSPACE_PROFILE_ADDRESS: {
route: 'settings/workspaces/:policyID/profile/address',
@@ -960,7 +990,12 @@ const ROUTES = {
},
WORKSPACE_WORKFLOWS: {
route: 'settings/workspaces/:policyID/workflows',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/workflows` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_WORKFLOWS route');
+ }
+ return `settings/workspaces/${policyID}/workflows` as const;
+ },
},
WORKSPACE_WORKFLOWS_APPROVALS_NEW: {
route: 'settings/workspaces/:policyID/workflows/approvals/new',
@@ -993,7 +1028,12 @@ const ROUTES = {
},
WORKSPACE_INVOICES: {
route: 'settings/workspaces/:policyID/invoices',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/invoices` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_INVOICES route');
+ }
+ return `settings/workspaces/${policyID}/invoices` as const;
+ },
},
WORKSPACE_INVOICES_COMPANY_NAME: {
route: 'settings/workspaces/:policyID/invoices/company-name',
@@ -1007,7 +1047,7 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/members',
getRoute: (policyID: string | undefined) => {
if (!policyID) {
- Log.warn('Invalid policyID while building route WORKSPACE_MEMBERS');
+ Log.warn('Invalid policyID is used to build the WORKSPACE_MEMBERS route');
}
return `settings/workspaces/${policyID}/members` as const;
},
@@ -1024,8 +1064,9 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting',
getRoute: (policyID: string | undefined, newConnectionName?: ConnectionName, integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean) => {
if (!policyID) {
- Log.warn('Invalid policyID while building route POLICY_ACCOUNTING');
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING route');
}
+
let queryParams = '';
if (newConnectionName) {
queryParams += `?newConnectionName=${newConnectionName}`;
@@ -1065,7 +1106,7 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/categories',
getRoute: (policyID: string | undefined) => {
if (!policyID) {
- Log.warn('Invalid policyID while building route WORKSPACE_CATEGORIES');
+ Log.warn('Invalid policyID is used to build the WORKSPACE_CATEGORIES route');
}
return `settings/workspaces/${policyID}/categories` as const;
},
@@ -1135,14 +1176,19 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/more-features',
getRoute: (policyID: string | undefined) => {
if (!policyID) {
- Log.warn('Invalid policyID while building route WORKSPACE_MORE_FEATURES');
+ Log.warn('Invalid policyID is used to build the WORKSPACE_MORE_FEATURES route');
}
return `settings/workspaces/${policyID}/more-features` as const;
},
},
WORKSPACE_TAGS: {
route: 'settings/workspaces/:policyID/tags',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_TAGS route');
+ }
+ return `settings/workspaces/${policyID}/tags` as const;
+ },
},
WORKSPACE_TAG_CREATE: {
route: 'settings/workspaces/:policyID/tags/new',
@@ -1186,7 +1232,12 @@ const ROUTES = {
},
WORKSPACE_TAXES: {
route: 'settings/workspaces/:policyID/taxes',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_TAXES route');
+ }
+ return `settings/workspaces/${policyID}/taxes` as const;
+ },
},
WORKSPACE_TAXES_SETTINGS: {
route: 'settings/workspaces/:policyID/taxes/settings',
@@ -1251,7 +1302,12 @@ const ROUTES = {
},
WORKSPACE_REPORT_FIELDS: {
route: 'settings/workspaces/:policyID/reportFields',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_REPORT_FIELDS route');
+ }
+ return `settings/workspaces/${policyID}/reportFields` as const;
+ },
},
WORKSPACE_CREATE_REPORT_FIELD: {
route: 'settings/workspaces/:policyID/reportFields/new',
@@ -1291,6 +1347,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,
@@ -1318,7 +1383,12 @@ const ROUTES = {
},
WORKSPACE_EXPENSIFY_CARD: {
route: 'settings/workspaces/:policyID/expensify-card',
- getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/expensify-card` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_EXPENSIFY_CARD route');
+ }
+ return `settings/workspaces/${policyID}/expensify-card` as const;
+ },
},
WORKSPACE_EXPENSIFY_CARD_DETAILS: {
route: 'settings/workspaces/:policyID/expensify-card/:cardID',
@@ -1358,7 +1428,12 @@ const ROUTES = {
},
WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT: {
route: 'settings/workspaces/:policyID/expensify-card/choose-bank-account',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/choose-bank-account` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT route');
+ }
+ return `settings/workspaces/${policyID}/expensify-card/choose-bank-account` as const;
+ },
},
WORKSPACE_EXPENSIFY_CARD_SETTINGS: {
route: 'settings/workspaces/:policyID/expensify-card/settings',
@@ -1382,11 +1457,21 @@ const ROUTES = {
},
WORKSPACE_RULES: {
route: 'settings/workspaces/:policyID/rules',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_RULES route');
+ }
+ return `settings/workspaces/${policyID}/rules` as const;
+ },
},
WORKSPACE_DISTANCE_RATES: {
route: 'settings/workspaces/:policyID/distance-rates',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_DISTANCE_RATES route');
+ }
+ return `settings/workspaces/${policyID}/distance-rates` as const;
+ },
},
WORKSPACE_CREATE_DISTANCE_RATE: {
route: 'settings/workspaces/:policyID/distance-rates/new',
@@ -1414,7 +1499,12 @@ const ROUTES = {
},
WORKSPACE_PER_DIEM: {
route: 'settings/workspaces/:policyID/per-diem',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the WORKSPACE_PER_DIEM route');
+ }
+ return `settings/workspaces/${policyID}/per-diem` as const;
+ },
},
WORKSPACE_PER_DIEM_IMPORT: {
route: 'settings/workspaces/:policyID/per-diem/import',
@@ -1917,7 +2007,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/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 (
({
- 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/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx
index 7526720bddc0..4e6327ec5661 100644
--- a/src/components/KYCWall/BaseKYCWall.tsx
+++ b/src/components/KYCWall/BaseKYCWall.tsx
@@ -4,16 +4,16 @@ import {Dimensions} from 'react-native';
import type {EmitterSubscription, GestureResponderEvent, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
-import * as BankAccounts from '@libs/actions/BankAccounts';
+import {openPersonalBankAccountSetupView} from '@libs/actions/BankAccounts';
import {completePaymentOnboarding} from '@libs/actions/IOU';
import getClickedTargetLocation from '@libs/getClickedTargetLocation';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
-import * as PaymentUtils from '@libs/PaymentUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import * as PaymentMethods from '@userActions/PaymentMethods';
-import * as Policy from '@userActions/Policy/Policy';
-import * as Wallet from '@userActions/Wallet';
+import {hasExpensifyPaymentMethod} from '@libs/PaymentUtils';
+import {isExpenseReport as isExpenseReportReportUtils, isIOUReport} from '@libs/ReportUtils';
+import {kycWallRef} from '@userActions/PaymentMethods';
+import {createWorkspaceFromIOUPayment} from '@userActions/Policy/Policy';
+import {setKYCWallSource} from '@userActions/Wallet';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -104,19 +104,19 @@ function KYCWall({
onSelectPaymentMethod(paymentMethod);
if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
- BankAccounts.openPersonalBankAccountSetupView();
+ openPersonalBankAccountSetupView();
} else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
Navigation.navigate(addDebitCardRoute);
} else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) {
- if (iouReport && ReportUtils.isIOUReport(iouReport)) {
- const {policyID, workspaceChatReportID, reportPreviewReportActionID, adminsChatReportID} = Policy.createWorkspaceFromIOUPayment(iouReport) ?? {};
+ if (iouReport && isIOUReport(iouReport)) {
+ const {policyID, workspaceChatReportID, reportPreviewReportActionID, adminsChatReportID} = createWorkspaceFromIOUPayment(iouReport) ?? {};
completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, adminsChatReportID, policyID);
if (workspaceChatReportID) {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(workspaceChatReportID, reportPreviewReportActionID));
}
// Navigate to the bank account set up flow for this specific policy
- Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID));
+ Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID));
return;
}
@@ -140,7 +140,7 @@ function KYCWall({
* Set the source, so we can tailor the process according to how we got here.
* We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold.
*/
- Wallet.setKYCWallSource(source, chatReportID);
+ setKYCWallSource(source, chatReportID);
if (shouldShowAddPaymentMenu) {
setShouldShowAddPaymentMenu(false);
@@ -152,13 +152,13 @@ function KYCWall({
transferBalanceButtonRef.current = targetElement;
- const isExpenseReport = ReportUtils.isExpenseReport(iouReport);
+ const isExpenseReport = isExpenseReportReportUtils(iouReport);
const paymentCardList = fundList ?? {};
// Check to see if user has a valid payment method on file and display the add payment popover if they don't
if (
(isExpenseReport && reimbursementAccount?.achData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN) ||
- (!isExpenseReport && bankAccountList !== null && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard))
+ (!isExpenseReport && bankAccountList !== null && !hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard))
) {
Log.info('[KYC Wallet] User does not have valid payment method');
@@ -213,7 +213,7 @@ function KYCWall({
useEffect(() => {
let dimensionsSubscription: EmitterSubscription | null = null;
- PaymentMethods.kycWallRef.current = {continueAction};
+ kycWallRef.current = {continueAction};
if (shouldListenForResize) {
dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition);
@@ -224,7 +224,7 @@ function KYCWall({
dimensionsSubscription.remove();
}
- PaymentMethods.kycWallRef.current = null;
+ kycWallRef.current = null;
};
}, [chatReportID, setMenuPosition, shouldListenForResize, continueAction]);
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/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index 7ad0eeb433ff..e59f8f6453fd 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -53,6 +53,7 @@ function BaseModal(
restoreFocusType,
shouldUseModalPaddingStyle = true,
initialFocus = false,
+ shouldPreventScrollOnFocus = false,
}: BaseModalProps,
ref: React.ForwardedRef,
) {
@@ -268,6 +269,7 @@ function BaseModal(
& {
/** 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/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index af312151da2e..4536b18217a2 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -851,7 +851,7 @@ function MoneyRequestConfirmationList({
if (iouType !== CONST.IOU.TYPE.PAY) {
// validate the amount for distance expenses
const decimals = getCurrencyDecimals(iouCurrencyCode);
- if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !validateAmount(String(iouAmount), decimals)) {
+ if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !validateAmount(String(iouAmount), decimals, CONST.IOU.DISTANCE_REQUEST_AMOUNT_MAX_LENGTH)) {
setFormError('common.error.invalidAmount');
return;
}
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index 7469fd170dde..0c934ff12500 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -712,7 +712,7 @@ function MoneyRequestConfirmationListFooter({
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
disabledStyle={styles.cursorDefault}
- style={styles.flex1}
+ style={[styles.h100, styles.flex1]}
>
;
@@ -113,6 +114,22 @@ const TOOLTIPS: Record = {
priority: 800,
shouldShow: () => true,
},
+ [SCAN_TEST_TOOLTIP]: {
+ content: [
+ {text: 'productTrainingTooltip.scanTestTooltip.part1', isBold: false},
+ {text: 'productTrainingTooltip.scanTestTooltip.part2', isBold: true},
+ {text: 'productTrainingTooltip.scanTestTooltip.part3', isBold: false},
+ {text: 'productTrainingTooltip.scanTestTooltip.part4', isBold: true},
+ {text: 'productTrainingTooltip.scanTestTooltip.part5', isBold: false},
+ {text: 'productTrainingTooltip.scanTestTooltip.part6', isBold: false},
+ {text: 'productTrainingTooltip.scanTestTooltip.part7', isBold: true},
+ {text: 'productTrainingTooltip.scanTestTooltip.part8', isBold: false},
+ ],
+ onHideTooltip: () => dismissProductTraining(SCAN_TEST_TOOLTIP),
+ name: SCAN_TEST_TOOLTIP,
+ priority: 900,
+ shouldShow: () => false,
+ },
};
export default TOOLTIPS;
diff --git a/src/components/ReportActionItem/ChronosOOOListActions.tsx b/src/components/ReportActionItem/ChronosOOOListActions.tsx
index 460104a71d68..365b22a49c2c 100644
--- a/src/components/ReportActionItem/ChronosOOOListActions.tsx
+++ b/src/components/ReportActionItem/ChronosOOOListActions.tsx
@@ -6,14 +6,14 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import DateUtils from '@libs/DateUtils';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as Chronos from '@userActions/Chronos';
+import {getOriginalMessage} from '@libs/ReportActionsUtils';
+import {removeEvent} from '@userActions/Chronos';
import type CONST from '@src/CONST';
import type ReportAction from '@src/types/onyx/ReportAction';
type ChronosOOOListActionsProps = {
/** The ID of the report */
- reportID: string;
+ reportID: string | undefined;
/** All the data of the action */
action: ReportAction;
@@ -24,7 +24,7 @@ function ChronosOOOListActions({reportID, action}: ChronosOOOListActionsProps) {
const {translate, preferredLocale} = useLocalize();
- const events = ReportActionsUtils.getOriginalMessage(action)?.events ?? [];
+ const events = getOriginalMessage(action)?.events ?? [];
if (!events.length) {
return (
@@ -61,7 +61,7 @@ function ChronosOOOListActions({reportID, action}: ChronosOOOListActionsProps) {
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index d1dcdb2f57f5..6354b69bc58e 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -16,12 +16,25 @@ import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as CurrencyUtils from '@libs/CurrencyUtils';
+import {convertToDisplayString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as ReportUtils from '@libs/ReportUtils';
+import {
+ getAvailableReportFields,
+ getFieldViolation,
+ getFieldViolationTranslation,
+ getMoneyRequestSpendBreakdown,
+ getReportFieldKey,
+ hasUpdatedTotal,
+ isClosedExpenseReportWithNoExpenses as isClosedExpenseReportWithNoExpensesReportUtils,
+ isInvoiceReport as isInvoiceReportUtils,
+ isPaidGroupPolicyExpenseReport as isPaidGroupPolicyExpenseReportUtils,
+ isReportFieldDisabled,
+ isReportFieldOfTypeTitle,
+ isSettled as isSettledReportUtils,
+} from '@libs/ReportUtils';
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
import variables from '@styles/variables';
-import * as reportActions from '@src/libs/actions/Report';
+import {clearReportFieldKeyErrors} from '@src/libs/actions/Report';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy, PolicyReportField, Report} from '@src/types/onyx';
@@ -29,7 +42,7 @@ import type {PendingAction} from '@src/types/onyx/OnyxCommon';
type MoneyReportViewProps = {
/** The report currently being looked at */
- report: Report;
+ report: OnyxEntry;
/** Policy that the report belongs to */
policy: OnyxEntry;
@@ -52,15 +65,15 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- const isSettled = ReportUtils.isSettled(report.reportID);
- const isTotalUpdated = ReportUtils.hasUpdatedTotal(report, policy);
+ const isSettled = isSettledReportUtils(report?.reportID);
+ const isTotalUpdated = hasUpdatedTotal(report, policy);
- const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report);
+ const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(report);
const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend && shouldShowTotal;
- const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, report.currency);
- const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, report.currency);
- const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, report.currency);
+ const formattedTotalAmount = convertToDisplayString(totalDisplaySpend, report?.currency);
+ const formattedOutOfPocketAmount = convertToDisplayString(reimbursableSpend, report?.currency);
+ const formattedCompanySpendAmount = convertToDisplayString(nonReimbursableSpend, report?.currency);
const isPartiallyPaid = !!report?.pendingFields?.partial;
const subAmountTextStyles: StyleProp = [
@@ -70,25 +83,25 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
StyleUtils.getColorStyle(theme.textSupporting),
];
- const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`);
+ const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report?.reportID}`);
const sortedPolicyReportFields = useMemo((): PolicyReportField[] => {
- const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.fieldList ?? {}));
- return fields.filter((field) => field.target === report.type).sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight);
+ const fields = getAvailableReportFields(report, Object.values(policy?.fieldList ?? {}));
+ return fields.filter((field) => field.target === report?.type).sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight);
}, [policy, report]);
- const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !ReportUtils.isReportFieldDisabled(report, reportField, policy));
- const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && ReportUtils.isReportFieldOfTypeTitle(enabledReportFields.at(0));
- const isClosedExpenseReportWithNoExpenses = ReportUtils.isClosedExpenseReportWithNoExpenses(report);
- const isPaidGroupPolicyExpenseReport = ReportUtils.isPaidGroupPolicyExpenseReport(report);
- const isInvoiceReport = ReportUtils.isInvoiceReport(report);
+ const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !isReportFieldDisabled(report, reportField, policy));
+ const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0));
+ const isClosedExpenseReportWithNoExpenses = isClosedExpenseReportWithNoExpensesReportUtils(report);
+ const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report);
+ const isInvoiceReport = isInvoiceReportUtils(report);
const shouldShowReportField = !isClosedExpenseReportWithNoExpenses && (isPaidGroupPolicyExpenseReport || isInvoiceReport) && (!isCombinedReport || !isOnlyTitleFieldEnabled);
const renderThreadDivider = useMemo(
() =>
shouldHideThreadDividerLine && !isCombinedReport ? (
) : (
@@ -97,7 +110,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
style={[!shouldHideThreadDividerLine ? styles.reportHorizontalRule : {}]}
/>
),
- [shouldHideThreadDividerLine, report.reportID, styles.reportHorizontalRule, isCombinedReport],
+ [shouldHideThreadDividerLine, report?.reportID, styles.reportHorizontalRule, isCombinedReport],
);
return (
@@ -110,39 +123,34 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
policy?.areReportFieldsEnabled &&
(!isCombinedReport || !isOnlyTitleFieldEnabled) &&
sortedPolicyReportFields.map((reportField) => {
- if (ReportUtils.isReportFieldOfTypeTitle(reportField)) {
+ if (isReportFieldOfTypeTitle(reportField)) {
return null;
}
const fieldValue = reportField.value ?? reportField.defaultValue;
- const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
- const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID);
+ const isFieldDisabled = isReportFieldDisabled(report, reportField, policy);
+ const fieldKey = getReportFieldKey(reportField.fieldID);
- const violation = ReportUtils.getFieldViolation(violations, reportField);
- const violationTranslation = ReportUtils.getFieldViolationTranslation(reportField, violation);
+ const violation = getFieldViolation(violations, reportField);
+ const violationTranslation = getFieldViolationTranslation(reportField, violation);
return (
reportActions.clearReportFieldKeyErrors(report.reportID, fieldKey)}
+ onClose={() => clearReportFieldKeyErrors(report?.reportID, fieldKey)}
>
+ onPress={() => {
Navigation.navigate(
- ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(
- report.reportID,
- report.policyID ?? '-1',
- reportField.fieldID,
- Navigation.getReportRHPActiveRoute(),
- ),
- )
- }
+ ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report?.reportID, report?.policyID, reportField.fieldID, Navigation.getReportRHPActiveRoute()),
+ );
+ }}
shouldShowRightIcon
disabled={isFieldDisabled}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx
index cc64fed1d3e6..2936fddd0376 100644
--- a/src/components/ReportActionItem/MoneyRequestAction.tsx
+++ b/src/components/ReportActionItem/MoneyRequestAction.tsx
@@ -7,7 +7,12 @@ import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {isIOUReportPendingCurrencyConversion} from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {isDeletedParentAction, isReversedTransaction, isSplitBillAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
+import {
+ isDeletedParentAction as isDeletedParentActionReportActionsUtils,
+ isReversedTransaction as isReversedTransactionReportActionsUtils,
+ isSplitBillAction as isSplitBillActionReportActionsUtils,
+ isTrackExpenseAction as isTrackExpenseActionReportActionsUtils,
+} from '@libs/ReportActionsUtils';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
@@ -22,13 +27,13 @@ type MoneyRequestActionProps = {
action: OnyxTypes.ReportAction;
/** The ID of the associated chatReport */
- chatReportID: string;
+ chatReportID: string | undefined;
/** The ID of the associated expense report */
- requestReportID: string;
+ requestReportID: string | undefined;
/** The ID of the current report */
- reportID: string;
+ reportID: string | undefined;
/** Is this IOUACTION the most recent? */
isMostRecentIOUReportAction: boolean;
@@ -65,32 +70,28 @@ function MoneyRequestAction({
isWhisper = false,
shouldDisplayContextMenu = true,
}: MoneyRequestActionProps) {
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`);
+ const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, {canEvict: false});
+
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- const isActionSplitBill = isSplitBillAction(action);
- const isActionTrackExpense = isTrackExpenseAction(action);
- const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID || CONST.DEFAULT_NUMBER_ID}`, {canEvict: false});
- const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`);
- const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`);
+ const isSplitBillAction = isSplitBillActionReportActionsUtils(action);
+ const isTrackExpenseAction = isTrackExpenseActionReportActionsUtils(action);
const onMoneyRequestPreviewPressed = () => {
- if (isActionSplitBill) {
- const reportActionID = action.reportActionID;
- Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, reportActionID, Navigation.getReportRHPActiveRoute()));
+ if (isSplitBillAction) {
+ Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, action.reportActionID, Navigation.getReportRHPActiveRoute()));
return;
}
- const childReportID = action?.childReportID;
- if (!childReportID) {
- return;
- }
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(action?.childReportID));
};
let shouldShowPendingConversionMessage = false;
- const isParentActionDeleted = isDeletedParentAction(action);
- const isTransactionReveresed = isReversedTransaction(action);
+ const isDeletedParentAction = isDeletedParentActionReportActionsUtils(action);
+ const isReversedTransaction = isReversedTransactionReportActionsUtils(action);
if (
!isEmptyObject(iouReport) &&
!isEmptyObject(reportActions) &&
@@ -102,22 +103,22 @@ function MoneyRequestAction({
shouldShowPendingConversionMessage = isIOUReportPendingCurrencyConversion(iouReport);
}
- if (isParentActionDeleted || isTransactionReveresed) {
+ if (isDeletedParentAction || isReversedTransaction) {
let message: TranslationPaths;
- if (isTransactionReveresed) {
+ if (isReversedTransaction) {
message = 'parentReportAction.reversedTransaction';
} else {
message = 'parentReportAction.deletedExpense';
}
- return ${translate(message)}`} />;
+ return ${translate(message)}`} />;
}
return (
>();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
- const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
const [session] = useOnyx(ONYXKEYS.SESSION);
- const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || CONST.DEFAULT_NUMBER_ID}`);
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
const policy = usePolicy(iouReport?.policyID);
const isMoneyRequestAction = isMoneyRequestActionReportActionsUtils(action);
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx
index f902948b2cb5..0b9d4e5f5629 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx
@@ -6,7 +6,7 @@ import MoneyRequestPreviewContent from './MoneyRequestPreviewContent';
import type {MoneyRequestPreviewProps} from './types';
function MoneyRequestPreview(props: MoneyRequestPreviewProps) {
- const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${props.iouReportID || '-1'}`);
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${props.iouReportID}`);
// We should not render the component if there is no iouReport and it's not a split or track expense.
// Moved outside of the component scope to allow for easier use of hooks in the main component.
// eslint-disable-next-line react/jsx-props-no-spreading
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts
index c40b45c6d2bd..186c81a8c866 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts
+++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts
@@ -7,13 +7,13 @@ type MoneyRequestPreviewProps = {
/** The active IOUReport, used for Onyx subscription */
// The iouReportID is used inside withOnyx HOC
// eslint-disable-next-line react/no-unused-prop-types
- iouReportID: string;
+ iouReportID: string | undefined;
/** The associated chatReport */
- chatReportID: string;
+ chatReportID: string | undefined;
/** The ID of the current report */
- reportID: string;
+ reportID: string | undefined;
/** Callback for the preview pressed */
onPreviewPressed: (event?: GestureResponderEvent | KeyboardEvent) => void;
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index ae40e8ceed79..4616feeffbca 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -96,13 +96,13 @@ type ReportPreviewProps = {
action: ReportAction;
/** The associated chatReport */
- chatReportID: string;
+ chatReportID: string | undefined;
/** The active IOUReport, used for Onyx subscription */
iouReportID: string | undefined;
/** The report's policyID, used for Onyx subscription */
- policyID: string;
+ policyID: string | undefined;
/** Extra styles to pass to View wrapper */
containerStyles?: StyleProp;
diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx
index 91e9cdbbc9c1..3eca7561eb04 100644
--- a/src/components/ReportActionItem/TaskPreview.tsx
+++ b/src/components/ReportActionItem/TaskPreview.tsx
@@ -38,9 +38,9 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & {
/** The ID of the associated policy */
// eslint-disable-next-line react/no-unused-prop-types
- policyID: string;
+ policyID: string | undefined;
/** The ID of the associated taskReport */
- taskReportID: string;
+ taskReportID: string | undefined;
/** Whether the task preview is hovered so we can modify its style */
isHovered: boolean;
@@ -49,7 +49,7 @@ type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & {
action: OnyxEntry;
/** The chat report associated with taskReport */
- chatReportID: string;
+ chatReportID: string | undefined;
/** Popover context menu anchor, used for showing context menu */
contextMenuAnchor: ContextMenuAnchor;
diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx
index 7901426b33e0..3e077c2bda4a 100644
--- a/src/components/ReportActionItem/TaskView.tsx
+++ b/src/components/ReportActionItem/TaskView.tsx
@@ -1,5 +1,6 @@
import React, {useEffect} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import Checkbox from '@components/Checkbox';
import Hoverable from '@components/Hoverable';
import Icon from '@components/Icon';
@@ -17,18 +18,18 @@ import useThemeStyles from '@hooks/useThemeStyles';
import convertToLTR from '@libs/convertToLTR';
import getButtonState from '@libs/getButtonState';
import Navigation from '@libs/Navigation/Navigation';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import * as TaskUtils from '@libs/TaskUtils';
-import * as Session from '@userActions/Session';
-import * as Task from '@userActions/Task';
+import {getAvatarsForAccountIDs, getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils';
+import {getDisplayNameForParticipant, getDisplayNamesWithTooltips, isCompletedTaskReport, isOpenTaskReport} from '@libs/ReportUtils';
+import {isActiveTaskEditRoute} from '@libs/TaskUtils';
+import {checkIfActionIsAllowed} from '@userActions/Session';
+import {canActionTask as canActionTaskUtil, canModifyTask as canModifyTaskUtil, clearTaskErrors, completeTask, reopenTask, setTaskReport} from '@userActions/Task';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Report} from '@src/types/onyx';
type TaskViewProps = {
/** The report currently being looked at */
- report: Report;
+ report: OnyxEntry;
};
function TaskView({report}: TaskViewProps) {
@@ -37,17 +38,14 @@ function TaskView({report}: TaskViewProps) {
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const personalDetails = usePersonalDetails();
useEffect(() => {
- Task.setTaskReport(report);
+ setTaskReport(report);
}, [report]);
- const taskTitle = convertToLTR(report.reportName ?? '');
- const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(
- OptionsListUtils.getPersonalDetailsForAccountIDs(report.managerID ? [report.managerID] : [], personalDetails),
- false,
- );
- const isOpen = ReportUtils.isOpenTaskReport(report);
- const isCompleted = ReportUtils.isCompletedTaskReport(report);
- const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID);
- const canActionTask = Task.canActionTask(report, currentUserPersonalDetails.accountID);
+ const taskTitle = convertToLTR(report?.reportName ?? '');
+ const assigneeTooltipDetails = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(report?.managerID ? [report?.managerID] : [], personalDetails), false);
+ const isOpen = isOpenTaskReport(report);
+ const isCompleted = isCompletedTaskReport(report);
+ const canModifyTask = canModifyTaskUtil(report, currentUserPersonalDetails.accountID);
+ const canActionTask = canActionTaskUtil(report, currentUserPersonalDetails.accountID);
const disableState = !canModifyTask;
const isDisableInteractive = !canModifyTask || !isOpen;
const {translate} = useLocalize();
@@ -56,14 +54,14 @@ function TaskView({report}: TaskViewProps) {
Task.clearTaskErrors(report.reportID)}
+ errors={report?.errorFields?.editTask ?? report?.errorFields?.createTask}
+ onClose={() => clearTaskErrors(report?.reportID)}
errorRowStyles={styles.ph5}
>
{(hovered) => (
{
+ onPress={checkIfActionIsAllowed((e) => {
if (isDisableInteractive) {
return;
}
@@ -71,7 +69,7 @@ function TaskView({report}: TaskViewProps) {
(e.currentTarget as HTMLElement).blur();
}
- Navigation.navigate(ROUTES.TASK_TITLE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()));
+ Navigation.navigate(ROUTES.TASK_TITLE.getRoute(report?.reportID, Navigation.getReportRHPActiveRoute()));
})}
style={({pressed}) => [
styles.ph5,
@@ -83,19 +81,19 @@ function TaskView({report}: TaskViewProps) {
disabled={isDisableInteractive}
>
{({pressed}) => (
-
+ {translate('task.title')} {
+ onPress={checkIfActionIsAllowed(() => {
// If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page.
- if (TaskUtils.isActiveTaskEditRoute(report.reportID)) {
+ if (isActiveTaskEditRoute(report?.reportID)) {
return;
}
if (isCompleted) {
- Task.reopenTask(report);
+ reopenTask(report);
} else {
- Task.completeTask(report);
+ completeTask(report);
}
})}
isChecked={isCompleted}
@@ -129,12 +127,12 @@ function TaskView({report}: TaskViewProps) {
)}
-
+ Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))}
+ title={report?.description ?? ''}
+ onPress={() => Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID, Navigation.getReportRHPActiveRoute()))}
shouldShowRightIcon={!isDisableInteractive}
disabled={disableState}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
@@ -144,16 +142,16 @@ function TaskView({report}: TaskViewProps) {
shouldUseDefaultCursorWhenDisabled
/>
-
- {report.managerID ? (
+
+ {report?.managerID ? (
@@ -105,7 +102,7 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans
}
return (
-
+
+
{!isEmptyObject(transactionThreadReport?.reportID) ? (
<>
@@ -176,7 +173,7 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans
)}
@@ -186,8 +183,8 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans
return (
);
}
diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx
index 1adb24fa23a7..b5584c00087b 100644
--- a/src/pages/home/report/ReportActionItemCreated.tsx
+++ b/src/pages/home/report/ReportActionItemCreated.tsx
@@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as ReportUtils from '@libs/ReportUtils';
+import {getIcons, isChatReport, isCurrentUserInvoiceReceiver, isInvoiceRoom, navigateToDetailsPage, shouldDisableDetailPage as shouldDisableDetailPageReportUtils} from '@libs/ReportUtils';
import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -17,7 +17,7 @@ import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground';
type ReportActionItemCreatedProps = {
/** The id of the report */
- reportID: string;
+ reportID: string | undefined;
/** The id of the policy */
// eslint-disable-next-line react/no-unused-prop-types
@@ -31,16 +31,16 @@ function ReportActionItemCreated({reportID, policyID}: ReportActionItemCreatedPr
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
- const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : -1}`);
+ const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : undefined}`);
- if (!ReportUtils.isChatReport(report)) {
+ if (!isChatReport(report)) {
return null;
}
- let icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy);
- const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(report);
+ let icons = getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy);
+ const shouldDisableDetailPage = shouldDisableDetailPageReportUtils(report);
- if (ReportUtils.isInvoiceRoom(report) && ReportUtils.isCurrentUserInvoiceReceiver(report)) {
+ if (isInvoiceRoom(report) && isCurrentUserInvoiceReceiver(report)) {
icons = [...icons].reverse();
}
@@ -59,7 +59,7 @@ function ReportActionItemCreated({reportID, policyID}: ReportActionItemCreatedPr
>
ReportUtils.navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute())}
+ onPress={() => navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute())}
style={[styles.mh5, styles.mb3, styles.alignSelfStart, shouldDisableDetailPage && styles.cursorDefault]}
accessibilityLabel={translate('common.details')}
role={CONST.ROLE.BUTTON}
diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx
index 647c17f70d88..550f7d81cdf3 100644
--- a/src/pages/home/report/ReportActionItemMessage.tsx
+++ b/src/pages/home/report/ReportActionItemMessage.tsx
@@ -8,8 +8,20 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportUtils from '@libs/ReportUtils';
+import {
+ getLinkedTransactionID,
+ getMemberChangeMessageFragment,
+ getOriginalMessage,
+ getReportActionMessage,
+ getReportActionMessageFragments,
+ getUpdateRoomDescriptionFragment,
+ isAddCommentAction,
+ isApprovedOrSubmittedReportAction as isApprovedOrSubmittedReportActionUtils,
+ isMemberChangeAction,
+ isMoneyRequestAction,
+ isThreadParentMessage,
+} from '@libs/ReportActionsUtils';
+import {getIOUReportActionDisplayMessage, hasMissingInvoiceBankAccount, isSettled} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -31,20 +43,20 @@ type ReportActionItemMessageProps = {
isHidden?: boolean;
/** The ID of the report */
- reportID: string;
+ reportID: string | undefined;
};
function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHidden = false}: ReportActionItemMessageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
- const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${ReportActionsUtils.getLinkedTransactionID(action) ?? -1}`);
+ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getLinkedTransactionID(action)}`);
- const fragments = ReportActionsUtils.getReportActionMessageFragments(action);
- const isIOUReport = ReportActionsUtils.isMoneyRequestAction(action);
+ const fragments = getReportActionMessageFragments(action);
+ const isIOUReport = isMoneyRequestAction(action);
- if (ReportActionsUtils.isMemberChangeAction(action)) {
- const fragment = ReportActionsUtils.getMemberChangeMessageFragment(action);
+ if (isMemberChangeAction(action)) {
+ const fragment = getMemberChangeMessageFragment(action);
return (
@@ -60,7 +72,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid
}
if (action.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) {
- const fragment = ReportActionsUtils.getUpdateRoomDescriptionFragment(action);
+ const fragment = getUpdateRoomDescriptionFragment(action);
return (
type === action.actionName);
@@ -99,11 +111,11 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid
key={`actionFragment-${action.reportActionID}-${index}`}
fragment={fragment}
iouMessage={iouMessage}
- isThreadParentMessage={ReportActionsUtils.isThreadParentMessage(action, reportID)}
+ isThreadParentMessage={isThreadParentMessage(action, reportID)}
pendingAction={action.pendingAction}
actionName={action.actionName}
- source={ReportActionsUtils.isAddCommentAction(action) ? ReportActionsUtils.getOriginalMessage(action)?.source : ''}
- accountID={action.actorAccountID ?? -1}
+ source={isAddCommentAction(action) ? getOriginalMessage(action)?.source : ''}
+ accountID={action.actorAccountID ?? CONST.DEFAULT_NUMBER_ID}
style={style}
displayAsGroup={displayAsGroup}
isApprovedOrSubmittedReportAction={isApprovedOrSubmittedReportAction}
@@ -113,7 +125,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid
// to decide if the fragment should be from left to right for RTL display names e.g. Arabic for proper
// formatting.
isFragmentContainingDisplayName={index === 0}
- moderationDecision={ReportActionsUtils.getReportActionMessage(action)?.moderationDecision?.decision}
+ moderationDecision={getReportActionMessage(action)?.moderationDecision?.decision}
/>
));
@@ -132,7 +144,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid
Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID));
};
- const shouldShowAddBankAccountButton = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && ReportUtils.hasMissingInvoiceBankAccount(reportID) && !ReportUtils.isSettled(reportID);
+ const shouldShowAddBankAccountButton = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && hasMissingInvoiceBankAccount(reportID) && !isSettled(reportID);
return (
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx
index 1862e2b96596..628c50a8a5a5 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx
@@ -60,7 +60,7 @@ type ReportActionItemMessageEditProps = {
draftMessage: string;
/** ReportID that holds the comment we're editing */
- reportID: string;
+ reportID: string | undefined;
/** PolicyID of the policy the report belongs to */
policyID?: string;
diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx
index a3cef660cf1a..edbd70349127 100644
--- a/src/pages/home/report/ReportActionsList.tsx
+++ b/src/pages/home/report/ReportActionsList.tsx
@@ -190,7 +190,6 @@ function ReportActionsList({
const lastMessageTime = useRef(null);
const [isVisible, setIsVisible] = useState(Visibility.isVisible);
const isFocused = useIsFocused();
- const [pendingBottomScroll, setPendingBottomScroll] = useState(false);
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`);
const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID});
@@ -456,48 +455,24 @@ function ReportActionsList({
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
- const isNewMessageDisplayed = useMemo(() => {
- const prevActions = Object.values(prevSortedVisibleReportActionsObjects);
- const lastPrevVisibleAction = prevActions.at(0);
- return lastAction?.reportActionID !== lastPrevVisibleAction?.reportActionID;
- }, [prevSortedVisibleReportActionsObjects, lastAction]);
-
const scrollToBottomForCurrentUserAction = useCallback(
(isFromCurrentUser: boolean) => {
- // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where
- // they are now in the list.
- if (!isFromCurrentUser || scrollingVerticalOffset.current === 0 || !isReportScreenTopmostCentralPane()) {
- return;
- }
- if (!hasNewestReportActionRef.current) {
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID));
- return;
- }
- if (!isNewMessageDisplayed) {
- setPendingBottomScroll(true);
- } else {
- InteractionManager.runAfterInteractions(() => {
- reportScrollManager.scrollToBottom();
- });
- }
- },
- [reportScrollManager, report.reportID, isNewMessageDisplayed],
- );
-
- useEffect(() => {
- if (!pendingBottomScroll || scrollingVerticalOffset.current === 0) {
- return;
- }
-
- if (isNewMessageDisplayed) {
InteractionManager.runAfterInteractions(() => {
setIsFloatingMessageCounterVisible(false);
+ // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where
+ // they are now in the list.
+ if (!isFromCurrentUser || !isReportScreenTopmostCentralPane()) {
+ return;
+ }
+ if (!hasNewestReportActionRef.current) {
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID));
+ return;
+ }
reportScrollManager.scrollToBottom();
- setPendingBottomScroll(false);
});
- }
- }, [pendingBottomScroll, reportScrollManager, isNewMessageDisplayed]);
-
+ },
+ [reportScrollManager, report.reportID],
+ );
useEffect(() => {
// Why are we doing this, when in the cleanup of the useEffect we are already calling the unsubscribe function?
// Answer: On web, when navigating to another report screen, the previous report screen doesn't get unmounted,
diff --git a/src/pages/iou/SplitBillDetailsPage.tsx b/src/pages/iou/SplitBillDetailsPage.tsx
index 13d841cc5441..b31fe7f71a6d 100644
--- a/src/pages/iou/SplitBillDetailsPage.tsx
+++ b/src/pages/iou/SplitBillDetailsPage.tsx
@@ -5,23 +5,25 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import {ImageBehaviorContextProvider} from '@components/Image/ImageBehaviorContextProvider';
import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList';
import MoneyRequestHeaderStatusBar from '@components/MoneyRequestHeaderStatusBar';
import ScreenWrapper from '@components/ScreenWrapper';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import {completeSplitBill, setDraftSplitTransaction} from '@libs/actions/IOU';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SplitDetailsNavigatorParamList} from '@libs/Navigation/types';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import * as TransactionUtils from '@libs/TransactionUtils';
+import {getParticipantsOption, getPolicyExpenseReportOption} from '@libs/OptionsListUtils';
+import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {getTransactionDetails, isPolicyExpenseChat} from '@libs/ReportUtils';
+import type {OptionData} from '@libs/ReportUtils';
+import {areRequiredFieldsEmpty, hasReceipt, isDistanceRequest, isReceiptBeingScanned} from '@libs/TransactionUtils';
import withReportAndReportActionOrNotFound from '@pages/home/report/withReportAndReportActionOrNotFound';
import type {WithReportAndReportActionOrNotFoundProps} from '@pages/home/report/withReportAndReportActionOrNotFound';
import variables from '@styles/variables';
-import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
@@ -35,34 +37,34 @@ function SplitBillDetailsPage({route, report, reportAction}: SplitBillDetailsPag
const {translate} = useLocalize();
const theme = useTheme();
- const reportID = report?.reportID ?? '-1';
- const originalMessage = reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction) ? ReportActionsUtils.getOriginalMessage(reportAction) : undefined;
- const IOUTransactionID = originalMessage?.IOUTransactionID ? originalMessage.IOUTransactionID : '-1';
+ const reportID = report?.reportID;
+ const originalMessage = reportAction && isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction) : undefined;
+ const IOUTransactionID = originalMessage?.IOUTransactionID;
const participantAccountIDs = originalMessage?.participantAccountIDs ?? [];
- const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${IOUTransactionID}`);
- const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${IOUTransactionID}`);
+ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID}`);
+ const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID}`);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [session] = useOnyx(ONYXKEYS.SESSION);
// In case this is workspace split expense, we manually add the workspace as the second participant of the split expense
// because we don't save any accountID in the report action's originalMessage other than the payee's accountID
- let participants: Array;
- if (ReportUtils.isPolicyExpenseChat(report)) {
+ let participants: Array;
+ if (isPolicyExpenseChat(report)) {
participants = [
- OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs.at(0), selected: true, reportID: ''}, personalDetails),
- OptionsListUtils.getPolicyExpenseReportOption({...report, selected: true, reportID}),
+ getParticipantsOption({accountID: participantAccountIDs.at(0), selected: true, reportID: ''}, personalDetails),
+ getPolicyExpenseReportOption({...report, selected: true, reportID}),
];
} else {
- participants = participantAccountIDs.map((accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true, reportID: ''}, personalDetails));
+ participants = participantAccountIDs.map((accountID) => getParticipantsOption({accountID, selected: true, reportID: ''}, personalDetails));
}
- const actorAccountID = reportAction?.actorAccountID ?? -1;
+ const actorAccountID = reportAction?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
const payeePersonalDetails = personalDetails?.[actorAccountID];
const participantsExcludingPayee = participants.filter((participant) => participant.accountID !== reportAction?.actorAccountID);
- const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
- const hasSmartScanFailed = TransactionUtils.hasReceipt(transaction) && transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.SCANFAILED;
- const isEditingSplitBill = session?.accountID === actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction);
+ const isScanning = hasReceipt(transaction) && isReceiptBeingScanned(transaction);
+ const hasSmartScanFailed = hasReceipt(transaction) && transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.SCANFAILED;
+ const isEditingSplitBill = session?.accountID === actorAccountID && areRequiredFieldsEmpty(transaction);
const [isConfirmed, setIsConfirmed] = useState(false);
const {
@@ -73,11 +75,11 @@ function SplitBillDetailsPage({route, report, reportAction}: SplitBillDetailsPag
created: splitCreated,
category: splitCategory,
billable: splitBillable,
- } = ReportUtils.getTransactionDetails(isEditingSplitBill && draftTransaction ? draftTransaction : transaction) ?? {};
+ } = getTransactionDetails(isEditingSplitBill && draftTransaction ? draftTransaction : transaction) ?? {};
const onConfirm = useCallback(() => {
setIsConfirmed(true);
- IOU.completeSplitBill(reportID, reportAction, draftTransaction, session?.accountID ?? -1, session?.email ?? '');
+ completeSplitBill(reportID, reportAction, draftTransaction, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email);
}, [reportID, reportAction, draftTransaction, session?.accountID, session?.email]);
return (
@@ -104,38 +106,40 @@ function SplitBillDetailsPage({route, report, reportAction}: SplitBillDetailsPag
/>
)}
- {!!participants.length && (
- {
- IOU.setDraftSplitTransaction(transaction?.transactionID ?? '-1', {billable});
- }}
- isConfirmed={isConfirmed}
- />
- )}
+
+ {!!participants.length && (
+ {
+ setDraftSplitTransaction(transaction?.transactionID, {billable});
+ }}
+ isConfirmed={isConfirmed}
+ />
+ )}
+
diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
index b9da0147b525..db33902c2d88 100644
--- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
@@ -7,50 +7,42 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import * as IOUUtils from '@libs/IOUUtils';
+import {openWorkspace} from '@libs/actions/Policy/Policy';
+import {isValidMoneyRequestType} from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
-import * as ReportUtils from '@libs/ReportUtils';
+import {canSendInvoice, isControlPolicy, isPaidGroupPolicy, isPolicyAccessible, isPolicyAdmin, isPolicyFeatureEnabled as isPolicyFeatureEnabledUtil} from '@libs/PolicyUtils';
+import {canCreateRequest} from '@libs/ReportUtils';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
-import * as Policy from '@userActions/Policy/Policy';
import type {IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
-import type * as OnyxTypes from '@src/types/onyx';
+import type {Report} from '@src/types/onyx';
import type {PolicyFeatureName} from '@src/types/onyx/Policy';
+import type Policy from '@src/types/onyx/Policy';
import callOrReturn from '@src/types/utils/callOrReturn';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
const ACCESS_VARIANTS = {
- [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => PolicyUtils.isPaidGroupPolicy(policy),
- [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => PolicyUtils.isControlPolicy(policy),
- [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => PolicyUtils.isPolicyAdmin(policy, login),
- [CONST.IOU.ACCESS_VARIANTS.CREATE]: (
- policy: OnyxEntry,
- login: string,
- report: OnyxEntry,
- allPolicies: NonNullable> | null,
- iouType?: IOUType,
- ) =>
+ [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => isPaidGroupPolicy(policy),
+ [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => isControlPolicy(policy),
+ [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => isPolicyAdmin(policy, login),
+ [CONST.IOU.ACCESS_VARIANTS.CREATE]: (policy: OnyxEntry, login: string, report: OnyxEntry, allPolicies: NonNullable> | null, iouType?: IOUType) =>
!!iouType &&
- IOUUtils.isValidMoneyRequestType(iouType) &&
+ isValidMoneyRequestType(iouType) &&
// Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the expense
- (isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType)) &&
- (iouType !== CONST.IOU.TYPE.INVOICE || PolicyUtils.canSendInvoice(allPolicies, login)),
-} as const satisfies Record<
- string,
- (policy: OnyxTypes.Policy, login: string, report: OnyxTypes.Report, allPolicies: NonNullable> | null, iouType?: IOUType) => boolean
->;
+ (isEmptyObject(report?.reportID) || canCreateRequest(report, policy, iouType)) &&
+ (iouType !== CONST.IOU.TYPE.INVOICE || canSendInvoice(allPolicies, login)),
+} as const satisfies Record> | null, iouType?: IOUType) => boolean>;
type AccessVariant = keyof typeof ACCESS_VARIANTS;
type AccessOrNotFoundWrapperChildrenProps = {
/** The report that holds the transaction */
- report: OnyxEntry;
+ report: OnyxEntry;
/** The report currently being looked at */
- policy: OnyxEntry;
+ policy: OnyxEntry;
/** Indicated whether the report data is loading */
isLoadingReportData: OnyxEntry;
@@ -82,7 +74,7 @@ type AccessOrNotFoundWrapperProps = {
iouType?: IOUType;
/** The list of all policies */
- allPolicies?: OnyxCollection;
+ allPolicies?: OnyxCollection;
} & Pick;
type PageNotFoundFallbackProps = Pick & {
@@ -97,6 +89,7 @@ function PageNotFoundFallback({policyID, fullPageNotFoundViewProps, isFeatureEna
return (
{
if (isPolicyNotAccessible) {
Navigation.dismissModal();
@@ -127,8 +120,8 @@ function AccessOrNotFoundWrapper({
const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true});
const {login = ''} = useCurrentUserPersonalDetails();
const isPolicyIDInRoute = !!policyID?.length;
- const isMoneyRequest = !!iouType && IOUUtils.isValidMoneyRequestType(iouType);
- const isFromGlobalCreate = isEmptyObject(report?.reportID);
+ const isMoneyRequest = !!iouType && isValidMoneyRequestType(iouType);
+ const isFromGlobalCreate = !!reportID && isEmptyObject(report?.reportID);
const pendingField = featureName ? policy?.pendingFields?.[featureName] : undefined;
useEffect(() => {
@@ -137,13 +130,13 @@ function AccessOrNotFoundWrapper({
return;
}
- Policy.openWorkspace(policyID, []);
+ openWorkspace(policyID, []);
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isPolicyIDInRoute, policyID]);
const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id);
- const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true;
+ const isFeatureEnabled = featureName ? isPolicyFeatureEnabledUtil(policy, featureName) : true;
const [isPolicyFeatureEnabled, setIsPolicyFeatureEnabled] = useState(isFeatureEnabled);
const {isOffline} = useNetwork();
@@ -153,9 +146,8 @@ function AccessOrNotFoundWrapper({
return acc && accessFunction(policy, login, report, allPolicies ?? null, iouType);
}, true);
- const isPolicyNotAccessible = !PolicyUtils.isPolicyAccessible(policy);
+ const isPolicyNotAccessible = !isPolicyAccessible(policy);
const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isPolicyFeatureEnabled || shouldBeBlocked;
-
// We only update the feature state if it isn't pending.
// This is because the feature state changes several times during the creation of a workspace, while we are waiting for a response from the backend.
// Without this, we can have unexpectedly have 'Not Found' be shown.
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 2a9b77551c0f..af7d71b70d92 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -21,17 +21,29 @@ import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import {isConnectionInProgress} from '@libs/actions/connections';
-import * as CardUtils from '@libs/CardUtils';
-import * as CurrencyUtils from '@libs/CurrencyUtils';
+import {clearErrors, openPolicyInitialPage, removeWorkspace, updateGeneralSettings} from '@libs/actions/Policy/Policy';
+import {navigateToBankAccountRoute} from '@libs/actions/ReimbursementAccount';
+import {convertToDisplayString} from '@libs/CurrencyUtils';
import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
-import * as PolicyUtils from '@libs/PolicyUtils';
+import {
+ shouldShowPolicy as checkIfShouldShowPolicy,
+ getWorkspaceAccountID,
+ goBackFromInvalidPolicy,
+ hasPolicyCategoriesError,
+ isPaidGroupPolicy,
+ isPendingDeletePolicy,
+ isPolicyAdmin,
+ isPolicyFeatureEnabled,
+ shouldShowEmployeeListError,
+ shouldShowSyncError,
+ shouldShowTaxRateError,
+} from '@libs/PolicyUtils';
import {getDefaultWorkspaceAvatar, getIcons, getPolicyExpenseChat, getReportName, getReportOfflinePendingActionAndErrors} from '@libs/ReportUtils';
import type {FullScreenNavigatorParamList} from '@navigation/types';
-import * as App from '@userActions/App';
-import * as Policy from '@userActions/Policy/Policy';
-import * as ReimbursementAccount from '@userActions/ReimbursementAccount';
+import {confirmReadyToOpenApp} from '@userActions/App';
+import {checkIfFeedConnectionIsBroken, flatAllCardsList} from '@userActions/CompanyCards';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -74,22 +86,24 @@ type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & PlatformS
type PolicyFeatureStates = Record;
-function dismissError(policyID: string, pendingAction: PendingAction | undefined) {
+function dismissError(policyID: string | undefined, pendingAction: PendingAction | undefined) {
if (!policyID || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) {
- PolicyUtils.goBackFromInvalidPolicy();
- Policy.removeWorkspace(policyID);
+ goBackFromInvalidPolicy();
+ if (policyID) {
+ removeWorkspace(policyID);
+ }
} else {
- Policy.clearErrors(policyID);
+ clearErrors(policyID);
}
}
function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: WorkspaceInitialPageProps) {
const styles = useThemeStyles();
const policy = policyDraft?.id ? policyDraft : policyProp;
- const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id);
+ const workspaceAccountID = getWorkspaceAccountID(policy?.id);
const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false);
const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors));
- const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
+ const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`);
const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`);
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`);
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`);
@@ -97,7 +111,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params?.policyID}`);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const {login, accountID} = useCurrentUserPersonalDetails();
- const hasSyncError = PolicyUtils.shouldShowSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy));
+ const hasSyncError = shouldShowSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy));
const waitForNavigate = useWaitForNavigation();
const {singleExecution, isExecuting} = useSingleExecution();
const activeRoute = useNavigationState(getTopmostRouteName);
@@ -131,7 +145,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
if (policyDraft?.id) {
return;
}
- Policy.openPolicyInitialPage(route.params.policyID);
+ openPolicyInitialPage(route.params.policyID);
}, [policyDraft?.id, route.params.policyID]);
useNetwork({onReconnect: fetchPolicyData});
@@ -142,7 +156,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
}, [fetchPolicyData]),
);
- const policyID = `${policy?.id ?? CONST.DEFAULT_NUMBER_ID}`;
+ const policyID = policy?.id;
const policyName = policy?.name ?? '';
useEffect(() => {
if (!isCurrencyModalOpen || policy?.outputCurrency !== CONST.CURRENCY.USD) {
@@ -153,20 +167,19 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
/** Call update workspace currency and hide the modal */
const confirmCurrencyChangeAndHideModal = useCallback(() => {
- Policy.updateGeneralSettings(policyID, policyName, CONST.CURRENCY.USD);
+ updateGeneralSettings(policyID, policyName, CONST.CURRENCY.USD);
setIsCurrencyModalOpen(false);
- ReimbursementAccount.navigateToBankAccountRoute(policyID);
+ navigateToBankAccountRoute(policyID);
}, [policyID, policyName]);
- const hasMembersError = PolicyUtils.shouldShowEmployeeListError(policy);
- const hasPolicyCategoryError = PolicyUtils.hasPolicyCategoriesError(policyCategories);
+ const hasMembersError = shouldShowEmployeeListError(policy);
+ const hasPolicyCategoryError = hasPolicyCategoriesError(policyCategories);
const hasGeneralSettingsError =
!isEmptyObject(policy?.errorFields?.name ?? {}) ||
!isEmptyObject(policy?.errorFields?.avatarURL ?? {}) ||
!isEmptyObject(policy?.errorFields?.ouputCurrency ?? {}) ||
!isEmptyObject(policy?.errorFields?.address ?? {});
- const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy, login);
- const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy);
+ const shouldShowProtectedItems = isPolicyAdmin(policy, login);
const [featureStates, setFeatureStates] = useState(policyFeatureStates);
const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = [];
@@ -177,7 +190,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
setFeatureStates((currentFeatureStates) => {
const newFeatureStates = {} as PolicyFeatureStates;
(Object.keys(policy?.pendingFields ?? {}) as PolicyFeatureName[]).forEach((key) => {
- const isFeatureEnabled = PolicyUtils.isPolicyFeatureEnabled(policy, key);
+ const isFeatureEnabled = isPolicyFeatureEnabled(policy, key);
newFeatureStates[key] =
prevPendingFields?.[key] !== policy?.pendingFields?.[key] || isOffline || !policy?.pendingFields?.[key] ? isFeatureEnabled : currentFeatureStates[key];
});
@@ -189,7 +202,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
}, [policy, isOffline, policyFeatureStates, prevPendingFields]);
useEffect(() => {
- App.confirmReadyToOpenApp();
+ confirmReadyToOpenApp();
}, []);
if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED]) {
@@ -212,14 +225,14 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
}
if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED]) {
- const hasPolicyFeedsError = PolicyUtils.hasPolicyFeedsError(CardUtils.getCompanyFeeds(cardFeeds));
+ const hasBrokenFeedConnection = checkIfFeedConnectionIsBroken(flatAllCardsList(allFeedsCards, workspaceAccountID));
protectedCollectPolicyMenuItems.push({
translationKey: 'workspace.common.companyCards',
icon: Expensicons.CreditCard,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)))),
routeName: SCREENS.WORKSPACE.COMPANY_CARDS,
- brickRoadIndicator: hasPolicyFeedsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
+ brickRoadIndicator: hasBrokenFeedConnection ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
});
}
@@ -259,7 +272,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
icon: Expensicons.InvoiceGeneric,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)))),
routeName: SCREENS.WORKSPACE.INVOICES,
- badgeText: CurrencyUtils.convertToDisplayString(policy?.invoice?.bankAccount?.stripeConnectAccountBalance ?? 0, currencyCode),
+ badgeText: convertToDisplayString(policy?.invoice?.bankAccount?.stripeConnectAccountBalance ?? 0, currencyCode),
});
}
@@ -288,7 +301,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
icon: Expensicons.Coins,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)))),
routeName: SCREENS.WORKSPACE.TAXES,
- brickRoadIndicator: PolicyUtils.shouldShowTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
+ brickRoadIndicator: shouldShowTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
});
}
@@ -333,24 +346,24 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
routeName: SCREENS.WORKSPACE.MEMBERS,
},
- ...(isPaidGroupPolicy && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []),
+ ...(isPaidGroupPolicy(policy) && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []),
];
const prevPolicy = usePrevious(policy);
const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems);
const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName));
- const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]);
- const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]);
+ const shouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(policy, false, currentUserLogin), [policy, currentUserLogin]);
+ const prevShouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(prevPolicy, false, currentUserLogin), [prevPolicy, currentUserLogin]);
// We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy);
useEffect(() => {
- if (isEmptyObject(prevPolicy) || PolicyUtils.isPendingDeletePolicy(prevPolicy) || !PolicyUtils.isPendingDeletePolicy(policy)) {
+ if (isEmptyObject(prevPolicy) || isPendingDeletePolicy(prevPolicy) || !isPendingDeletePolicy(policy)) {
return;
}
- PolicyUtils.goBackFromInvalidPolicy();
+ goBackFromInvalidPolicy();
}, [policy, prevPolicy]);
// We are checking if the user can access the route.
@@ -450,9 +463,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
title={getReportName(currentUserPolicyExpenseChat)}
description={translate('workspace.common.workspace')}
icon={getIcons(currentUserPolicyExpenseChat, personalDetails)}
- onPress={() =>
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(`${currentUserPolicyExpenseChat?.reportID ?? CONST.DEFAULT_NUMBER_ID}`), CONST.NAVIGATION.TYPE.UP)
- }
+ onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(currentUserPolicyExpenseChat?.reportID), CONST.NAVIGATION.TYPE.UP)}
shouldShowRightIcon
wrapperStyle={[styles.br2, styles.pl2, styles.pr0, styles.pv3, styles.mt1, styles.alignItemsCenter]}
shouldShowSubscriptAvatar
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index 26175c9793d9..8867e32fa8ef 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -10,14 +10,13 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/types';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollViewWithContext from '@components/ScrollViewWithContext';
-import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
+import {openWorkspaceView} from '@libs/actions/BankAccounts';
import BankAccount from '@libs/models/BankAccount';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
-import * as BankAccounts from '@userActions/BankAccounts';
+import {isPolicyAdmin, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
@@ -92,7 +91,7 @@ function fetchData(policyID: string, skipVBBACal?: boolean) {
return;
}
- BankAccounts.openWorkspaceView(policyID);
+ openWorkspaceView(policyID);
}
function WorkspacePageWithSections({
@@ -123,8 +122,7 @@ function WorkspacePageWithSections({
threeDotsAnchorPosition,
}: WorkspacePageWithSectionsProps) {
const styles = useThemeStyles();
- const policyID = route.params?.policyID ?? '-1';
- const {isOffline} = useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)});
+ const policyID = route.params?.policyID ?? `${CONST.DEFAULT_NUMBER_ID}`;
const [user] = useOnyx(ONYXKEYS.USER);
const [reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
@@ -132,7 +130,7 @@ function WorkspacePageWithSections({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true;
- const achState = reimbursementAccount?.achData?.state ?? '-1';
+ const achState = reimbursementAccount?.achData?.state;
const isUsingECard = user?.isUsingExpensifyCard ?? false;
const hasVBA = achState === BankAccount.STATE.OPEN;
const content = typeof children === 'function' ? children(hasVBA, policyID, isUsingECard) : children;
@@ -151,8 +149,8 @@ function WorkspacePageWithSections({
}, [policyID, shouldSkipVBBACall]),
);
- const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]);
- const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]);
+ const shouldShowPolicy = useMemo(() => shouldShowPolicyUtil(policy, false, currentUserLogin), [policy, currentUserLogin]);
+ const prevShouldShowPolicy = useMemo(() => shouldShowPolicyUtil(prevPolicy, false, currentUserLogin), [prevPolicy, currentUserLogin]);
const shouldShow = useMemo(() => {
// If the policy object doesn't exist or contains only error data, we shouldn't display it.
if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) {
@@ -160,7 +158,7 @@ function WorkspacePageWithSections({
}
// We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace
- return (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !prevShouldShowPolicy);
+ return (!isEmptyObject(policy) && !isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !prevShouldShowPolicy);
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [policy, shouldShowNonAdmin, shouldShowPolicy, prevShouldShowPolicy]);
@@ -170,6 +168,7 @@ function WorkspacePageWithSections({
shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
testID={testID ?? WorkspacePageWithSections.displayName}
+ shouldShowOfflineIndicator={!shouldShow}
shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow}
>
0;
- const currentConnectionName = PolicyUtils.getCurrentConnectionName(policy);
+ const currentConnectionName = getCurrentConnectionName(policy);
const isQuickSettingsFlow = !!backTo;
const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true;
const fetchCategories = useCallback(() => {
- Category.openPolicyCategoriesPage(policyId);
+ openPolicyCategoriesPage(policyId);
}, [policyId]);
const {isOffline} = useNetwork({onReconnect: fetchCategories});
@@ -173,7 +172,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
};
const dismissError = (item: PolicyOption) => {
- Category.clearCategoryErrors(policyId, item.keyForList);
+ clearCategoryErrors(policyId, item.keyForList);
};
const selectedCategoriesArray = Object.keys(selectedCategories).filter((key) => selectedCategories[key]);
@@ -261,7 +260,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
return (
- {!PolicyUtils.hasAccountingConnections(policy) && (
+ {!hasAccountingConnections(policy) && (
);
}
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
index 7dc7a4d370e5..470031f2d3c8 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
@@ -16,7 +16,7 @@ import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
import {getWorkspaceAccountID, isDeletedPolicyEmployee} from '@libs/PolicyUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
-import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAssignCardStepAndData} from '@userActions/CompanyCards';
+import {checkIfFeedConnectionIsBroken, openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAssignCardStepAndData} from '@userActions/CompanyCards';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -55,18 +55,19 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) {
const isPending = !!selectedFeedData?.pending;
const isFeedAdded = !isPending && !isNoFeed;
const isFeedExpired = isSelectedFeedExpired(selectedFeed ? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed] : undefined);
+ const isFeedConnectionBroken = checkIfFeedConnectionIsBroken(cards);
const fetchCompanyCards = useCallback(() => {
openPolicyCompanyCardsPage(policyID, workspaceAccountID);
}, [policyID, workspaceAccountID]);
const {isOffline} = useNetwork({onReconnect: fetchCompanyCards});
- const isLoading = !isOffline && (!cardFeeds || (cardFeeds.isLoading && !cardsList));
+ const isLoading = !isOffline && (!cardFeeds || (!!cardFeeds.isLoading && !cardsList));
useFocusEffect(fetchCompanyCards);
useEffect(() => {
- if (!!isLoading || !selectedFeed || isPending) {
+ if (isLoading || !selectedFeed || isPending) {
return;
}
@@ -149,7 +150,7 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) {
cardsList={cardsList}
policyID={policyID}
handleAssignCard={handleAssignCard}
- isDisabledAssignCardButton={!selectedFeedData || !!selectedFeedData?.errors}
+ isDisabledAssignCardButton={!selectedFeedData || isFeedConnectionBroken}
/>
)}
diff --git a/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx
index 76bf9b2bd967..903abeb0d22f 100644
--- a/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx
+++ b/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx
@@ -3,7 +3,6 @@ import {ActivityIndicator} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {WebViewNavigation} from 'react-native-webview';
import {WebView} from 'react-native-webview';
-import type {ValueOf} from 'type-fest';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -17,17 +16,21 @@ import {checkIfNewFeedConnected} from '@libs/CardUtils';
import getUAForWebView from '@libs/getUAForWebView';
import Navigation from '@libs/Navigation/Navigation';
import {getWorkspaceAccountID} from '@libs/PolicyUtils';
+import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types';
+import type {SettingsNavigatorParamList} from '@navigation/types';
import {setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards';
import getCompanyCardBankConnection from '@userActions/getCompanyCardBankConnection';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
type BankConnectionStepProps = {
policyID?: string;
+ route?: PlatformStackRouteProp;
};
-function BankConnection({policyID}: BankConnectionStepProps) {
+function BankConnection({policyID: policyIDFromProps, route}: BankConnectionStepProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
@@ -35,7 +38,9 @@ function BankConnection({policyID}: BankConnectionStepProps) {
const [session] = useOnyx(ONYXKEYS.SESSION);
const authToken = session?.authToken ?? null;
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
- const bankName: ValueOf | undefined = addNewCard?.data?.selectedBank;
+ const {bankName: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {};
+ const policyID = policyIDFromProps ?? policyIDFromRoute;
+ const bankName = bankNameFromRoute ?? addNewCard?.data?.selectedBank;
const url = getCompanyCardBankConnection(policyID, bankName);
const workspaceAccountID = getWorkspaceAccountID(policyID);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
@@ -46,6 +51,10 @@ function BankConnection({policyID}: BankConnectionStepProps) {
const renderLoading = () => ;
const handleBackButtonPress = () => {
+ if (backTo) {
+ Navigation.goBack(backTo);
+ return;
+ }
if (bankName === CONST.COMPANY_CARDS.BANKS.BREX) {
setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK});
return;
@@ -85,7 +94,7 @@ function BankConnection({policyID}: BankConnectionStepProps) {
shouldEnableMaxHeight
>
diff --git a/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx b/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx
index e563f9020a39..fb7731589e84 100644
--- a/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx
+++ b/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx
@@ -1,6 +1,5 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useOnyx} from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
import BlockingView from '@components/BlockingViews/BlockingView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
@@ -13,25 +12,31 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {checkIfNewFeedConnected} from '@libs/CardUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getWorkspaceAccountID} from '@libs/PolicyUtils';
+import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types';
+import type {SettingsNavigatorParamList} from '@navigation/types';
import {updateSelectedFeed} from '@userActions/Card';
import {setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards';
import getCompanyCardBankConnection from '@userActions/getCompanyCardBankConnection';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import openBankConnection from './openBankConnection';
let customWindow: Window | null = null;
type BankConnectionStepProps = {
policyID?: string;
+ route?: PlatformStackRouteProp;
};
-function BankConnection({policyID}: BankConnectionStepProps) {
+function BankConnection({policyID: policyIDFromProps, route}: BankConnectionStepProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
- const bankName: ValueOf | undefined = addNewCard?.data?.selectedBank;
+ const {bankName: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {};
+ const policyID = policyIDFromProps ?? policyIDFromRoute;
+ const bankName = bankNameFromRoute ?? addNewCard?.data?.selectedBank;
const workspaceAccountID = getWorkspaceAccountID(policyID);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const prevFeedsData = usePrevious(cardFeeds?.settings?.oAuthAccountDetails);
@@ -49,6 +54,10 @@ function BankConnection({policyID}: BankConnectionStepProps) {
const handleBackButtonPress = () => {
customWindow?.close();
+ if (backTo) {
+ Navigation.goBack(backTo);
+ return;
+ }
if (bankName === CONST.COMPANY_CARDS.BANKS.BREX) {
setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK});
return;
@@ -88,7 +97,7 @@ function BankConnection({policyID}: BankConnectionStepProps) {
return (
{
- Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('new', policyID, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID)));
+ Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID)));
};
const handleSelectBankAccount = (value?: number) => {
- Card.configureExpensifyCardsForPolicy(policyID, value);
+ configureExpensifyCardsForPolicy(policyID, value);
};
const renderBankOptions = () => {
@@ -73,7 +74,7 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA
return null;
}
- const eligibleBankAccounts = CardUtils.getEligibleBankAccountsForCard(bankAccountsList);
+ const eligibleBankAccounts = getEligibleBankAccountsForCard(bankAccountsList);
return eligibleBankAccounts.map((bankAccount) => {
const bankName = (bankAccount.accountData?.addressName ?? '') as BankName;
diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx
index a7dd15d7c9a5..a3b0762ab7cb 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx
@@ -11,14 +11,15 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as CardUtils from '@libs/CardUtils';
+import {getEligibleBankAccountsForCard} from '@libs/CardUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
+import {REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
-import * as Policy from '@userActions/Policy/Policy';
+import {isCurrencySupportedForDirectReimbursement, updateGeneralSettings as updatePolicyGeneralSettings} from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -56,16 +57,16 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif
const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => !!account?.delegatedAccess?.delegate});
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
- const eligibleBankAccounts = CardUtils.getEligibleBankAccountsForCard(bankAccountList ?? {});
+ const eligibleBankAccounts = getEligibleBankAccountsForCard(bankAccountList ?? {});
const reimbursementAccountStatus = reimbursementAccount?.achData?.state ?? '';
const isSetupUnfinished = isEmptyObject(bankAccountList) && reimbursementAccountStatus && reimbursementAccountStatus !== CONST.BANK_ACCOUNT.STATE.OPEN;
const startFlow = useCallback(() => {
if (!eligibleBankAccounts.length || isSetupUnfinished) {
- Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('new', policy?.id, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policy?.id ?? '-1')));
+ Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policy?.id, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policy?.id)));
} else {
- Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT.getRoute(policy?.id ?? '-1'));
+ Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT.getRoute(policy?.id));
}
}, [eligibleBankAccounts.length, isSetupUnfinished, policy?.id]);
@@ -73,7 +74,7 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif
if (!policy) {
return;
}
- Policy.updateGeneralSettings(policy.id, policy.name, CONST.CURRENCY.USD);
+ updatePolicyGeneralSettings(policy.id, policy.name, CONST.CURRENCY.USD);
setIsCurrencyModalOpen(false);
startFlow();
}, [policy, startFlow]);
@@ -100,7 +101,7 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif
setIsNoDelegateAccessMenuVisible(true);
return;
}
- if (!Policy.isCurrencySupportedForDirectReimbursement(policy?.outputCurrency ?? '')) {
+ if (!isCurrencySupportedForDirectReimbursement(policy?.outputCurrency ?? '')) {
setIsCurrencyModalOpen(true);
return;
}
diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts
index 7d3d252dd86b..032cc1df9402 100644
--- a/src/types/onyx/Card.ts
+++ b/src/types/onyx/Card.ts
@@ -55,6 +55,9 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Last updated time */
lastScrape?: string;
+ /** Last update result */
+ lastScrapeResult?: number;
+
/** Card related error messages */
errors?: OnyxCommon.Errors;
diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts
index fa3929ac5b6e..4886e35ebfce 100644
--- a/src/types/onyx/CardFeeds.ts
+++ b/src/types/onyx/CardFeeds.ts
@@ -37,9 +37,6 @@ type CustomCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Indicates the day when the statement period for this card ends */
statementPeriodEndDay?: string;
-
- /** Broken connection errors */
- errors?: OnyxCommon.Errors;
}>;
/** Direct card feed data */
@@ -58,9 +55,6 @@ type DirectCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Whether any actions are pending */
pending?: boolean;
-
- /** Broken connection errors */
- errors?: OnyxCommon.Errors;
}>;
/** Card feed data */
diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts
index aba386448c09..0ab9425398bd 100644
--- a/src/types/onyx/DismissedProductTraining.ts
+++ b/src/types/onyx/DismissedProductTraining.ts
@@ -9,6 +9,7 @@ const {
BOTTOM_NAV_INBOX_TOOLTIP,
LHN_WORKSPACE_CHAT_TOOLTIP,
GLOBAL_CREATE_TOOLTIP,
+ SCAN_TEST_TOOLTIP,
} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES;
/**
* This type is used to store the timestamp of when the user dismisses a product training ui elements.
@@ -58,6 +59,11 @@ type DismissedProductTraining = {
* When user dismisses the globalCreateTooltip product training tooltip, we store the timestamp here.
*/
[GLOBAL_CREATE_TOOLTIP]: string;
+
+ /**
+ * When user dismisses the globalCreateTooltip product training tooltip, we store the timestamp here.
+ */
+ [SCAN_TEST_TOOLTIP]: string;
};
export default DismissedProductTraining;
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index 1bae98f96d50..dd619c01e797 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -370,6 +370,18 @@ type OriginalMessageModifiedExpense = {
attendees?: string;
};
+/** Model of the `deleted transaction` report action */
+type OriginalMessageDeletedTransaction = {
+ /** The merchant of the transaction */
+ merchant?: string;
+
+ /** The amount of the transaction */
+ amount?: number;
+
+ /** The currency of the transaction */
+ currency?: string;
+};
+
/** Model of `reimbursement queued` report action */
type OriginalMessageReimbursementQueued = {
/** How is the payment getting reimbursed */
@@ -637,6 +649,7 @@ type OriginalMessageMap = {
[CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL]: OriginalMessageCard;
[CONST.REPORT.ACTIONS.TYPE.CARD_ASSIGNED]: OriginalMessageCard;
[CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED]: OriginalMessageIntegrationSyncFailed;
+ [CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION]: OriginalMessageDeletedTransaction;
} & OldDotOriginalMessageMap & {
[T in ValueOf]: OriginalMessageChangeLog;
} & {
diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts
index a9e916486809..19ebf261bd7a 100644
--- a/tests/unit/OptionsListUtilsTest.ts
+++ b/tests/unit/OptionsListUtilsTest.ts
@@ -1251,4 +1251,14 @@ describe('OptionsListUtils', () => {
expect(result?.reportID).toEqual(REPORT.reportID);
});
});
+
+ describe('filterReports', () => {
+ it('should match a user with an accented name when searching using non-accented characters', () => {
+ const reports = [{text: "Álex Timón D'artagnan Zo-e"} as ReportUtils.OptionData];
+ const searchTerms = ['Alex Timon Dartagnan Zoe'];
+ const filteredReports = OptionsListUtils.filterReports(reports, searchTerms);
+
+ expect(filteredReports).toEqual(reports);
+ });
+ });
});
diff --git a/tests/unit/ReimbursementAccountUtilsTest.ts b/tests/unit/ReimbursementAccountUtilsTest.ts
new file mode 100644
index 000000000000..93086ba321a5
--- /dev/null
+++ b/tests/unit/ReimbursementAccountUtilsTest.ts
@@ -0,0 +1,15 @@
+import Onyx from 'react-native-onyx';
+import CONST from '../../src/CONST';
+import {getRouteForCurrentStep, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '../../src/libs/ReimbursementAccountUtils';
+import ONYXKEYS from '../../src/ONYXKEYS';
+
+Onyx.init({keys: ONYXKEYS});
+
+describe('ReimbursementAccountUtils', () => {
+ describe('getRouteForCurrentStep', () => {
+ it("should return 'new' step if 'BankAccountStep' or '' is provided", () => {
+ expect(getRouteForCurrentStep(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT)).toEqual(REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW);
+ expect(getRouteForCurrentStep('')).toEqual(REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW);
+ });
+ });
+});