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.6 FullStory OrgId 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 @@ CFBundleShortVersionString 9.0.89 CFBundleVersion - 9.0.89.5 + 9.0.89.6 NSExtension NSExtensionPointIdentifier 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 ? ( Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))} + onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report?.reportID, Navigation.getReportRHPActiveRoute()))} shouldShowRightIcon={!isDisableInteractive} disabled={disableState} wrapperStyle={[styles.pv2]} @@ -166,7 +164,7 @@ function TaskView({report}: TaskViewProps) { ) : ( Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))} + onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report?.reportID, Navigation.getReportRHPActiveRoute()))} shouldShowRightIcon={!isDisableInteractive} disabled={disableState} wrapperStyle={[styles.pv2]} diff --git a/src/components/ReportActionItem/TripRoomPreview.tsx b/src/components/ReportActionItem/TripRoomPreview.tsx index 8c275464804c..d85c19d21ee0 100644 --- a/src/components/ReportActionItem/TripRoomPreview.tsx +++ b/src/components/ReportActionItem/TripRoomPreview.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ListRenderItemInfo, StyleProp, ViewStyle} from 'react-native'; import {FlatList, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -36,7 +36,7 @@ type TripRoomPreviewProps = { action: ReportAction; /** The associated chatReport */ - chatReportID: string; + chatReportID: string | undefined; /** Extra styles to pass to View wrapper */ containerStyles?: StyleProp; @@ -111,7 +111,7 @@ function ReservationView({reservation}: ReservationViewProps) { ); } -const renderItem = ({item}: {item: ReservationData}) => ; +const renderItem = ({item}: ListRenderItemInfo) => ; function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) { const styles = useThemeStyles(); diff --git a/src/components/SelectionList/BaseSelectionListItemRenderer.tsx b/src/components/SelectionList/BaseSelectionListItemRenderer.tsx index 204b147508b4..987a72e025c1 100644 --- a/src/components/SelectionList/BaseSelectionListItemRenderer.tsx +++ b/src/components/SelectionList/BaseSelectionListItemRenderer.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {NativeSyntheticEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; import type useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import type useSingleExecution from '@hooks/useSingleExecution'; +import {isMobileChrome} from '@libs/Browser'; import {isReportListItemType} from '@libs/SearchUIUtils'; -import type {BaseListItemProps, BaseSelectionListProps, ListItem} from './types'; +import type {BaseListItemProps, BaseSelectionListProps, ExtendedTargetedEvent, ListItem} from './types'; type BaseSelectionListItemRendererProps = Omit, 'onSelectRow'> & Pick, 'ListItem' | 'shouldHighlightSelectedItem' | 'shouldIgnoreFocus' | 'shouldSingleExecuteRowSelect'> & { @@ -77,11 +78,15 @@ function BaseSelectionListItemRenderer({ isMultilineSupported={isMultilineSupported} isAlternateTextMultilineSupported={isAlternateTextMultilineSupported} alternateTextNumberOfLines={alternateTextNumberOfLines} - onFocus={() => { + onFocus={(event: NativeSyntheticEvent) => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (shouldIgnoreFocus || isDisabled) { return; } + // Prevent unexpected scrolling on mobile Chrome after the context menu closes by ignoring programmatic focus not triggered by direct user interaction. + if (isMobileChrome() && event.nativeEvent && !event.nativeEvent.sourceCapabilities) { + return; + } setFocusedIndex(normalizedIndex); }} shouldSyncFocus={shouldSyncFocus} diff --git a/src/components/SelectionList/Search/SearchQueryListItem.tsx b/src/components/SelectionList/Search/SearchQueryListItem.tsx index 770bad4faa31..82adb668fa95 100644 --- a/src/components/SelectionList/Search/SearchQueryListItem.tsx +++ b/src/components/SelectionList/Search/SearchQueryListItem.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import Icon from '@components/Icon'; import BaseListItem from '@components/SelectionList/BaseListItem'; -import type {ListItem} from '@components/SelectionList/types'; +import type {ListItem, ListItemFocusEventHandler} from '@components/SelectionList/types'; import TextWithTooltip from '@components/TextWithTooltip'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -25,7 +25,7 @@ type SearchQueryListItemProps = { isFocused?: boolean; showTooltip: boolean; onSelectRow: (item: SearchQueryItem) => void; - onFocus?: () => void; + onFocus?: ListItemFocusEventHandler; shouldSyncFocus?: boolean; }; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index caf941911ec5..71d172d4146d 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -7,6 +7,7 @@ import type { NativeSyntheticEvent, SectionListData, StyleProp, + TargetedEvent, TextInput, TextStyle, ViewStyle, @@ -84,12 +85,19 @@ type CommonListItemProps = { alternateTextNumberOfLines?: number; /** Handles what to do when the item is focused */ - onFocus?: () => void; + onFocus?: ListItemFocusEventHandler; /** Callback to fire when the item is long pressed */ onLongPressRow?: (item: TItem) => void; } & TRightHandSideComponent; +type ListItemFocusEventHandler = (event: NativeSyntheticEvent) => void; + +type ExtendedTargetedEvent = TargetedEvent & { + /** Provides information about the input device responsible for the event, or null if triggered programmatically, available in some browsers */ + sourceCapabilities?: unknown; +}; + type ListItem = { /** Text to display */ text?: string; @@ -684,10 +692,12 @@ export type { BaseSelectionListProps, ButtonOrCheckBoxRoles, CommonListItemProps, + ExtendedTargetedEvent, FlattenedSectionsReturn, InviteMemberListItemProps, ItemLayout, ListItem, + ListItemFocusEventHandler, ListItemProps, RadioListItemProps, ReportListItemProps, diff --git a/src/components/SelectionListWithModal/index.tsx b/src/components/SelectionListWithModal/index.tsx index 59e9b268bc52..a4c8b83b29d8 100644 --- a/src/components/SelectionListWithModal/index.tsx +++ b/src/components/SelectionListWithModal/index.tsx @@ -113,6 +113,7 @@ function SelectionListWithModal( isVisible={isModalVisible} type={CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED} onClose={() => setIsModalVisible(false)} + shouldPreventScrollOnFocus > `deleted an expense on this report, ${merchant} - ${amount}`, pendingMatchWithCreditCard: 'Receipt pending match with card transaction', pendingMatchWithCreditCardDescription: 'Receipt pending match with card transaction. Mark as cash to cancel.', markAsCash: 'Mark as cash', @@ -5691,6 +5693,16 @@ const translations = { part2: ', start chatting,', part3: '\nand more!', }, + scanTestTooltip: { + part1: 'Want to see how Scan works?', + part2: ' Try a test receipt!', + part3: 'Choose our', + part4: ' test manager', + part5: ' to try it out!', + part6: 'Now,', + part7: ' submit your expense', + part8: ' and watch the magic happen!', + }, }, discardChangesConfirmation: { title: 'Discard changes?', diff --git a/src/languages/es.ts b/src/languages/es.ts index dff3dcd575c0..b64635608952 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -58,6 +58,7 @@ import type { DelegatorParams, DeleteActionParams, DeleteConfirmationParams, + DeleteTransactionParams, DidSplitAmountMessageParams, EarlyDiscountSubtitleParams, EarlyDiscountTitleParams, @@ -880,6 +881,7 @@ const translations = { pendingMatchWithCreditCardDescription: 'Recibo pendiente de adjuntar con la transacción de la tarjeta. Márcalo como efectivo para cancelar.', markAsCash: 'Marcar como efectivo', routePending: 'Ruta pendiente...', + deletedTransaction: ({amount, merchant}: DeleteTransactionParams) => `eliminó un gasto de este informe, ${merchant} - ${amount}`, receiptIssuesFound: () => ({ one: 'Problema encontrado', other: 'Problemas encontrados', @@ -6210,6 +6212,16 @@ const translations = { part2: ', comienza a chatear,', part3: '\ny mucho más!', }, + scanTestTooltip: { + part1: '¿Quieres ver cómo funciona Escanear?', + part2: ' ¡Prueba con un recibo de prueba!', + part3: '¡Elige a', + part4: ' nuestro gerente', + part5: ' de prueba para probarlo!', + part6: 'Ahora,', + part7: ' envía tu gasto y', + part8: ' ¡observa cómo ocurre la magia!', + }, }, discardChangesConfirmation: { title: '¿Descartar cambios?', diff --git a/src/languages/params.ts b/src/languages/params.ts index 9d28f198b704..59e4acf74f6a 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -115,6 +115,11 @@ type RequestCountParams = { pendingReceipts: number; }; +type DeleteTransactionParams = { + amount: string; + merchant: string; +}; + type SettleExpensifyCardParams = { formattedAmount: string; }; @@ -725,6 +730,7 @@ export type { ReportArchiveReasonsRemovedFromPolicyParams, RequestAmountParams, RequestCountParams, + DeleteTransactionParams, RequestedAmountMessageParams, ResolutionConstraintsParams, RoomNameReservedErrorParams, diff --git a/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts b/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts deleted file mode 100644 index 758152abc2af..000000000000 --- a/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -type OptInOutToPushNotificationsParams = { - deviceID: string | null; -}; - -export default OptInOutToPushNotificationsParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 54362c4558f0..43a2a75577c8 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -93,7 +93,6 @@ export type {default as DisableTwoFactorAuthParams} from './DisableTwoFactorAuth export type {default as VerifyIdentityForBankAccountParams} from './VerifyIdentityForBankAccountParams'; export type {default as AnswerQuestionsForWalletParams} from './AnswerQuestionsForWalletParams'; export type {default as AddCommentOrAttachementParams} from './AddCommentOrAttachementParams'; -export type {default as OptInOutToPushNotificationsParams} from './OptInOutToPushNotificationsParams'; export type {default as ReadNewestActionParams} from './ReadNewestActionParams'; export type {default as MarkAsUnreadParams} from './MarkAsUnreadParams'; export type {default as TogglePinnedChatParams} from './TogglePinnedChatParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index b24a687e930a..a98bf28028ee 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -107,8 +107,6 @@ const WRITE_COMMANDS = { ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', RESTART_BANK_ACCOUNT_SETUP: 'RestartBankAccountSetup', RESEND_VALIDATE_CODE: 'ResendValidateCode', - OPT_IN_TO_PUSH_NOTIFICATIONS: 'OptInToPushNotifications', - OPT_OUT_OF_PUSH_NOTIFICATIONS: 'OptOutOfPushNotifications', READ_NEWEST_ACTION: 'ReadNewestAction', MARK_AS_UNREAD: 'MarkAsUnread', TOGGLE_PINNED_CHAT: 'TogglePinnedChat', @@ -540,9 +538,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: Parameters.ConnectBankAccountParams; [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams; [WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP]: Parameters.RestartBankAccountSetupParams; - [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; [WRITE_COMMANDS.RESEND_VALIDATE_CODE]: null; - [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; [WRITE_COMMANDS.READ_NEWEST_ACTION]: Parameters.ReadNewestActionParams; [WRITE_COMMANDS.MARK_AS_UNREAD]: Parameters.MarkAsUnreadParams; [WRITE_COMMANDS.TOGGLE_PINNED_CHAT]: Parameters.TogglePinnedChatParams; diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts index efa4c3c7e780..2b32857b1d06 100644 --- a/src/libs/E2E/utils/NetworkInterceptor.ts +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -156,14 +156,6 @@ export default function installNetworkInterceptor( const headers = getFetchRequestHeadersAsObject(options); const url = fetchArgsGetUrl(args); - // Don't process these specific API commands because running them over and over again in the tests increases the size of the notificationPreferences NVP on the server to an infinite size. - // This is due to the NVP storing this setting once for each user's device, and since the E2E tests use AWS device farm, the user ends up with thousands of different devices, - // unlike normal users that might only ever have about a dozen. We found the NVP was over 2.5mb in size and that slows down database replication. - if (url.includes('OptInToPushNotifications') || url.includes('OptOutOfPushNotifications')) { - console.debug('Skipping request to opt in or out of push notifications'); - return Promise.resolve(new Response()); - } - // Check if headers contain any of the ignored headers, or if react native metro server: if (IGNORE_REQUEST_HEADERS.some((header) => headers[header] != null) || url.includes('8081')) { return originalFetch(...args); diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 82211eabe00f..0e2ac8746dd3 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -1,4 +1,4 @@ -import {findFocusedRoute} from '@react-navigation/native'; +import {findFocusedRoute, useNavigation} from '@react-navigation/native'; import React, {memo, useEffect, useMemo, useRef, useState} from 'react'; import {NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -248,6 +248,14 @@ function AuthScreens() { const isInitialLastUpdateIDAppliedToClientLoading = isLoadingOnyxValue(initialLastUpdateIDAppliedToClientStatus); const isLastOpenedPublicRoomIDLoadedRef = useRef(false); const isInitialLastUpdateIDAppliedToClientLoadedRef = useRef(false); + const navigation = useNavigation(); + + useEffect(() => { + const unsubscribe = navigation.addListener('state', () => { + PriorityMode.autoSwitchToFocusMode(); + }); + return () => unsubscribe(); + }, [navigation]); // On HybridApp we need to prevent flickering during transition to OldDot const shouldRenderOnboardingExclusivelyOnHybridApp = useMemo(() => { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0b31401d7e25..1e5e5027dc4f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -533,6 +533,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: () => require('../../../../pages/workspace/companyCards/assignCard/AssignCardFeedPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage').default, + [SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: () => require('../../../../pages/workspace/companyCards/addNew/BankConnection').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ADD_NEW]: () => require('../../../../pages/workspace/companyCards/addNew/AddNewCardPage').default, [SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage').default, [SCREENS.WORKSPACE.COMPANY_CARD_NAME]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage').default, diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx index 35ece3c28a3d..db6fae0669c2 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx @@ -14,6 +14,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import {getRouteForCurrentStep as getReimbursementAccountRouteForCurrentStep} from '@libs/ReimbursementAccountUtils'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import {getChatTabBrickRoadReport} from '@libs/WorkspacesSettingsUtils'; import CONST from '@src/CONST'; @@ -76,7 +77,10 @@ function getSettingsRoute(status: IndicatorStatus | undefined, reimbursementAcco case CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS: return ROUTES.WORKSPACE_INITIAL.getRoute(policyIDWithErrors); case CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS: - return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(reimbursementAccount?.achData?.currentStep, reimbursementAccount?.achData?.policyID); + return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute( + reimbursementAccount?.achData?.policyID, + getReimbursementAccountRouteForCurrentStep(reimbursementAccount?.achData?.currentStep ?? CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT), + ); case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS: return ROUTES.SETTINGS_SUBSCRIPTION; case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO: diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 2c3b060e0835..7865993d08e9 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -213,6 +213,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.COMPANY_CARDS_NAME, SCREENS.WORKSPACE.COMPANY_CARDS_DETAILS, SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED, + SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 579dfe227fb9..9b7061e09ccc 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -620,6 +620,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.route, }, + [SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: { + path: ROUTES.WORKSPACE_COMPANY_CARDS_BANK_CONNECTION.route, + }, [SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: { path: ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 67752a152941..54e9c7a56e26 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -14,6 +14,7 @@ import type {TupleToUnion, ValueOf} from 'type-fest'; import type {SearchQueryString} from '@components/Search/types'; import type {IOURequestType} from '@libs/actions/IOU'; import type {SaveSearchParams} from '@libs/API/parameters'; +import type {ReimbursementAccountStepToOpen} from '@libs/ReimbursementAccountUtils'; import type CONST from '@src/CONST'; import type {Country, IOUAction, IOUType} from '@src/CONST'; import type NAVIGATORS from '@src/NAVIGATORS'; @@ -822,6 +823,11 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: { policyID: string; }; + [SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: { + policyID: string; + bankName: string; + backTo: Routes; + }; [SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: { policyID: string; bank: CompanyCardFeed; @@ -1350,7 +1356,7 @@ type AddPersonalBankAccountNavigatorParamList = { type ReimbursementAccountNavigatorParamList = { [SCREENS.REIMBURSEMENT_ACCOUNT_ROOT]: { - stepToOpen?: string; + stepToOpen?: ReimbursementAccountStepToOpen; backTo?: Routes; policyID?: string; }; diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 448365c1cd1d..8416d96ef5a6 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -1,10 +1,7 @@ import type {PushPayload} from '@ua/react-native-airship'; import Airship, {EventType} from '@ua/react-native-airship'; -import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import ShortcutManager from '@libs/ShortcutManager'; -import * as PushNotificationActions from '@userActions/PushNotification'; -import ONYXKEYS from '@src/ONYXKEYS'; import ForegroundNotifications from './ForegroundNotifications'; import type {PushNotificationData} from './NotificationType'; import NotificationType from './NotificationType'; @@ -16,12 +13,6 @@ type NotificationEventActionCallback = (data: PushNotificationData) => Promise>>; -let isUserOptedInToPushNotifications = false; -Onyx.connect({ - key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, - callback: (value) => (isUserOptedInToPushNotifications = value ?? false), -}); - const notificationEventActionMap: NotificationEventActionMap = {}; /** @@ -61,21 +52,6 @@ function pushNotificationEventCallback(eventType: EventType, notification: PushP return action(data); } -/** - * Check if a user is opted-in to push notifications on this device and update the `pushNotificationsEnabled` NVP accordingly. - */ -function refreshNotificationOptInStatus() { - Airship.push.getNotificationStatus().then((notificationStatus) => { - const isOptedIn = notificationStatus.isOptedIn && notificationStatus.areNotificationsAllowed; - if (isOptedIn === isUserOptedInToPushNotifications) { - return; - } - - Log.info('[PushNotification] Push notification opt-in status changed.', false, {isOptedIn}); - PushNotificationActions.setPushNotificationOptInStatus(isOptedIn); - }); -} - /** * Configure push notifications and register callbacks. This is separate from namedUser registration because it needs to be executed * from a headless JS process, outside of any react lifecycle. @@ -91,9 +67,6 @@ const init: Init = () => { // so event.notification refers to the same thing as notification above ^ Airship.addListener(EventType.NotificationResponse, (event) => pushNotificationEventCallback(EventType.NotificationResponse, event.pushPayload)); - // Keep track of which users have enabled push notifications via an NVP. - Airship.addListener(EventType.PushNotificationStatusChangedStatus, refreshNotificationOptInStatus); - ForegroundNotifications.configureForegroundNotifications(); }; @@ -122,9 +95,6 @@ const register: Register = (notificationID) => { // Regardless of the user's opt-in status, we still want to receive silent push notifications. Log.info(`[PushNotification] Subscribing to notifications`); Airship.contact.identify(notificationID.toString()); - - // Refresh notification opt-in status NVP for the new user. - refreshNotificationOptInStatus(); }) .catch((error: Record) => { Log.warn('[PushNotification] Failed to register for push notifications! Reason: ', error); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index c704b588d0cb..a8e23888db0d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -127,6 +127,7 @@ import { shouldReportShowSubscript, } from './ReportUtils'; import type {OptionData} from './ReportUtils'; +import StringUtils from './StringUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from './TaskUtils'; import {generateAccountID} from './UserUtils'; @@ -1839,29 +1840,31 @@ function filteredPersonalDetailsOfRecentReports(recentReports: OptionData[], per * Filters options based on the search input value */ function filterReports(reports: OptionData[], searchTerms: string[]): OptionData[] { + const normalizedSearchTerms = searchTerms.map((term) => StringUtils.normalizeAccents(term)); // We search eventually for multiple whitespace separated search terms. // We start with the search term at the end, and then narrow down those filtered search results with the next search term. // We repeat (reduce) this until all search terms have been used: - const filteredReports = searchTerms.reduceRight( + const filteredReports = normalizedSearchTerms.reduceRight( (items, term) => filterArrayByMatch(items, term, (item) => { const values: string[] = []; if (item.text) { - values.push(item.text); + values.push(StringUtils.normalizeAccents(item.text)); + values.push(StringUtils.normalizeAccents(item.text).replace(/['-]/g, '')); } if (item.login) { - values.push(item.login); - values.push(item.login.replace(CONST.EMAIL_SEARCH_REGEX, '')); + values.push(StringUtils.normalizeAccents(item.login)); + values.push(StringUtils.normalizeAccents(item.login.replace(CONST.EMAIL_SEARCH_REGEX, ''))); } if (item.isThread) { if (item.alternateText) { - values.push(item.alternateText); + values.push(StringUtils.normalizeAccents(item.alternateText)); } } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { if (item.subtitle) { - values.push(item.subtitle); + values.push(StringUtils.normalizeAccents(item.subtitle)); } } @@ -2125,6 +2128,7 @@ export { filterWorkspaceChats, orderWorkspaceOptions, filterSelfDMChat, + filterReports, }; export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Option, OptionTree, ReportAndPersonalDetailOptions, GetUserToInviteConfig}; diff --git a/src/libs/PhoneNumber.ts b/src/libs/PhoneNumber.ts index 84ef35a18489..b0cbbc9176de 100644 --- a/src/libs/PhoneNumber.ts +++ b/src/libs/PhoneNumber.ts @@ -43,7 +43,7 @@ function parsePhoneNumber(phoneNumber: string, options?: PhoneNumberParseOptions /** * Adds expensify SMS domain (@expensify.sms) if login is a phone number and if it's not included yet */ -function addSMSDomainIfPhoneNumber(login: string): string { +function addSMSDomainIfPhoneNumber(login = ''): string { const parsedPhoneNumber = parsePhoneNumber(login); if (parsedPhoneNumber.possible && !Str.isValidEmail(login)) { return `${parsedPhoneNumber.number?.e164}${CONST.SMS.DOMAIN}`; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 9e5e03acd1b0..6662016201c7 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -9,7 +9,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm'; import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagLists, PolicyTags, Report, TaxRate} from '@src/types/onyx'; -import type {CardFeedData} from '@src/types/onyx/CardFeeds'; import type {ErrorFields, PendingAction, PendingFields} from '@src/types/onyx/OnyxCommon'; import type { ConnectionLastSync, @@ -211,17 +210,17 @@ function getPolicyRole(policy: OnyxInputOrEntry | SearchPolicy, currentU /** * Check if the policy can be displayed - * If offline, always show the policy pending deletion. - * If online, show the policy pending deletion only if there is an error. + * If shouldShowPendingDeletePolicy is true, show the policy pending deletion. + * If shouldShowPendingDeletePolicy is false, show the policy pending deletion only if there is an error. * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. */ -function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean, currentUserLogin: string | undefined): boolean { +function shouldShowPolicy(policy: OnyxEntry, shouldShowPendingDeletePolicy: boolean, currentUserLogin: string | undefined): boolean { return ( !!policy?.isJoinRequestPending || (!!policy && policy?.type !== CONST.POLICY.TYPE.PERSONAL && - (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) && + (shouldShowPendingDeletePolicy || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) && !!getPolicyRole(policy, currentUserLogin)) ); } @@ -1146,10 +1145,6 @@ function getWorkflowApprovalsUnavailable(policy: OnyxEntry) { return policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL || !!policy?.errorFields?.approvalMode; } -function hasPolicyFeedsError(feeds: Record, feedToSkip?: string): boolean { - return Object.entries(feeds).filter(([feedName, feedData]) => feedName !== feedToSkip && !!feedData.errors).length > 0; -} - function getAllPoliciesLength() { return Object.keys(allPolicies ?? {}).length; } @@ -1170,7 +1165,9 @@ function getUserFriendlyWorkspaceType(workspaceType: ValueOf): boolean { - return !isEmptyObject(policy) && (Object.keys(policy).length !== 1 || isEmptyObject(policy.errors)) && !!policy?.id; + return ( + !isEmptyObject(policy) && (Object.keys(policy).length !== 1 || isEmptyObject(policy.errors)) && !!policy?.id && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ); } function areAllGroupPoliciesExpenseChatDisabled(policies = allPolicies) { @@ -1233,7 +1230,6 @@ export { goBackFromInvalidPolicy, hasAccountingConnections, shouldShowSyncError, - hasPolicyFeedsError, shouldShowCustomUnitsError, shouldShowEmployeeListError, hasIntegrationAutoSync, diff --git a/src/libs/ReimbursementAccountUtils.ts b/src/libs/ReimbursementAccountUtils.ts new file mode 100644 index 000000000000..e844c815c8af --- /dev/null +++ b/src/libs/ReimbursementAccountUtils.ts @@ -0,0 +1,38 @@ +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; +import type {ReimbursementAccountStep} from '@src/types/onyx/ReimbursementAccount'; + +type ReimbursementAccountStepToOpen = ValueOf | ''; + +const REIMBURSEMENT_ACCOUNT_ROUTE_NAMES = { + COMPANY: 'company', + PERSONAL_INFORMATION: 'personal-information', + BENEFICIAL_OWNERS: 'beneficial-owners', + CONTRACT: 'contract', + VALIDATE: 'validate', + ENABLE: 'enable', + NEW: 'new', +} as const; + +function getRouteForCurrentStep(currentStep: ReimbursementAccountStep): ReimbursementAccountStepToOpen { + switch (currentStep) { + case CONST.BANK_ACCOUNT.STEP.COMPANY: + return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.COMPANY; + case CONST.BANK_ACCOUNT.STEP.REQUESTOR: + return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.PERSONAL_INFORMATION; + case CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS: + return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.BENEFICIAL_OWNERS; + case CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT: + return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.CONTRACT; + case CONST.BANK_ACCOUNT.STEP.VALIDATION: + return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.VALIDATE; + case CONST.BANK_ACCOUNT.STEP.ENABLE: + return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.ENABLE; + case CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT: + default: + return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW; + } +} + +export {getRouteForCurrentStep, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES}; +export type {ReimbursementAccountStepToOpen}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1015d0b5fec6..df7da776f135 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -64,7 +64,7 @@ import {autoSwitchToFocusMode} from './actions/PriorityMode'; import {hasCreditBankAccount} from './actions/ReimbursementAccount/store'; import {handleReportChanged} from './actions/Report'; import {isAnonymousUser as isAnonymousUserSession} from './actions/Session'; -import {convertToDisplayString} from './CurrencyUtils'; +import {convertToDisplayString, getCurrencySymbol} from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; import {getMicroSecondOnyxErrorWithTranslationKey} from './ErrorUtils'; @@ -1365,7 +1365,7 @@ function isPublicAnnounceRoom(report: OnyxEntry): boolean { * else since the report is a personal IOU, the route should be for personal bank account. */ function getBankAccountRoute(report: OnyxEntry): Route { - return isPolicyExpenseChat(report) ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', report?.policyID) : ROUTES.SETTINGS_ADD_BANK_ACCOUNT; + return isPolicyExpenseChat(report) ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(report?.policyID) : ROUTES.SETTINGS_ADD_BANK_ACCOUNT; } /** @@ -3233,10 +3233,10 @@ function getReportFieldsByPolicyID(policyID: string | undefined): Record, policyReportFields: PolicyReportField[]): PolicyReportField[] { // Get the report fields that are attached to a report. These will persist even if a field is deleted from the policy. - const reportFields = Object.values(report.fieldList ?? {}); - const reportIsSettled = isSettled(report.reportID); + const reportFields = Object.values(report?.fieldList ?? {}); + const reportIsSettled = isSettled(report?.reportID); // If the report is settled, we don't want to show any new field that gets added to the policy. if (reportIsSettled) { @@ -5136,6 +5136,17 @@ function getWorkspaceNameUpdatedMessage(action: ReportAction) { return Str.htmlEncode(message); } +function getDeletedTransactionMessage(action: ReportAction) { + const deletedTransactionOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; + const amount = Math.abs(deletedTransactionOriginalMessage.amount ?? 0) / 100; + const currency = getCurrencySymbol(deletedTransactionOriginalMessage.currency ?? ''); + const message = translateLocal('iou.deletedTransaction', { + amount: `${currency}${amount}`, + merchant: deletedTransactionOriginalMessage.merchant ?? '', + }); + return message; +} + /** * @param iouReportID - the report ID of the IOU report the action belongs to * @param type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split) @@ -8971,6 +8982,7 @@ export { getIOUForwardedMessage, getRejectedReportMessage, getWorkspaceNameUpdatedMessage, + getDeletedTransactionMessage, getUpgradeWorkspaceMessage, getDowngradeWorkspaceMessage, getReportAutomaticallySubmittedMessage, diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts index 975c3e617ce5..ac66bdebd42c 100644 --- a/src/libs/TaskUtils.ts +++ b/src/libs/TaskUtils.ts @@ -6,7 +6,7 @@ import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; -import * as Localize from './Localize'; +import {translateLocal} from './Localize'; import Navigation from './Navigation/Navigation'; import {getReportActionHtml, getReportActionText} from './ReportActionsUtils'; @@ -22,7 +22,11 @@ Onyx.connect({ /** * Check if the active route belongs to task edit flow. */ -function isActiveTaskEditRoute(reportID: string): boolean { +function isActiveTaskEditRoute(reportID: string | undefined): boolean { + if (!reportID) { + return false; + } + return [ROUTES.TASK_TITLE, ROUTES.TASK_ASSIGNEE, ROUTES.REPORT_DESCRIPTION].map((route) => route.getRoute(reportID)).some(Navigation.isActiveRoute); } @@ -32,18 +36,18 @@ function isActiveTaskEditRoute(reportID: string): boolean { function getTaskReportActionMessage(action: OnyxEntry): Pick { switch (action?.actionName) { case CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED: - return {text: Localize.translateLocal('task.messages.completed')}; + return {text: translateLocal('task.messages.completed')}; case CONST.REPORT.ACTIONS.TYPE.TASK_CANCELLED: - return {text: Localize.translateLocal('task.messages.canceled')}; + return {text: translateLocal('task.messages.canceled')}; case CONST.REPORT.ACTIONS.TYPE.TASK_REOPENED: - return {text: Localize.translateLocal('task.messages.reopened')}; + return {text: translateLocal('task.messages.reopened')}; case CONST.REPORT.ACTIONS.TYPE.TASK_EDITED: return { text: getReportActionText(action), html: getReportActionHtml(action), }; default: - return {text: Localize.translateLocal('task.task')}; + return {text: translateLocal('task.task')}; } } @@ -54,15 +58,15 @@ function getTaskTitleFromReport(taskReport: OnyxEntry, fallbackTitle = ' return taskReport?.reportID && taskReport.reportName ? taskReport.reportName : fallbackTitle; } -function getTaskTitle(taskReportID: string, fallbackTitle = ''): string { +function getTaskTitle(taskReportID: string | undefined, fallbackTitle = ''): string { const taskReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`]; return getTaskTitleFromReport(taskReport, fallbackTitle); } function getTaskCreatedMessage(reportAction: OnyxEntry) { - const taskReportID = reportAction?.childReportID ?? '-1'; + const taskReportID = reportAction?.childReportID; const taskTitle = getTaskTitle(taskReportID, reportAction?.childReportName); - return taskTitle ? Localize.translateLocal('task.messages.created', {title: taskTitle}) : ''; + return taskTitle ? translateLocal('task.messages.created', {title: taskTitle}) : ''; } export {isActiveTaskEditRoute, getTaskReportActionMessage, getTaskTitle, getTaskTitleFromReport, getTaskCreatedMessage}; diff --git a/src/libs/actions/Chronos.ts b/src/libs/actions/Chronos.ts index 548b8398beec..5a54c2ee9368 100644 --- a/src/libs/actions/Chronos.ts +++ b/src/libs/actions/Chronos.ts @@ -7,7 +7,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ChronosOOOEvent} from '@src/types/onyx/OriginalMessage'; -const removeEvent = (reportID: string, reportActionID: string, eventID: string, events: ChronosOOOEvent[]) => { +const removeEvent = (reportID: string | undefined, reportActionID: string, eventID: string, events: ChronosOOOEvent[]) => { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index d4a905d5ef6d..f6e6dbb3729d 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type { @@ -18,10 +18,11 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card, CardFeeds} from '@src/types/onyx'; +import type {Card, CardFeeds, WorkspaceCardsList} from '@src/types/onyx'; import type {AssignCard, AssignCardData} from '@src/types/onyx/AssignCard'; import type {AddNewCardFeedData, AddNewCardFeedStep, CompanyCardFeed} from '@src/types/onyx/CardFeeds'; import type {OnyxData} from '@src/types/onyx/Request'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AddNewCompanyCardFlowData = { /** Step to be set in Onyx */ @@ -403,8 +404,6 @@ function unassignWorkspaceCompanyCard(workspaceAccountID: number, bankName: stri function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, bankName: CompanyCardFeed) { const authToken = NetworkStore.getAuthToken(); - const optimisticFeedUpdates = {[bankName]: {errors: null}}; - const failureFeedUpdates = {[bankName]: {errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR}}}; const optimisticData: OnyxUpdate[] = [ { @@ -437,13 +436,6 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, }, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, - value: { - settings: {companyCards: optimisticFeedUpdates}, - }, - }, ]; const finallyData: OnyxUpdate[] = [ @@ -504,13 +496,6 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, }, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, - value: { - settings: {companyCards: failureFeedUpdates}, - }, - }, ]; const parameters = { @@ -740,6 +725,41 @@ function openPolicyCompanyCardsFeed(policyID: string, feed: CompanyCardFeed) { API.read(READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED, parameters); } +/** + * Takes the list of cards divided by workspaces and feeds and returns the flattened non-Expensify cards related to the provided workspace + * + * @param allCardsList the list where cards split by workspaces and feeds and stored under `card_${workspaceAccountID}_${feedName}` keys + * @param workspaceAccountID the workspace account id we want to get cards for + */ +function flatAllCardsList(allCardsList: OnyxCollection, workspaceAccountID: number): Record | undefined { + if (!allCardsList) { + return; + } + + return Object.entries(allCardsList).reduce((acc, [key, allCards]) => { + if (!key.includes(workspaceAccountID.toString()) || key.includes(CONST.EXPENSIFY_CARD.BANK)) { + return acc; + } + const {cardList, ...feedCards} = allCards ?? {}; + Object.assign(acc, feedCards); + return acc; + }, {}); +} + +/** + * Check if any feed card has a broken connection + * + * @param feedCards the list of the cards, related to one or several feeds + * @param [feedToExclude] the feed to ignore during the check, it's useful for checking broken connection error only in the feeds other than the selected one + */ +function checkIfFeedConnectionIsBroken(feedCards: Record | undefined, feedToExclude?: string): boolean { + if (!feedCards || isEmptyObject(feedCards)) { + return false; + } + + return Object.values(feedCards).some((card) => card.bank !== feedToExclude && card.lastScrapeResult !== 200); +} + export { setWorkspaceCompanyCardFeedName, deleteWorkspaceCompanyCardFeed, @@ -757,4 +777,6 @@ export { clearAddNewCardFlow, setAssignCardStepAndData, clearAssignCardStepAndData, + checkIfFeedConnectionIsBroken, + flatAllCardsList, }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 080e9505c6d5..0cd321851b24 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5741,7 +5741,13 @@ function startSplitBill({ * @param sessionAccountID - accountID of the current user * @param sessionEmail - email of the current user */ -function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportAction, updatedTransaction: OnyxEntry, sessionAccountID: number, sessionEmail: string) { +function completeSplitBill( + chatReportID: string, + reportAction: OnyxTypes.ReportAction, + updatedTransaction: OnyxEntry, + sessionAccountID: number, + sessionEmail?: string, +) { const currentUserEmailForIOUSplit = addSMSDomainIfPhoneNumber(sessionEmail); const transactionID = updatedTransaction?.transactionID; const unmodifiedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -5980,7 +5986,10 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA notifyNewAction(chatReportID, sessionAccountID); } -function setDraftSplitTransaction(transactionID: string, transactionChanges: TransactionChanges = {}, policy?: OnyxEntry) { +function setDraftSplitTransaction(transactionID: string | undefined, transactionChanges: TransactionChanges = {}, policy?: OnyxEntry) { + if (!transactionID) { + return undefined; + } let draftSplitTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`]; if (!draftSplitTransaction) { diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 44b0a71dc72c..834812013b5c 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -874,11 +874,12 @@ function setWorkspaceInviteMembersDraft(policyID: string, invitedEmailsToAccount /** * Accept user join request to a workspace */ -function acceptJoinRequest(reportID: string, reportAction: OnyxEntry) { - const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT; - if (!reportAction) { +function acceptJoinRequest(reportID: string | undefined, reportAction: OnyxEntry) { + if (!reportAction || !reportID) { + Log.warn('acceptJoinRequest missing reportID or reportAction', {reportAction, reportID}); return; } + const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT; const optimisticData: OnyxUpdate[] = [ { @@ -933,8 +934,9 @@ function acceptJoinRequest(reportID: string, reportAction: OnyxEntry) { - if (!reportAction) { +function declineJoinRequest(reportID: string | undefined, reportAction: OnyxEntry) { + if (!reportAction || !reportID) { + Log.warn('declineJoinRequest missing reportID or reportAction', {reportAction, reportID}); return; } const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 5ae1c07f1eec..1d1090996e48 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1241,9 +1241,13 @@ function clearAvatarErrors(policyID: string) { * Optimistically update the general settings. Set the general settings as pending until the response succeeds. * If the response fails set a general error message. Clear the error message when updating. */ -function updateGeneralSettings(policyID: string, name: string, currencyValue?: string) { +function updateGeneralSettings(policyID: string | undefined, name: string, currencyValue?: string) { + if (!policyID) { + return; + } + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; - if (!policy || !policyID) { + if (!policy) { return; } diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts index 95042697c911..619783420be2 100644 --- a/src/libs/actions/PriorityMode.ts +++ b/src/libs/actions/PriorityMode.ts @@ -1,10 +1,13 @@ import debounce from 'lodash/debounce'; import Onyx from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; +import navigationRef from '@libs/Navigation/navigationRef'; import {isReportParticipant, isValidReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; import type {Report} from '@src/types/onyx'; /** @@ -101,6 +104,15 @@ function tryFocusModeUpdate() { return; } + const currentRoute = navigationRef.getCurrentRoute(); + if (getIsNarrowLayout()) { + if (currentRoute?.name !== SCREENS.HOME) { + return; + } + } else if (currentRoute?.name !== SCREENS.REPORT) { + return; + } + // Check to see if the user is using #focus mode, has tried it before, or we have already switched them over automatically. if ((isInFocusMode ?? false) || hasTriedFocusMode) { Log.info('Not switching user to optimized focus mode.', false, {isInFocusMode, hasTriedFocusMode}); diff --git a/src/libs/actions/PushNotification.ts b/src/libs/actions/PushNotification.ts deleted file mode 100644 index d4caf7925d4c..000000000000 --- a/src/libs/actions/PushNotification.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Onyx from 'react-native-onyx'; -import * as API from '@libs/API'; -import {WRITE_COMMANDS} from '@libs/API/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as Device from './Device'; - -let isUserOptedInToPushNotifications = false; -Onyx.connect({ - key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, - callback: (value) => { - if (value === undefined) { - return; - } - isUserOptedInToPushNotifications = value; - }, -}); - -/** - * Record that user opted-in or opted-out of push notifications on the current device. - */ -function setPushNotificationOptInStatus(isOptingIn: boolean) { - Device.getDeviceID().then((deviceID) => { - const commandName = isOptingIn ? WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS : WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS; - const optimisticData = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, - value: isOptingIn, - }, - ]; - const failureData = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, - value: isUserOptedInToPushNotifications, - }, - ]; - API.write(commandName, {deviceID: deviceID ?? null}, {optimisticData, failureData}); - }); -} - -export { - // eslint-disable-next-line import/prefer-default-export - setPushNotificationOptInStatus, -}; diff --git a/src/libs/actions/ReimbursementAccount/navigation.ts b/src/libs/actions/ReimbursementAccount/navigation.ts index 49cf17fcc5bf..192c582e9366 100644 --- a/src/libs/actions/ReimbursementAccount/navigation.ts +++ b/src/libs/actions/ReimbursementAccount/navigation.ts @@ -17,8 +17,8 @@ function goToWithdrawalAccountSetupStep(stepID: BankAccountStep) { * @param policyID - The policy ID associated with the bank account. * @param [backTo] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. */ -function navigateToBankAccountRoute(policyID: string, backTo?: string) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID, backTo)); +function navigateToBankAccountRoute(policyID: string | undefined, backTo?: string) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, '', backTo)); } export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index ea8aa5414c7b..fa6046c6baa4 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1361,7 +1361,11 @@ function getNewerActions(reportID: string | undefined, reportActionID: string | /** * Gets metadata info about links in the provided report action */ -function expandURLPreview(reportID: string, reportActionID: string) { +function expandURLPreview(reportID: string | undefined, reportActionID: string) { + if (!reportID) { + return; + } + const parameters: ExpandURLPreviewParams = { reportID, reportActionID, @@ -1455,7 +1459,11 @@ function markCommentAsUnread(reportID: string | undefined, reportActionCreated: } /** Toggles the pinned state of the report. */ -function togglePinnedState(reportID: string, isPinnedChat: boolean) { +function togglePinnedState(reportID: string | undefined, isPinnedChat: boolean) { + if (!reportID) { + return; + } + const pinnedValue = !isPinnedChat; // Optimistically pin/unpin the report before we send out the command @@ -1743,14 +1751,13 @@ function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMar } /** Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. */ -function editReportComment(reportID: string, originalReportAction: OnyxEntry, textForNewComment: string, videoAttributeCache?: Record) { +function editReportComment(reportID: string | undefined, originalReportAction: OnyxEntry, textForNewComment: string, videoAttributeCache?: Record) { const originalReportID = getOriginalReportID(reportID, originalReportAction); - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; - const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report); - if (!originalReportID || !originalReportAction) { return; } + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; + const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report); // Do not autolink if someone explicitly tries to remove a link from message. // https://github.com/Expensify/App/issues/9090 @@ -1873,13 +1880,13 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry = { [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null, @@ -2523,7 +2534,7 @@ function deleteReport(reportID: string, shouldDeleteChildReports = false) { /** * @param reportID The reportID of the policy report (workspace room) */ -function navigateToConciergeChatAndDeleteReport(reportID: string, shouldPopToTop = false, shouldDeleteChildReports = false) { +function navigateToConciergeChatAndDeleteReport(reportID: string | undefined, shouldPopToTop = false, shouldDeleteChildReports = false) { // Dismiss the current report screen and replace it with Concierge Chat if (shouldPopToTop) { Navigation.setShouldPopAllStateOnUP(true); @@ -2730,7 +2741,7 @@ function showReportActionNotification(reportID: string, reportAction: ReportActi } /** Clear the errors associated with the IOUs of a given report. */ -function clearIOUError(reportID: string) { +function clearIOUError(reportID: string | undefined) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {errorFields: {iou: null}}); } @@ -2832,7 +2843,7 @@ function removeEmojiReaction(reportID: string, reportActionID: string, emoji: Em * Uses the NEW FORMAT for "emojiReactions" */ function toggleEmojiReaction( - reportID: string, + reportID: string | undefined, reportAction: ReportAction, reactionObject: Emoji, existingReactions: OnyxEntry, @@ -4318,9 +4329,13 @@ function clearNewRoomFormError() { }); } -function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEntry, resolution: ValueOf) { +function resolveActionableMentionWhisper( + reportID: string | undefined, + reportAction: OnyxEntry, + resolution: ValueOf, +) { const message = ReportActionsUtils.getReportActionMessage(reportAction); - if (!message || !reportAction) { + if (!message || !reportAction || !reportID) { return; } @@ -4337,8 +4352,8 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt }, }; - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; - const reportUpdateDataWithPreviousLastMessage = getReportLastMessage(reportId, optimisticReportActions as ReportActions); + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const reportUpdateDataWithPreviousLastMessage = getReportLastMessage(reportID, optimisticReportActions as ReportActions); const reportUpdateDataWithCurrentLastMessage = { lastMessageText: report?.lastMessageText, @@ -4349,7 +4364,7 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportId}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { [reportAction.reportActionID]: { message: [updatedMessage], @@ -4361,7 +4376,7 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportId}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: reportUpdateDataWithPreviousLastMessage, }, ]; @@ -4369,7 +4384,7 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportId}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { [reportAction.reportActionID]: { message: [message], @@ -4381,7 +4396,7 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportId}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: reportUpdateDataWithCurrentLastMessage, // revert back to the current report last message data in case of failure }, ]; @@ -4395,11 +4410,11 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt } function resolveActionableReportMentionWhisper( - reportId: string, + reportId: string | undefined, reportAction: OnyxEntry, resolution: ValueOf, ) { - if (!reportAction) { + if (!reportAction || !reportId) { return; } @@ -4466,10 +4481,10 @@ function resolveActionableReportMentionWhisper( API.write(WRITE_COMMANDS.RESOLVE_ACTIONABLE_REPORT_MENTION_WHISPER, parameters, {optimisticData, failureData}); } -function dismissTrackExpenseActionableWhisper(reportID: string, reportAction: OnyxEntry): void { +function dismissTrackExpenseActionableWhisper(reportID: string | undefined, reportAction: OnyxEntry): void { const isArrayMessage = Array.isArray(reportAction?.message); const message = ReportActionsUtils.getReportActionMessage(reportAction); - if (!message || !reportAction) { + if (!message || !reportAction || !reportID) { return; } diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index 89517a753c26..e8d7949464d8 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -1,12 +1,12 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import * as ReportActionUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getLinkedTransactionID, getReportAction, getReportActionMessage, isCreatedTaskReportAction} from '@libs/ReportActionsUtils'; +import {getOriginalReportID} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type ReportAction from '@src/types/onyx/ReportAction'; -import * as Report from './Report'; +import {deleteReport} from './Report'; type IgnoreDirection = 'parent' | 'child'; @@ -27,7 +27,7 @@ Onyx.connect({ }); function clearReportActionErrors(reportID: string, reportAction: ReportAction, keys?: string[]) { - const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); + const originalReportID = getOriginalReportID(reportID, reportAction); if (!reportAction?.reportActionID) { return; @@ -41,16 +41,16 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction, k // If there's a linked transaction, delete that too // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportAction.reportActionID, originalReportID || '-1'); + const linkedTransactionID = getLinkedTransactionID(reportAction.reportActionID, originalReportID); if (linkedTransactionID) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportAction.childReportID}`, null); } // Delete the failed task report too - const taskReportID = ReportActionUtils.getReportActionMessage(reportAction)?.taskReportID; - if (taskReportID && ReportActionUtils.isCreatedTaskReportAction(reportAction)) { - Report.deleteReport(taskReportID); + const taskReportID = getReportActionMessage(reportAction)?.taskReportID; + if (taskReportID && isCreatedTaskReportAction(reportAction)) { + deleteReport(taskReportID); } return; } @@ -81,9 +81,9 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction, k ignore: `undefined` means we want to check both parent and children report actions ignore: `parent` or `child` means we want to ignore checking parent or child report actions because they've been previously checked */ -function clearAllRelatedReportActionErrors(reportID: string, reportAction: ReportAction | null | undefined, ignore?: IgnoreDirection, keys?: string[]) { +function clearAllRelatedReportActionErrors(reportID: string | undefined, reportAction: ReportAction | null | undefined, ignore?: IgnoreDirection, keys?: string[]) { const errorKeys = keys ?? Object.keys(reportAction?.errors ?? {}); - if (!reportAction || errorKeys.length === 0) { + if (!reportAction || errorKeys.length === 0 || !reportID) { return; } @@ -91,7 +91,7 @@ function clearAllRelatedReportActionErrors(reportID: string, reportAction: Repor const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (report?.parentReportID && report?.parentReportActionID && ignore !== 'parent') { - const parentReportAction = ReportActionUtils.getReportAction(report.parentReportID, report.parentReportActionID); + const parentReportAction = getReportAction(report.parentReportID, report.parentReportActionID); const parentErrorKeys = Object.keys(parentReportAction?.errors ?? {}).filter((err) => errorKeys.includes(err)); clearAllRelatedReportActionErrors(report.parentReportID, parentReportAction, 'child', parentErrorKeys); @@ -101,7 +101,7 @@ function clearAllRelatedReportActionErrors(reportID: string, reportAction: Repor const childActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportAction.childReportID}`] ?? {}; Object.values(childActions).forEach((action) => { const childErrorKeys = Object.keys(action.errors ?? {}).filter((err) => errorKeys.includes(err)); - clearAllRelatedReportActionErrors(reportAction.childReportID ?? '-1', action, 'parent', childErrorKeys); + clearAllRelatedReportActionErrors(reportAction.childReportID, action, 'parent', childErrorKeys); }); } } diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index fa14e6fef7a8..2864b989ed3d 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1251,7 +1251,11 @@ function canActionTask(taskReport: OnyxEntry, sessionAccountID return sessionAccountID === ownerAccountID || sessionAccountID === assigneeAccountID; } -function clearTaskErrors(reportID: string) { +function clearTaskErrors(reportID: string | undefined) { + if (!reportID) { + return; + } + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; // Delete the task preview in the parent report diff --git a/src/libs/actions/getCompanyCardBankConnection/index.tsx b/src/libs/actions/getCompanyCardBankConnection/index.tsx index fb6dd9943972..a136a3783136 100644 --- a/src/libs/actions/getCompanyCardBankConnection/index.tsx +++ b/src/libs/actions/getCompanyCardBankConnection/index.tsx @@ -11,7 +11,7 @@ type CompanyCardBankConnection = { isNewDot: string; }; -export default function getCompanyCardBankConnection(policyID?: string, bankName?: string, scrapeMinDate?: string) { +export default function getCompanyCardBankConnection(policyID?: string, bankName?: string) { const bankConnection = Object.keys(CONST.COMPANY_CARDS.BANKS).find((key) => CONST.COMPANY_CARDS.BANKS[key as keyof typeof CONST.COMPANY_CARDS.BANKS] === bankName); if (!bankName || !bankConnection || !policyID) { @@ -23,7 +23,7 @@ export default function getCompanyCardBankConnection(policyID?: string, bankName isNewDot: 'true', domainName: PolicyUtils.getDomainNameForPolicy(policyID), isCorporate: 'true', - scrapeMinDate: scrapeMinDate ?? '', + scrapeMinDate: '', }; const commandURL = getApiRoot({ shouldSkipWebProxy: true, diff --git a/src/pages/ErrorPage/NotFoundPage.tsx b/src/pages/ErrorPage/NotFoundPage.tsx index 26b6aae5bc0b..51149e0ac775 100644 --- a/src/pages/ErrorPage/NotFoundPage.tsx +++ b/src/pages/ErrorPage/NotFoundPage.tsx @@ -10,10 +10,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; type NotFoundPageProps = { onBackButtonPress?: () => void; isReportRelatedPage?: boolean; + shouldShowOfflineIndicator?: boolean; } & FullPageNotFoundViewProps; // eslint-disable-next-line rulesdir/no-negated-variables -function NotFoundPage({onBackButtonPress = () => Navigation.goBack(), isReportRelatedPage, ...fullPageNotFoundViewProps}: NotFoundPageProps) { +function NotFoundPage({onBackButtonPress = () => Navigation.goBack(), isReportRelatedPage, shouldShowOfflineIndicator, ...fullPageNotFoundViewProps}: NotFoundPageProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to go back to the not found page on large screens and to the home page on small screen // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); @@ -21,7 +22,10 @@ function NotFoundPage({onBackButtonPress = () => Navigation.goBack(), isReportRe const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${topmostReportId}`); return ( - + { diff --git a/src/pages/ReimbursementAccount/BankAccountStep.tsx b/src/pages/ReimbursementAccount/BankAccountStep.tsx index 1314bad9dd6f..2e094d425d16 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.tsx +++ b/src/pages/ReimbursementAccount/BankAccountStep.tsx @@ -19,6 +19,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getEarliestErrorField, getLatestErrorField} from '@libs/ErrorUtils'; import getPlaidDesktopMessage from '@libs/getPlaidDesktopMessage'; +import {REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils'; import {openPlaidView, setBankAccountSubStep} from '@userActions/BankAccounts'; import {openExternalLink, openExternalLinkWithToken} from '@userActions/Link'; import {updateReimbursementAccountDraft} from '@userActions/ReimbursementAccount'; @@ -90,7 +91,7 @@ function BankAccountStep({ subStep = CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID; } const plaidDesktopMessage = getPlaidDesktopMessage(); - const bankAccountRoute = `${ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('new', policyID, ROUTES.WORKSPACE_INITIAL.getRoute(policyID))}`; + const bankAccountRoute = `${ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, ROUTES.WORKSPACE_INITIAL.getRoute(policyID))}`; const personalBankAccounts = bankAccountList ? Object.keys(bankAccountList).filter((key) => bankAccountList[key].accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) : []; useEffect(() => { diff --git a/src/pages/ReimbursementAccount/ConnectBankAccount/components/Enable2FACard.tsx b/src/pages/ReimbursementAccount/ConnectBankAccount/components/Enable2FACard.tsx index 8d35f485474f..27f73c64232f 100644 --- a/src/pages/ReimbursementAccount/ConnectBankAccount/components/Enable2FACard.tsx +++ b/src/pages/ReimbursementAccount/ConnectBankAccount/components/Enable2FACard.tsx @@ -27,7 +27,7 @@ function Enable2FACard({policyID}: Enable2FACardProps) { { title: translate('validationStep.secureYourAccount'), onPress: () => { - Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID))); + Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID))); }, icon: Expensicons.Shield, shouldShowRightIcon: true, diff --git a/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx b/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx index 48da42b8b1ef..0821f100814a 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx @@ -12,7 +12,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@navigation/Navigation'; -import * as Report from '@userActions/Report'; +import {navigateToConciergeChat} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -21,12 +21,12 @@ function Finish() { const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); - const policyID = reimbursementAccount?.achData?.policyID ?? '-1'; + const policyID = reimbursementAccount?.achData?.policyID; const handleBackButtonPress = () => { Navigation.goBack(); }; - const handleNavigateToConciergeChat = () => Report.navigateToConciergeChat(true); + const handleNavigateToConciergeChat = () => navigateToConciergeChat(true); return ( { - Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID))); + Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID))); }, icon: Expensicons.Shield, shouldShowRightIcon: true, diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 25ce1c6be39b..35c89cc72285 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -3,7 +3,6 @@ import lodashPick from 'lodash/pick'; import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -22,12 +21,22 @@ import BankAccount from '@libs/models/BankAccount'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp, PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReimbursementAccountNavigatorParamList} from '@libs/Navigation/types'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getRouteForCurrentStep, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils'; import shouldReopenOnfido from '@libs/shouldReopenOnfido'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; -import * as BankAccounts from '@userActions/BankAccounts'; -import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; +import { + clearOnfidoToken, + goToWithdrawalAccountSetupStep, + hideBankAccountErrors, + openReimbursementAccountPage, + setBankAccountSubStep, + setPlaidEvent, + setReimbursementAccountLoading, + updateReimbursementAccountDraft, +} from '@userActions/BankAccounts'; +import {clearReimbursementAccountDraft} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -52,16 +61,6 @@ import RequestorStep from './RequestorStep'; type ReimbursementAccountPageProps = WithPolicyOnyxProps & PlatformStackScreenProps; -const ROUTE_NAMES = { - COMPANY: 'company', - PERSONAL_INFORMATION: 'personal-information', - BENEFICIAL_OWNERS: 'beneficial-owners', - CONTRACT: 'contract', - VALIDATE: 'validate', - ENABLE: 'enable', - NEW: 'new', -}; - const SUPPORTED_FOREIGN_CURRENCIES: string[] = [CONST.CURRENCY.EUR, CONST.CURRENCY.GBP, CONST.CURRENCY.CAD, CONST.CURRENCY.AUD]; /** @@ -70,45 +69,25 @@ const SUPPORTED_FOREIGN_CURRENCIES: string[] = [CONST.CURRENCY.EUR, CONST.CURREN */ function getStepToOpenFromRouteParams(route: PlatformStackRouteProp): TBankAccountStep | '' { switch (route.params.stepToOpen) { - case ROUTE_NAMES.NEW: + case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW: return CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT; - case ROUTE_NAMES.COMPANY: + case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.COMPANY: return CONST.BANK_ACCOUNT.STEP.COMPANY; - case ROUTE_NAMES.PERSONAL_INFORMATION: + case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.PERSONAL_INFORMATION: return CONST.BANK_ACCOUNT.STEP.REQUESTOR; - case ROUTE_NAMES.BENEFICIAL_OWNERS: + case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.BENEFICIAL_OWNERS: return CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS; - case ROUTE_NAMES.CONTRACT: + case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.CONTRACT: return CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT; - case ROUTE_NAMES.VALIDATE: + case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.VALIDATE: return CONST.BANK_ACCOUNT.STEP.VALIDATION; - case ROUTE_NAMES.ENABLE: + case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.ENABLE: return CONST.BANK_ACCOUNT.STEP.ENABLE; default: return ''; } } -function getRouteForCurrentStep(currentStep: TBankAccountStep): ValueOf { - switch (currentStep) { - case CONST.BANK_ACCOUNT.STEP.COMPANY: - return ROUTE_NAMES.COMPANY; - case CONST.BANK_ACCOUNT.STEP.REQUESTOR: - return ROUTE_NAMES.PERSONAL_INFORMATION; - case CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS: - return ROUTE_NAMES.BENEFICIAL_OWNERS; - case CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT: - return ROUTE_NAMES.CONTRACT; - case CONST.BANK_ACCOUNT.STEP.VALIDATION: - return ROUTE_NAMES.VALIDATE; - case CONST.BANK_ACCOUNT.STEP.ENABLE: - return ROUTE_NAMES.ENABLE; - case CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT: - default: - return ROUTE_NAMES.NEW; - } -} - /** * Returns selected bank account fields based on field names provided. */ @@ -267,7 +246,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const localCurrentStep = isPreviousPolicy ? achData?.currentStep ?? '' : ''; if (policyIDParam) { - BankAccounts.openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep, policyIDParam); + openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep, policyIDParam); } } @@ -278,14 +257,14 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen return; } - BankAccounts.setReimbursementAccountLoading(true); - ReimbursementAccount.clearReimbursementAccountDraft(); + setReimbursementAccountLoading(true); + clearReimbursementAccountDraft(); // If the step to open is empty, we want to clear the sub step, so the connect option view is shown to the user const isStepToOpenEmpty = getStepToOpenFromRouteParams(route) === ''; if (isStepToOpenEmpty) { - BankAccounts.setBankAccountSubStep(null); - BankAccounts.setPlaidEvent(null); + setBankAccountSubStep(null); + setPlaidEvent(null); } fetchData(); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps @@ -321,7 +300,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen if (currentStepRouteParam === currentStep) { // If the user is connecting online with plaid, reset any bank account errors so we don't persist old data from a potential previous connection if (currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT && achData?.subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID) { - BankAccounts.hideBankAccountErrors(); + hideBankAccountErrors(); } // The route is showing the correct step, no need to update the route param or clear errors. @@ -331,14 +310,14 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen // Update the data that is returned from back-end to draft value const draftStep = reimbursementAccount?.draftStep; if (draftStep) { - BankAccounts.updateReimbursementAccountDraft(getBankAccountFields(getFieldsForStep(draftStep))); + updateReimbursementAccountDraft(getBankAccountFields(getFieldsForStep(draftStep))); } if (currentStepRouteParam !== '') { // When we click "Connect bank account", we load the page without the current step param, if there // was an error when we tried to disconnect or start over, we want the user to be able to see the error, // so we don't clear it. We only want to clear the errors if we are moving between steps. - BankAccounts.hideBankAccountErrors(); + hideBankAccountErrors(); } Navigation.setParams({stepToOpen: getRouteForCurrentStep(currentStep)}); @@ -348,7 +327,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen ); const setManualStep = () => { - BankAccounts.setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL).then(() => { + setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL).then(() => { setShouldShowContinueSetupButton(false); }); }; @@ -363,37 +342,37 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen setShouldShowContinueSetupButton(true); } if (subStep) { - BankAccounts.setBankAccountSubStep(null); - BankAccounts.setPlaidEvent(null); + setBankAccountSubStep(null); + setPlaidEvent(null); } else { Navigation.goBack(); } break; case CONST.BANK_ACCOUNT.STEP.COMPANY: - BankAccounts.clearOnfidoToken(); - BankAccounts.goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); + clearOnfidoToken(); + goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); break; case CONST.BANK_ACCOUNT.STEP.REQUESTOR: if (shouldShowOnfido) { - BankAccounts.clearOnfidoToken(); + clearOnfidoToken(); } else { - BankAccounts.goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT); + goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT); } break; case CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS: - BankAccounts.goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.COMPANY); + goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.COMPANY); break; case CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT: - BankAccounts.goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS); + goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS); break; case CONST.BANK_ACCOUNT.STEP.VALIDATION: if ([BankAccount.STATE.VERIFYING, BankAccount.STATE.SETUP].some((value) => value === achData?.state)) { - BankAccounts.goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT); + goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT); } else if (!isOffline && achData?.state === BankAccount.STATE.PENDING) { setShouldShowContinueSetupButton(true); } else { @@ -438,14 +417,14 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen return ; } - if (!isLoading && (isEmptyObject(policy) || !PolicyUtils.isPolicyAdmin(policy))) { + if ((!isLoading && (isEmptyObject(policy) || !isPolicyAdmin(policy))) || isPendingDeletePolicy(policy)) { return ( ); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index b97e7f2c3dd7..663b83c619e1 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -42,7 +42,7 @@ import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuProps = { /** The ID of the report this report action is attached to. */ - reportID: string; + reportID: string | undefined; /** The ID of the report action this context menu is attached to. */ reportActionID: string; @@ -204,6 +204,7 @@ function BaseReportActionContextMenu({ let filteredContextMenuActions = ContextMenuActions.filter( (contextAction) => !disabledActions.includes(contextAction) && + reportID && contextAction.shouldShow({ type, reportAction, diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 91481cd30754..bf5e1b253f3f 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -64,6 +64,7 @@ import { canHoldUnholdReportAction, changeMoneyRequestHoldStatus, getChildReportNotificationPreference as getChildReportNotificationPreferenceReportUtils, + getDeletedTransactionMessage, getDowngradeWorkspaceMessage, getIOUApprovedMessage, getIOUForwardedMessage, @@ -147,7 +148,7 @@ type ShouldShow = (args: { type ContextMenuActionPayload = { reportAction: ReportAction; transaction?: OnyxEntry; - reportID: string; + reportID: string | undefined; report: OnyxEntry; draftMessage: string; selection: string; @@ -545,6 +546,8 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getPolicyChangeLogChangeRoleMessage(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_EMPLOYEE) { setClipboardMessage(getPolicyChangeLogDeleteMemberMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) { + setClipboardMessage(getDeletedTransactionMessage(reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) { const {label, errorMessage} = getOriginalMessage(reportAction) ?? {label: '', errorMessage: ''}; setClipboardMessage(translateLocal('report.actions.type.integrationSyncFailed', {label, errorMessage})); @@ -630,6 +633,10 @@ const ContextMenuActions: ContextMenuAction[] = [ !isChronosReport && reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, onPress: (closePopover, {reportID, reportAction}) => { + if (!reportID) { + return; + } + const activeRoute = Navigation.getActiveRoute(); if (closePopover) { hideContextMenu(false, () => Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute))); diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 14ce17798e51..a9c2d7c83225 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -105,9 +105,9 @@ function showContextMenu( event: GestureResponderEvent | MouseEvent, selection: string, contextMenuAnchor: ContextMenuAnchor, - reportID = '-1', - reportActionID = '-1', - originalReportID = '-1', + reportID: string | undefined = undefined, + reportActionID: string | undefined = undefined, + originalReportID: string | undefined = undefined, draftMessage: string | undefined = undefined, onShow = () => {}, onHide = () => {}, @@ -171,8 +171,8 @@ function hideDeleteModal() { /** * Opens the Confirm delete action modal */ -function showDeleteModal(reportID: string, reportAction: OnyxEntry, shouldSetModalVisibility?: boolean, onConfirm?: OnConfirm, onCancel?: OnCancel) { - if (!contextMenuRef.current) { +function showDeleteModal(reportID: string | undefined, reportAction: OnyxEntry, shouldSetModalVisibility?: boolean, onConfirm?: OnConfirm, onCancel?: OnCancel) { + if (!contextMenuRef.current || !reportID) { return; } contextMenuRef.current.showDeleteModal(reportID, reportAction, shouldSetModalVisibility, onConfirm, onCancel); diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index c74531acf317..e093850ec276 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -11,7 +11,7 @@ import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; +import {Eye} from '@components/Icon/Expensicons'; import InlineSystemMessage from '@components/InlineSystemMessage'; import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -68,7 +68,6 @@ import { isActionableReportMentionWhisper, isActionableTrackExpense, isActionOfType, - isAddCommentAction, isChronosOOOListAction, isCreatedTaskReportAction, isDeletedAction, @@ -89,6 +88,7 @@ import { import { canWriteInReport, chatIncludesConcierge, + getDeletedTransactionMessage, getDisplayNamesWithTooltips, getIconsForParticipants, getIOUApprovedMessage, @@ -231,7 +231,7 @@ type PureReportActionItemProps = { originalReportID?: string; /** Function to deletes the draft for a comment report action. */ - deleteReportActionDraft?: (reportID: string, action: OnyxTypes.ReportAction) => void; + deleteReportActionDraft?: (reportID: string | undefined, action: OnyxTypes.ReportAction) => void; /** Whether the room is archived */ isArchivedRoom?: boolean; @@ -241,7 +241,7 @@ type PureReportActionItemProps = { /** Function to toggle emoji reaction */ toggleEmojiReaction?: ( - reportID: string, + reportID: string | undefined, reportAction: OnyxTypes.ReportAction, reactionObject: Emoji, existingReactions: OnyxEntry, @@ -250,18 +250,18 @@ type PureReportActionItemProps = { ) => void; /** Function to create a draft transaction and navigate to participant selector */ - createDraftTransactionAndNavigateToParticipantSelector?: (transactionID: string, reportID: string, actionName: IOUAction, reportActionID: string) => void; + createDraftTransactionAndNavigateToParticipantSelector?: (transactionID: string | undefined, reportID: string | undefined, actionName: IOUAction, reportActionID: string) => void; /** Function to resolve actionable report mention whisper */ resolveActionableReportMentionWhisper?: ( - reportId: string, + reportId: string | undefined, reportAction: OnyxEntry, resolution: ValueOf, ) => void; /** Function to resolve actionable mention whisper */ resolveActionableMentionWhisper?: ( - reportId: string, + reportId: string | undefined, reportAction: OnyxEntry, resolution: ValueOf, ) => void; @@ -288,10 +288,10 @@ type PureReportActionItemProps = { clearError?: (transactionID: string) => void; /** Function to clear all errors from a report action */ - clearAllRelatedReportActionErrors?: (reportID: string, reportAction: OnyxTypes.ReportAction | null | undefined, ignore?: IgnoreDirection, keys?: string[]) => void; + clearAllRelatedReportActionErrors?: (reportID: string | undefined, reportAction: OnyxTypes.ReportAction | null | undefined, ignore?: IgnoreDirection, keys?: string[]) => void; /** Function to dismiss the actionable whisper for tracking expenses */ - dismissTrackExpenseActionableWhisper?: (reportID: string, reportAction: OnyxEntry) => void; + dismissTrackExpenseActionableWhisper?: (reportID: string | undefined, reportAction: OnyxEntry) => void; /** User payment card ID */ userBillingFundID?: number; @@ -354,7 +354,7 @@ function PureReportActionItem({ }: PureReportActionItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const reportID = report?.reportID ?? ''; + const reportID = report?.reportID; const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -556,7 +556,7 @@ function PureReportActionItem({ const contextValue = useMemo( () => ({ anchor: popoverAnchorRef.current, - report: {...report, reportID: report?.reportID ?? ''}, + report, reportNameValuePairs, action, transactionThreadReport, @@ -568,8 +568,7 @@ function PureReportActionItem({ const attachmentContextValue = useMemo(() => ({reportID, type: CONST.ATTACHMENT_TYPE.REPORT}), [reportID]); - const mentionReportContextValue = useMemo(() => ({currentReportID: report?.reportID ?? '-1'}), [report?.reportID]); - + const mentionReportContextValue = useMemo(() => ({currentReportID: report?.reportID}), [report?.reportID]); const actionableItemButtons: ActionableItem[] = useMemo(() => { if (isActionableAddPaymentCard(action) && userBillingFundID === undefined && shouldRenderAddPaymentCard()) { return [ @@ -596,7 +595,7 @@ function PureReportActionItem({ text: 'actionableMentionTrackExpense.submit', key: `${action.reportActionID}-actionableMentionTrackExpense-submit`, onPress: () => { - createDraftTransactionAndNavigateToParticipantSelector(transactionID ?? '0', reportID, CONST.IOU.ACTION.SUBMIT, action.reportActionID); + createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportID, CONST.IOU.ACTION.SUBMIT, action.reportActionID); }, isMediumSized: true, }, @@ -604,7 +603,7 @@ function PureReportActionItem({ text: 'actionableMentionTrackExpense.categorize', key: `${action.reportActionID}-actionableMentionTrackExpense-categorize`, onPress: () => { - createDraftTransactionAndNavigateToParticipantSelector(transactionID ?? '0', reportID, CONST.IOU.ACTION.CATEGORIZE, action.reportActionID); + createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportID, CONST.IOU.ACTION.CATEGORIZE, action.reportActionID); }, isMediumSized: true, }, @@ -612,7 +611,7 @@ function PureReportActionItem({ text: 'actionableMentionTrackExpense.share', key: `${action.reportActionID}-actionableMentionTrackExpense-share`, onPress: () => { - createDraftTransactionAndNavigateToParticipantSelector(transactionID ?? '0', reportID, CONST.IOU.ACTION.SHARE, action.reportActionID); + createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportID, CONST.IOU.ACTION.SHARE, action.reportActionID); }, isMediumSized: true, }, @@ -703,11 +702,11 @@ function PureReportActionItem({ getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK) ) { // There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID - const iouReportID = getOriginalMessage(action)?.IOUReportID ? getOriginalMessage(action)?.IOUReportID?.toString() ?? '-1' : '-1'; + const iouReportID = getOriginalMessage(action)?.IOUReportID?.toString(); children = ( ); } else if (isReimbursementQueuedAction(action)) { const linkedReport = isChatThread(report) ? parentReport : report; - const submitterDisplayName = formatPhoneNumber(getDisplayNameOrDefault(personalDetails?.[linkedReport?.ownerAccountID ?? -1])); + const submitterDisplayName = formatPhoneNumber(getDisplayNameOrDefault(personalDetails?.[linkedReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID])); const paymentType = getOriginalMessage(action)?.paymentType ?? ''; children = ( @@ -862,6 +861,8 @@ function PureReportActionItem({ children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) { + children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MERGED_WITH_CASH_TRANSACTION) { children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION)) { @@ -1057,7 +1058,7 @@ function PureReportActionItem({ }; if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const transactionID = isMoneyRequestAction(parentReportActionForTransactionThread) ? getOriginalMessage(parentReportActionForTransactionThread)?.IOUTransactionID : '-1'; + const transactionID = isMoneyRequestAction(parentReportActionForTransactionThread) ? getOriginalMessage(parentReportActionForTransactionThread)?.IOUTransactionID : undefined; return ( 1; - const iouReportID = isMoneyRequestAction(action) && getOriginalMessage(action)?.IOUReportID ? (getOriginalMessage(action)?.IOUReportID ?? '').toString() : '-1'; + const iouReportID = isMoneyRequestAction(action) && getOriginalMessage(action)?.IOUReportID ? getOriginalMessage(action)?.IOUReportID?.toString() : undefined; const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); const isWhisper = whisperedTo.length > 0 && transactionsWithReceipts.length === 0; const whisperedToPersonalDetails = isWhisper - ? (Object.values(personalDetails ?? {}).filter((details) => whisperedTo.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) + ? (Object.values(personalDetails ?? {}).filter((details) => whisperedTo.includes(details?.accountID ?? CONST.DEFAULT_NUMBER_ID)) as OnyxTypes.PersonalDetails[]) : []; const isWhisperOnlyVisibleByUser = isWhisper && isCurrentUserTheOnlyParticipant(whisperedTo); const displayNamesWithTooltips = isWhisper ? getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; @@ -1173,7 +1174,7 @@ function PureReportActionItem({ diff --git a/src/pages/home/report/ReportActionItemContentCreated.tsx b/src/pages/home/report/ReportActionItemContentCreated.tsx index 80867a9be95b..7c0016b56338 100644 --- a/src/pages/home/report/ReportActionItemContentCreated.tsx +++ b/src/pages/home/report/ReportActionItemContentCreated.tsx @@ -15,7 +15,7 @@ import UnreadActionIndicator from '@components/UnreadActionIndicator'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isMessageDeleted, isReversedTransaction, isTransactionThread} from '@libs/ReportActionsUtils'; +import {isMessageDeleted, isReversedTransaction as isReversedTransactionReportActionsUtils, isTransactionThread} from '@libs/ReportActionsUtils'; import {isCanceledTaskReport, isExpenseReport, isInvoiceReport, isIOUReport, isTaskReport} from '@libs/ReportUtils'; import {getCurrency} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; @@ -29,10 +29,7 @@ import ReportActionItemSingle from './ReportActionItemSingle'; type ReportActionItemContentCreatedProps = { /** The context value containing the report and action data, along with the show context menu props */ - contextValue: ShowContextMenuContextProps & { - report: OnyxTypes.Report; - action: OnyxTypes.ReportAction; - }; + contextValue: ShowContextMenuContextProps; /** Report action belonging to the report's parent */ parentReportAction: OnyxEntry; @@ -52,8 +49,8 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans const {translate} = useLocalize(); const {report, action, transactionThreadReport} = contextValue; - const policy = usePolicy(report.policyID === CONST.POLICY.OWNER_EMAIL_FAKE ? undefined : report.policyID); - const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`); + const policy = usePolicy(report?.policyID === CONST.POLICY.OWNER_EMAIL_FAKE ? undefined : report?.policyID); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const transactionCurrency = getCurrency(transaction); @@ -61,7 +58,7 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans () => shouldHideThreadDividerLine ? ( ) : ( @@ -70,18 +67,18 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans style={[!shouldHideThreadDividerLine ? styles.reportHorizontalRule : {}]} /> ), - [shouldHideThreadDividerLine, report.reportID, styles.reportHorizontalRule], + [shouldHideThreadDividerLine, report?.reportID, styles.reportHorizontalRule], ); const contextMenuValue = useMemo(() => ({...contextValue, isDisabled: true}), [contextValue]); if (isTransactionThread(parentReportAction)) { - const isTransactionReversed = isReversedTransaction(parentReportAction); + const isReversedTransaction = isReversedTransactionReportActionsUtils(parentReportAction); - if (isMessageDeleted(parentReportAction) || isTransactionReversed) { + if (isMessageDeleted(parentReportAction) || isReversedTransaction) { let message: TranslationPaths; - if (isTransactionReversed) { + if (isReversedTransaction) { message = 'parentReportAction.reversedTransaction'; } else { message = 'parentReportAction.deletedExpense'; @@ -96,7 +93,7 @@ function ReportActionItemContentCreated({contextValue, parentReportAction, trans showHeader report={report} > - ${translate(message)}`} /> + ${translate(message)}`} /> @@ -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) && (