= {
- production: '',
+ production: '-dark',
staging: '-stg',
dev: '-dev',
adhoc: '-adhoc',
diff --git a/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-AUD.md
similarity index 100%
rename from docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md
rename to docs/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-AUD.md
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD.md
new file mode 100644
index 000000000000..4ae2c669561f
--- /dev/null
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD.md
@@ -0,0 +1,161 @@
+---
+title: Business Bank Accounts - USD
+description: How to add/remove Business Bank Accounts (US)
+---
+# Overview
+Adding a verified business bank account unlocks a myriad of features and automation in Expensify.
+Once you connect your business bank account, you can:
+- Pay employee expense reports via direct deposit (US)
+- Settle company bills via direct transfer
+- Accept invoice payments through direct transfer
+- Access the Expensify Card
+
+# How to add a verified business bank account
+To connect a business bank account to Expensify, follow the below steps:
+1. Go to **Settings > Account > Payments**
+2. Click **Add Verified Bank Account**
+3. Click **Log into your bank**
+4. Click **Continue**
+5. When you hit the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access
+6. Login to the business bank account
+- If the bank is not listed, click the X to go back to the connection type
+- Here you’ll see the option to **Connect Manually**
+- Enter your account and routing numbers
+7. Enter your bank login credentials.
+- If your bank requires additional security measures, you will be directed to obtain and enter a security code
+- If you have more than one account available to choose from, you will be directed to choose the desired account
+Next, to verify the bank account, you’ll enter some details about the business as well as some personal information.
+
+## Enter company information
+This is where you’ll add the legal business name as well as several other company details.
+
+### Company address
+The company address must:
+- Be located in the US
+- Be a physical location
+If you input a maildrop address (PO box, UPS Store, etc.), the address will likely be flagged for review and adding the bank account to Expensify will be delayed.
+
+### Tax Identification Number
+This is the identification number that was assigned to the business by the IRS.
+### Company website
+A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com.
+### Industry Classification Code
+You can locate a list of Industry Classification Codes here.
+## Enter personal information
+Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section:
+- The address must be a physical address
+- The address must be located in the US
+- The SSN must be US-issued
+This does not need to be a signor on the bank account. If someone other than the Expensify account holder enters their personal information in this section, the details will be flagged for review and adding the bank account to Expensify will be delayed.
+
+## Upload ID
+After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following:
+1. Upload the front and back of your ID
+2. Use your device to take a selfie and record a short video of yourself
+It’s required that your ID is:
+- Issued in the US
+- Unexpired
+
+## Additional Information
+Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate:
+- A Beneficial Owner refers to an **individual** who owns 25% or more of the business.
+- If you or another **individual** owns 25% or more of the business, please check the appropriate box
+- If someone else owns 25% or more of the business, you will be prompted to provide their personal information
+If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section.
+
+# How to validate the bank account
+The account you set up can be found under **Settings > Account > Payment > Bank Accounts** section in either **Verifying** or **Pending** status.
+If it is **Verifying**, then this means we sent you a message and need more information from you. Please check your Concierge chat which should include a message with specific details about what we require to move forward.
+If it is **Pending**, then in 1-2 business days Expensify will administer 3 test transactions to your bank account. Please check your Concierge chat for further instructions. If you do not see these test transactions
+After these transactions (2 withdrawals and 1 deposit) have been processed in your account, visit your Expensify Inbox, where you'll see a prompt to input the transaction amounts.
+Once you've finished these steps, your business bank account is ready to use in Expensify!
+
+# How to share a verified bank account
+Only admins with access to the verified bank account can reimburse employees or pay vendor bills. To grant another admin access to the bank account in Expensify, go to **Settings > Account > Payments > Bank Accounts** and click **"Share"**. Enter their email address, and they will receive instructions from us. Please note, they must be a policy admin on a policy you also have access to in order to share the bank account with them.
+When a bank account is shared, it must be revalidated with three new microtransactions to ensure the shared admin has access. This process takes 1-2 business days. Once received, the shared admin can enter the transactions via their Expensify account's Inbox tab.
+
+Note: A report is shared with all individuals with access to the same business bank account in Expensify for audit purposes.
+
+
+# How to remove access to a verified bank account
+This step is important when accountants and staff leave your business.
+To remove an admin's access to a shared bank account, go to **Settings > Account > Payments > Shared Business Bank Accounts**.
+You'll find a list of individuals who have access to the bank account. Next to each user, you'll see the option to Unshare the bank account.
+
+# How to delete a verified bank account
+If you need to delete a bank account from Expensify, run through the following steps:
+1. Head to **Settings > Account > Payments**
+2. Click the red **Delete** button under the corresponding bank account
+
+Be cautious, as if it hasn't been shared with someone else, the next user will need to set it up from the beginning.
+
+If the bank account is set as the settlement account for your Expensify Cards, you’ll need to designate another bank account as your settlement account under **Settings > Domains > Company Cards > Settings** before this account can be deleted.
+
+# Deep Dive
+
+## Verified bank account requirements
+
+To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US) or utilize the Expensify Card:
+- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location.
+- If you are adding the bank account to Expensify, you must add it from **your** Expensify account settings.
+- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US issued photo ID. For utilizing features related to US ACH, your idea must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address
+- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify.
+
+## Locked bank account
+When you reimburse a report, you authorize Expensify to withdraw the funds from your account. If your bank rejects Expensify’s withdrawal request, your verified bank account is locked until the issue is resolved.
+
+Withdrawal requests can be rejected due to insufficient funds, or if the bank account has not been enabled for direct debit.
+If you need to enable direct debits from your verified bank account, your bank will require the following details:
+- The ACH CompanyIDs (1270239450 and 4270239450)
+- The ACH Originator Name (Expensify)
+To request to unlock the bank account, click **Fix** on your bank account under **Settings > Account > Payments > Bank Accounts**.
+This sends a request to our support team to review exactly why the bank account was locked.
+Please note, unlocking a bank account can take 4-5 business days to process.
+
+## Error adding ID to Onfido
+Expensify is required by both our sponsor bank and federal law to verify the identity of the individual that is initiating the movement of money. We use Onfido to confirm that the person adding a payment method is genuine and not impersonating someone else.
+
+If you get a generic error message that indicates, "Something's gone wrong", please go through the following steps:
+
+1. Ensure you are using either Safari (on iPhone) or Chrome (on Android) as your web browser.
+2. Check your browser's permissions to make sure that the camera and microphone settings are set to "Allow"
+3. Clear your web cache for Safari (on iPhone) or Chrome (on Android).
+4. If using a corporate Wi-Fi network, confirm that your corporate firewall isn't blocking the website.
+5. Make sure no other apps are overlapping your screen, such as the Facebook Messenger bubble, while recording the video.
+6. On iPhone, if using iOS version 15 or later, disable the Hide IP address feature in Safari.
+7. If possible, try these steps on another device
+8. If you have another phone available, try to follow these steps on that device
+If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance.
+
+{% include faq-begin.md %}
+## What is a Beneficial Owner?
+
+A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner.
+
+
+## What do I do if the Beneficial Owner section only asks for personal details, but our business is owned by another company?
+
+
+Please only indicate you have a Beneficial Owner, if it is an individual that owns 25% or more of the business.
+
+## Why can’t I input my address or upload my ID?
+
+
+Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account, and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account, and then share access with you once verified.
+
+
+## Why am I being asked for documentation when adding my bank account?
+When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place.
+If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc.
+
+If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist.
+
+
+## I don’t see all three microtransactions I need to validate my bank account. What should I do?
+
+It's a good idea to wait till the end of that second business day. If you still don’t see them, please reach out to your bank and ask them to whitelist our ACH ID's **1270239450** and **4270239450**. Expensify’s ACH Originator Name is "Expensify".
+
+Make sure to reach out to your Account Manager or to Concierge once you have done so and our team will be able to re-trigger those 3 transactions!
+
+
+{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Deposit-Accounts-AUD.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Deposit-Accounts-AUD.md
new file mode 100644
index 000000000000..e274cb3d5b60
--- /dev/null
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/Deposit-Accounts-AUD.md
@@ -0,0 +1,21 @@
+---
+title: Deposit Accounts (AUD)
+description: Expensify allows you to add a personal bank account to receive reimbursements for your expenses. We never take money out of this account — it is only a place for us to deposit funds from your employer. This article covers deposit accounts for Australian banks.
+---
+
+## How-to add your Australian personal deposit account information
+1. Confirm with your Policy Admin that they’ve set up Global Reimbursment
+2. Set your default policy (by selecting the correct policy after clicking on your profile picture) before adding your deposit account.
+3. Go to **Settings > Account > Payments** and click **Add Deposit-Only Bank Account**
+{:width="100%"}
+
+4. Enter your BSB, account number and name. If your screen looks different than the image below, that means your company hasn't enabled reimbursements through Expensify. Please contact your administrator and ask them to enable reimbursements.
+
+{:width="100%"}
+
+# How-to delete a bank account
+Bank accounts are easy to delete! Simply click the red **Delete** button in the bank account under **Settings > Account > Payments**.
+
+{:width="100%"}
+
+You can complete this process on a computer or on the mobile app.
diff --git a/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-USD.md b/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-USD.md
deleted file mode 100644
index 0bc5cb0ad955..000000000000
--- a/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-USD.md
+++ /dev/null
@@ -1,77 +0,0 @@
----
-title: Deposit Accounts - USD
-description: How to add a deposit account to receive payments for yourself or your business (US)
----
-# Overview
-
-There are two types of deposit-only accounts:
-
-1. If you're an employee seeking reimbursement for expenses you’ve incurred, you’ll add a **Personal deposit-only bank account**.
-2. If you're a vendor seeking payment for goods or services, you’ll add a **Business deposit-only account**.
-
-# How to connect a personal deposit-only bank account
-
-**Connect a personal deposit-only bank account if you are:**
-
-- An employee based in the US who gets reimbursed by their employer
-- An employee based in Australia who gets reimbursed by their company via batch payments
-- An international (non-US) employee whose US-based employers send international reimbursements
-
-**To establish the connection to a personal bank account, follow these steps:**
-
-1. Navigate to your **Settings > Account > Payments** and click the **Add Deposit-Only Bank Account** button.
-2. Click **Log into your bank** button and click **Continue** on the Plaid connection pop-up window.
-3. Search for your bank account in the list of banks and follow the prompts to sign-in to your bank account.
-4. Enter your bank login credentials when prompted.
- - If your bank doesn't appear, click the 'x' in the upper right corner of the Plaid pop-up window and click **Connect Manually**.
- - Enter your account information, then click **Save & Continue**.
-
-You should be all set! You’ll receive reimbursement for your expense reports directly to this bank account.
-
-# How to connect a business deposit-only bank account
-
-**Connect a business deposit-only bank account if you are:**
-
-- A US-based vendor who wants to be paid directly for bills sent to customers/clients
-- A US-based vendor who want to pay invoices directly via Expensify
-
-**To establish the connection to a business bank account, follow these steps:**
-
-1. Navigate to your **Settings > Account > Payments and click the Add Deposit-Only Bank Account** button.
-2. Click **Log into your bank** button and click **Continue** on the Plaid connection pop-up window.
-3. Search for your bank account in the list of banks and follow the prompts to sign-in to your bank account.
-4. Enter your bank login credentials when prompted.
- - If your bank doesn't appear, click the 'x' in the upper right corner of the Plaid pop-up window and click **Connect Manually**.
- - Enter your account information, then click **Save & Continue**.
-5. If you see the option to “Switch to Business” after entering the account owner information, click that link.
-6. Enter your Company Name and FEIN or TIN information.
-7. Enter your company’s website formatted as https://www.domain.com.
-
-You should be all set! The bank account will display as a deposit-only business account, and you’ll be paid directly for any invoices you submit for payment.
-
-# How to delete a deposit-only bank account
-
-**To delete a deposit-only bank account, do the following:**
-
-1. Navigate to **Settings > Account > Payments > Bank Accounts**
-2. Click the **Delete** next to the bank account you want to remove
-
-{% include faq-begin.md %}
-
-## **What happens if my bank requires an additional security check before adding it to a third-party?**
-
-If your bank account has 2FA enabled or another security step, you should be prompted to complete this when adding the account. If not, and you encounter an error, you can always select the option to “Connect Manually”. Either way, please double check that you are entering the correct bank account details to ensure successful payments.
-
-## **What if I also want to pay employees with my business bank account?**
-
-If you’ve added a business deposit-only account and also wish to also pay employees, vendors, or utilize the Expensify Card with this bank account, select “Verify” on the listed bank account. This will take you through the additional verification steps to use this account to issue payments.
-
-## **I connected my deposit-only bank account – Why haven’t I received my reimbursement?**
-
-There are a few reasons a reimbursement may be unsuccessful. The first step is to review the estimated deposit date on the report. If it’s after that date and you still haven’t seen the funds, it could have been unsuccessful because:
- - The incorrect account was added. If you believe you may have entered the wrong account, please reach out to Concierge and provide the Report ID for the missing reimbursement.
- - Your account wasn’t set up for Direct Deposit/ACH. You may want to contact your bank to confirm.
-
-If you aren’t sure, please reach out to Concierge and we can assist!
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md b/docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md
deleted file mode 100644
index 5a27f58cf2e8..000000000000
--- a/docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-title: Invite Members
-description: Learn how add your employees to submit expenses in Expensify
----
-# Overview
-
-To invite your employees to Expensify, simply add them as members to your Workspace.
-
-# How to Invite members to Expensify
-
-## Inviting Members Manually
-
-Navigate to **Settings > Workspace > Group > *Workspace Name* > People** - then click **Invite** and enter the invitee's email address.
-
-Indicate whether you want them to be an Employee, Admin, or Auditor on the Workspace.
-
-If you are utilizing the Advanced Approval feature and the invitee is an approver, you can use the "Approves to" field to specify to whom they approve and forward reports for additional approval.
-
-## Inviting Members to a Workspace in Bulk
-
-Navigate to **Settings > Workspaces > Group > *Workspace Name* > People** - then click Invite and enter all of the email addresses separated by comma. Indicate whether you want them to be an Employee, Admin, or Auditor on the Workspace.
-
-If you are utilizing the Advanced Approval feature, you can specify who each member should submit their expense reports to and who an approver should send approved reports to for the next step in the approval process. If someone is the final approver, you can leave this field blank.
-
-Another convenient method is to employ the spreadsheet bulk upload option for inviting members to a Workspace. This proves particularly helpful when initially configuring your system or when dealing with numerous member updates. Simply click the "Import from Spreadsheet" button and upload a file in formats such as .csv, .txt, .xls, or .xlsx to streamline the process.
-
-After uploading the spreadsheet, we'll display a window where you can choose which columns to import and what they correspond to. These are the fields:
-- Email
-- Role
-- Custom Field 1
-- Custom Field 2
-- Submits To
-- Approves To
-- Approval Limit
-- Over Limit Forward To
-
-Click the **Import** button and you're done. We will import the new members with the optional settings and update any already existing ones.
-
-## Inviting Members with a Shareable Workspace Joining Link
-
-You have the ability to invite your colleagues to join your Expensify Workspace by sharing a unique Workspace Joining Link. You can use this link as many times as necessary to invite multiple members through various communication methods such as internal emails, chats, text messages, and more.
-
-To find your unique link, simply go to **Settings > Workspace > Group > *Workspace Name* > People**.
-
-## Allowing Members to Automatically Join Your Workspace
-
-You can streamline the process of inviting colleagues to your Workspace by enabling the Pre-approve switch located below your Workspace Joining Link. This allows teammates to automatically become part of your Workspace as soon as they create an Expensify account using their work email address.
-
-Here's how it works: If a colleague signs up with a work email address that matches the email domain of a company Workspace owner (e.g., if the Workspace owner's email is admin@expensify.com and the colleague signs up with employee@expensify.com), they will be able to join your Workspace seamlessly without requiring a manual invitation. When new members join the Workspace, they will be set up to submit their expense reports to the Workspace owner by default.
-
-To enable this feature, go to **Settings > Workspace > Group > *Workspace Name* > People**.
-
-
-{% include faq-begin.md %}
-## Who can invite members to Expensify
-Any Workspace Admin can add members to a Group Workspace using any of the above methods.
-
-## How can I customize an invite message?
-Under **Settings > Workspace > Group > *Workspace Name* > People > Invite** you can enter a custom message you'd like members to receive in their invitation email.
-
-## How can I invite members via the API?
-If you would like to integrate an open API HR software, you can use our [Advanced Employee Updater API](https://integrations.expensify.com/Integration-Server/doc/employeeUpdater/) to invite members to your Workspace.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/copilots-and-delegates/Removing-Members.md b/docs/articles/expensify-classic/copilots-and-delegates/Removing-Members.md
deleted file mode 100644
index 65acc3630582..000000000000
--- a/docs/articles/expensify-classic/copilots-and-delegates/Removing-Members.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-title: Remove a Workspace Member
-description: How to remove a member from a Workspace in Expensify
----
-
-Removing a member from a workspace disables their ability to use the workspace. Please note that it does not delete their account or deactivate the Expensify Card.
-
-## How to Remove a Workspace Member
-1. Important: Make sure the employee has submitted all Draft reports and the reports have been approved, reimbursed, etc.
-2. Go to Settings > Workspaces > Group > [Workspace Name] > Members > Workspace Members
-3. Select the member you'd like to remove and click the **Remove** button at the top of the Members table.
-4. If this member was an approver, make sure that reports are not routing to them in the workflow.
-
-{:width="100%"}
-
-{% include faq-begin.md %}
-
-## Will reports from this member on this workspace still be available?
-Yes, as long as the reports have been submitted. You can navigate to the Reports page and enter the member's email in the search field to find them. However, Draft reports will be removed from the workspace, so these will no longer be visible to the Workspace Admin.
-
-## Can members still access their reports on a workspace after they have been removed?
-Yes. Any report that has been approved will now show the workspace as “(not shared)” in their account. If it is a Draft Report they will still be able to edit it and add it to a new workspace. If the report is Approved or Reimbursed they will not be able to edit it further.
-
-## Who can remove members from a workspace?
-Only Workspace Admins. It is not possible for a member to add or remove themselves from a workspace. It is not possible for a Domain Admin who is not also a Workspace Admin to remove a member from a workspace.
-
-## How do I remove a member from a workspace if I am seeing an error message?
-If a member is a **preferred exporter, billing owner, report approver** or has **processing reports**, to remove them the workspace you will first need to:
-
-* **Preferred Exporter** - go to Settings > Workspaces > Group > [Workspace Name] > Connections > Configure and select a different Workspace Admin in the dropdown for **Preferred Exporter**.
-* **Billing Owner** - take over billing on the Settings > Workspaces > Group > [Workspace Name] > Overview page.
-* **Processing reports** - approve or reject the member’s reports on your Reports page.
-* **Approval Workflow** - remove them as a workflow approver on your Settings > Workspaces > Group > [Workspace Name] > Members > Approval Mode > page by changing the "**Submit reports to**" field.
-
-## How do I remove a user completely from a company account?
-If you have a Control Workspace and have Domain Control enabled, you will need to remove them from the domain to delete members' accounts entirely and deactivate the Expensify Card.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/reports/Add-comments-and-attachments-to-a-report.md b/docs/articles/expensify-classic/reports/Add-comments-and-attachments-to-a-report.md
new file mode 100644
index 000000000000..b647a02190bc
--- /dev/null
+++ b/docs/articles/expensify-classic/reports/Add-comments-and-attachments-to-a-report.md
@@ -0,0 +1,32 @@
+---
+title: Add comments & attachments to a report
+description: Add clarification for expenses by adding comments and attachments to a report
+---
+
+
+You can add comments and attachments to a report to help clarify or provide justification for the expenses.
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab.
+2. Click the report.
+3. Scroll down to the bottom of the report and add a comment or attachment.
+ - **To add a comment**: Type a comment into the field and click the Send icon, or press the Enter key on your keyboard.
+ - **To add an attachment**: Click the paperclip icon, then select a jpeg, jpg, png, gif, csv, or pdf file to attach to the report. Then click **Upload**.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap **Reports**.
+3. Tap the report.
+4. At the bottom of the report, add a comment or attachment.
+ - **To add a comment**: Type a comment into the field and click the Send icon.
+ - **To add an attachment**: Click the paperclip icon, then select a jpeg, jpg, png, gif, csv, or pdf file to attach to the report. Then click **Confirm**.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+In this section at the bottom of the report, Expensify also logs actions taken on the report.
+
+
diff --git a/docs/articles/expensify-classic/reports/Add-expenses-to-a-report.md b/docs/articles/expensify-classic/reports/Add-expenses-to-a-report.md
new file mode 100644
index 000000000000..e3c6c5fa2a46
--- /dev/null
+++ b/docs/articles/expensify-classic/reports/Add-expenses-to-a-report.md
@@ -0,0 +1,81 @@
+---
+title: Add expenses to a report
+description: Put your expenses on a report to submit them for reimbursement
+---
+
+
+Once you’ve created your expenses, they may be automatically added to an expense report if your company has this feature enabled. If not, your next step will be to add your expenses to a report and submit them for payment.
+
+You can either create a new report or add expenses to an existing report.
+
+{% include info.html %}
+There may be restrictions on your ability to create reports depending on your workspace settings.
+{% include end-info.html %}
+
+# Add expenses to an existing report
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab.
+2. Click the report.
+3. Click **Add Expenses** at the top of the report.
+4. Select the expenses to add to the report.
+ - If an expense you already added does not appear in the list, use the filter on the left to search by the merchant name or change the date range. *Note: Only expenses that are not already on a report will appear.*
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap **Reports**.
+3. Tap the report.
+4. Tap **Add Expense**, then tap an expense to add it to the report.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Create a new report
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab.
+ - If a report has been automatically created for your most recently submitted expense, then you don’t have to do anything else—your report is already created and will also be automatically submitted.
+ - If a report has not been automatically created, follow the steps below.
+2. Click **New Report**, or click the New Report dropdown and select **Expense Report** (*The other report types are explained in the FAQ section below*).
+3. Click **Add Expenses**.
+4. Click an expense to add it to the report.
+ - If an expense you already added does not appear in the list, use the filter on the left to search by the merchant name or change the date range. *Note: Only expenses that are not already on a report will appear.*
+5. Once all your expenses are added to the report, click the X to close the pop-up.
+6. (Optional) Make any desired changes to the report and/or expenses.
+ - Click the Edit icon next to the report name to change it. If this icon is not visible, the option has been disabled by your workspace.
+ - Click the X icon next to an expense to remove it from the report.
+ - Click the Expense Details icon to review or edit the expense details.
+ - At the bottom of the report, add comments to include more information.
+ - Click the Attachments icon to add additional attachments.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap **Reports**.
+ - If a report has been automatically created for your most recently submitted expense, then you don’t have to do anything else—your report is already created and will also be automatically submitted.
+ - If a report has not been automatically created, follow the steps below.
+3. Tap the + icon and tap **Expense Report** (*The other report types are explained in the FAQ section below*).
+4. Tap **Add Expenses**, then tap an expense to add it to the report. Repeat this step until all desired expenses are added. *Note: Only expenses that are not already on a report will appear.*
+5. (Optional) Make any desired changes to the report and/or expenses.
+ - Tap the report name to change it.
+ - Tap an expense to review or edit the expense details.
+ - At the bottom of the report, add comments to include more information.
+ - Click the Attachments icon to add additional attachments.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# FAQs
+
+**What’s the difference between expense reports, bills, and invoices?**
+
+- **Expense Report**: Expense reports are submitted by an employee to their employer. This may include reimbursable expenses like business travel paid for with personal funds, or non-reimbursable expenses like a lunch paid for with a company card.
+- **Invoice**: Invoices are reports that a business or contractor will send to another business to charge them for goods or services the business received. For example, a contractor that provides an hourly-rate service (like landscaping) may provide their clients with an invoice to detail the different services and products they provided, how many hours they worked, what their rate per hour is for each service, etc. Invoices are generally expected to be paid within a duration of time (for example, within 30 days of receipt).
+- **Bill**: Each invoice will have a matching bill owned by the recipient so they may use it to pay the invoice sender. Bills are for businesses and contractors who provide their client with a bill for goods or services. For example, a restaurant, store, or hair salon provides bills. Bills are generally expected to be paid upon receipt.
+
+
diff --git a/docs/articles/expensify-classic/reports/Edit-a-report.md b/docs/articles/expensify-classic/reports/Edit-a-report.md
new file mode 100644
index 000000000000..b10dd2ce3019
--- /dev/null
+++ b/docs/articles/expensify-classic/reports/Edit-a-report.md
@@ -0,0 +1,133 @@
+---
+title: Edit a report
+description: Make updates to a report
+---
+
+
+You can update a report’s details such as the report title, workspace, report type, layout, and the attached expenses.
+
+{% include info.html %}
+Some report details may be restricted from editing depending on your workspace settings.
+{% include end-info.html %}
+
+# Edit report title
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab and select the report.
+2. Click the pencil icon next to the name and edit the name as desired.
+3. Press Enter on your keyboard to save the changes.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap the **Reports** tab then tap the report.
+3. Tap the report name to edit it.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Change the report workspace
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab and select the report.
+2. Click **Details** in the top right of the report.
+3. Click the Workspace dropdown list and select the correct workspace.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap the **Reports** tab then tap the report.
+3. Tap **Edit** in the top right.
+4. Tap the current workspace name to select a new one.
+5. Tap **Done**.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Change the report type
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab and select the report.
+2. Click **Details** in the top right of the report.
+3. Click the Type dropdown and select either Expense Report or Invoice.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap the **Reports** tab then tap the report.
+3. Tap **Edit** in the top right.
+4. Tap either Expense Report or Invoice.
+5. Tap **Done**.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Change the report layout
+
+1. Click the **Reports** tab and select the report.
+2. Click **Details** in the top right of the report.
+3. Click the view option that you want to change:
+ - **View**: Choose between a basic or detailed report view.
+ - **Group By**: Group expenses on the report based on their category or tag.
+ - **Split By**: Split out the expenses based on their reimbursable or billable status.
+
+# Edit expenses
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab and select the report.
+2. Click **Details** in the top right of the report.
+3. Click the pencil icon at the top of the menu.
+4. Hover over an expense and edit:
+ - A specific field by clicking the pencil icon next to it.
+ - Multiple fields by clicking the pencil icon to the left of the expense.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap the **Reports** tab then tap the report.
+3. Tap an expense to open it.
+4. Tap any field on the expense to edit it.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Remove expenses
+
+{% include info.html %}
+This process only removes the expense from the report—it does not permanently delete the expense.
+{% include end-info.html %}
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab and select the report.
+2. Click the X icon to the left of the expense to remove it from the report.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+
+**Android**
+
+1. Tap the ☰ menu icon in the top left.
+2. Tap the **Reports** tab then tap the report.
+3. Hold the expense and tap Delete to remove it from the report.
+
+**iOS**
+
+1. Tap the ☰ menu icon in the top left.
+2. Tap the **Reports** tab then tap the report.
+3. Swipe the expense to the left and tap Delete to remove it from the report.
+
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+
diff --git a/docs/articles/expensify-classic/reports/Print-or-download-a-report.md b/docs/articles/expensify-classic/reports/Print-or-download-a-report.md
new file mode 100644
index 000000000000..b2e55b09e6c5
--- /dev/null
+++ b/docs/articles/expensify-classic/reports/Print-or-download-a-report.md
@@ -0,0 +1,15 @@
+---
+title: Print or download a report
+description: Share, print, or download a report
+---
+
+
+1. Click the **Reports** tab.
+2. Select the report.
+3. Click **Details** in the top right of the report.
+4. Use the icons at the top to print, download, or share the report.
+ - Click the Print icon to print the report.
+ - Click the Download icon to download a PDF of the report.
+ - Click the Share icon to share the report via email or SMS.
+
+
diff --git a/docs/articles/expensify-classic/reports/Report-Audit-Log-and-Comments.md b/docs/articles/expensify-classic/reports/Report-Audit-Log-and-Comments.md
deleted file mode 100644
index 04183608e3d1..000000000000
--- a/docs/articles/expensify-classic/reports/Report-Audit-Log-and-Comments.md
+++ /dev/null
@@ -1,62 +0,0 @@
----
-title: Report Audit Log and Comments
-description: Details on the expense report audit log and how to leave comments on reports
----
-
-# Overview
-
-At the bottom of each expense report, there’s a section that acts as an audit log for the report. This section details report actions, such as submitting, approving, or exporting. The audit log records the user who completed the action as well as the timestamp for the action.
-
-This section also doubles as the space where submitters, approvers, and admins can converse with each other by leaving comments. Comments trigger notifications to everyone connected to the report and help facilitate communication inside of Expensify.
-
-# How to use the audit log
-
-All report actions are recorded in the audit log. Anytime you need to identify who touched a report or track its progress through the approval process, simply scroll down to the bottom of the report and review the log.
-
-Each recorded action is timestamped - tap or mouse over the timestamp to see the exact date and time the action occurred.
-
-# How to use report comments
-
-There’s a freeform field just under the audit log where you can leave a comment on the report. Type in your comment and click or tap the green arrow to send. The comment will be visible to anyone with visibility on the report, and also automatically sent to anyone who has actioned the report.
-
-# Deep Dive
-
-## Audit log
-
-Here’s a list of actions recorded by the audit log:
-
-- Report creation
-- Report submission
-- Report approval
-- Report reimbursement
-- Exports to accounting or to CSV/Excel files
-- Report and expense rejections
-- Changes made to expenses by approvers/admins
-- Changes made to report fields by approvers/admins
-- Automated actions taken by Concierge
-
-Both manual and automated actions are recorded. If a report action is completed by Concierge, that generally indicates an automation feature triggered the action. For example, an entry that shows a report submitted by Concierge indicates that the **Scheduled Submit** feature is enabled.
-
-Note that timestamps for actions recorded in the log reflect your own timezone. You can either set a static time zone manually, or we can trace your location data to set a time zone automatically for you.
-
-To set your time zone manually, head to **Settings > Account > Preferences > Time Zone** and check **Automatically Set my Time Zone**, or uncheck the box and manually choose your time zone from the searchable list of locations.
-
-## Comments
-
-Anyone with visibility on a report can leave a comment. Comments are interspersed with audit log entries.
-
-Report comments initially trigger a mobile app notification to report participants. If you don't read the notification within a certain amount of time, you'll receive an email notification with the report comment instead. The email will include a link to the report, allowing you to view and add additional comments directly on the report. You can also reply directly to the email, which will record your response as a comment.
-
-Comments can be formatted with bold, italics, or strikethrough using basic Markdown formatting. You can also add receipts and supporting documents to a report by clicking the paperclip icon on the right side of the comment field.
-
-{% include faq-begin.md %}
-
-## Why don’t some timestamps in Expensify match up with what’s shown in the report audit log?
-
-While the audit log is localized to your own timezone, some other features in Expensify (like times shown on the reports page) are not. Those use UTC as a baseline, so it’s possible that some times may look mismatched at first glance. In reality, it’s just a timezone discrepancy.
-
-## Is commenting on a report a billable action?
-
-Yes. If you comment on a report, you become a billable actor for the current month.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/reports/Report-statuses.md b/docs/articles/expensify-classic/reports/Report-statuses.md
new file mode 100644
index 000000000000..7fbdefc5a999
--- /dev/null
+++ b/docs/articles/expensify-classic/reports/Report-statuses.md
@@ -0,0 +1,13 @@
+---
+title: Report statuses
+description: What your report status means
+---
+Each report is given a status based on where it is in the approval process:
+
+- **Open**: The report is “In Progress” and has not yet been submitted. If an open report is also labeled as Rejected, that means that the report was submitted but then rejected by an Approver because it requires adjustments. Open the report to review the comments for clarification on the rejection and next steps to take.
+- **Processing**: The report has been submitted and is pending approval.
+- **Approved**: The report has been approved but has not been reimbursed. For non-reimbursable reports, this is the final status.
+- **Reimbursed**: The report has been successfully reimbursed. If a reimbursed report is also labeled as
+ - **Withdrawing**, an ACH process is initiated.
+ - **Confirmed**, the ACH process is in progress or complete.
+- **Closed**: The report is closed.
diff --git a/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md b/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md
new file mode 100644
index 000000000000..857217189e50
--- /dev/null
+++ b/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md
@@ -0,0 +1,65 @@
+---
+title: Submit or retract a report
+description: Submit a report for reimbursement or retract a submitted report to make corrections
+---
+
+
+Once your report is ready to send, you can submit your expenses for approval. Depending on your workspace settings, your reports may be automatically submitted for you, or you may have to manually submit them.
+
+{% include info.html %}
+Depending on your workspace settings, your reports may be automatically submitted or approved. In this case, you will not need to manually submit your reports.
+{% include end-info.html %}
+
+# Manually submit a report
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab.
+2. Click a report to open it.
+3. Review the report for completion and click **Submit**.
+4. Verify or enter the details for who will receive a notification email about your report and what they will receive:
+ - **To**: Enter the name(s) who will be approving your report (if they are not already listed).
+ - **CC**: Enter the email address of anyone else who should be notified that your expense report has been submitted. Add a comma between each email address if adding more than one.
+ - **Memo**: Enter any relevant notes.
+ - **Attach PDF**: Select this checkbox to attach a copy of your report to the email.
+5. Click **Send**.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap the **Reports** tab.
+3. Tap a report to open it.
+4. Review the report for completion and tap Submit Report.
+5. Verify the details for who will receive a notification email about your report and what they will receive:
+ - **To**: Enter the name(s) who will be approving your report (if they are not already listed).
+ - **CC**: Enter the email address of anyone else who should be notified that your expense report has been submitted. Add a comma between each email address if adding more than one.
+ - **Memo**: Enter any relevant notes.
+ - **Attach PDF**: Select this checkbox to attach a copy of your report to the email.
+6. Tap **Submit**.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Retract a report
+
+You can retract a submitted report to edit the reported expenses and re-submit the report.
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Click the **Reports** tab.
+2. Click a report to open it.
+3. Click **Undo Submit** on the top left of the report.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Tap the ☰ menu icon in the top left.
+2. Tap the **Reports** tab.
+3. Tap a report to open it.
+4. Tap **Retract** at the top of the report.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+
diff --git a/docs/articles/expensify-classic/reports/The-Reports-Page.md b/docs/articles/expensify-classic/reports/The-Reports-Page.md
deleted file mode 100644
index 9c55cd9b4b8d..000000000000
--- a/docs/articles/expensify-classic/reports/The-Reports-Page.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-title: The Reports Page
-description: Details about the Reports Page filters and CSV export options
----
-
-## How to use the Reports Page
-The Reports page is your central hub for a high-level view of a Reports' status. You can see the Reports page on a web browser when you sign into your Expensify account.
-Here, you can quickly see which reports need submission (referred to as **Open**), which are currently awaiting approval (referred to as **Processing**), and which reports have successfully been **Approved** or **Reimbursed**.
-To streamline your experience, we've incorporated user-friendly filters on the Reports page. These filters allow you to refine your report search by specific criteria, such as dates, submitters, or their association with a workspace.
-
-## Report filters
-- **Reset Filters/Show Filters:** You can reset or display your filters at the top of the Reports page.
-- **From & To:** Use these fields to refine your search to a specific date range.
-- **Report ID, Name, or Email:** Narrow your search by entering a Report ID, Report Name, or the submitter's email.
-- **Report Types:** If you're specifically looking for Bills or Invoices, you can select this option.
-- **Submitters:** Choose between "All Submitters" or enter a specific employee's email to view their reports.
-- **Policies:** Select "All Policies" or specify a particular policy associated with the reports you're interested in.
-
-## Report status
-- **Open icon:** These reports are still "In Progress" and must be submitted by the creator. If they contain company card expenses, a domain admin can submit them. If labeled as “Rejected," an Approver has rejected it, typically requiring some adjustments. Click into the report and review the History for any comments from your Approver.
-- **Processing icon:** These reports have been submitted for Approval but have not received the final approval.
-- **Approved icon:** Reports in this category have been Approved but have yet to be Reimbursed. For non-reimbursable reports, this is the final status.
-- **Reimbursed icon:** These reports have been successfully Reimbursed. If you see "Withdrawing," it means the ACH (Automated Clearing House) process is initiated. "Confirmed" indicates the ACH process is in progress or complete. No additional status means your Admin is handling reimbursement outside of Expensify.
-- **Closed icon:** This status represents an officially closed report.
-
-
-## How to Export a report to a CSV
-To export a report to a CSV file, follow these steps on the Reports page:
-
-1. Click the checkbox on the far left of the report row you want to export.
-2. Navigate to the upper right corner of the page and click the "Export to" button.
-3. From the drop-down options that appear, select your preferred export format.
-
-{% include faq-begin.md %}
-## What does it mean if the integration icon for a report is grayed out?
-If the integration icon for a report appears grayed out, the report has yet to be fully exported.
-To address this, consider these options:
-- Go to **Settings > Policies > Group > Connections** within the workspace associated with the report to check for any errors with the accounting integration (i.e., The connection to NetSuite, QuickBooks Online, Xero, Sage Intacct shows an error).
-- Alternatively, click the “Sync Now" button on the Connections page to see if any error prevents the export.
-
-## How can I see a specific expense on a report?
-To locate a specific expense within a report, click on the Report from the Reports page and then click on an expense to view the expense details.
-
-{% include faq-end.md %}
diff --git a/docs/redirects.csv b/docs/redirects.csv
index fac32e6e4e57..51c8c7515e10 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -109,3 +109,53 @@ https://help.expensify.com/articles/expensify-classic/reports/Expense-Rules,http
https://help.expensify.com/articles/expensify-classic/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency
https://help.expensify.com/articles/expensify-classic/reports/The-Expenses-Page,https://help.expensify.com/articles/expensify-classic/expenses/The-Expenses-Page
https://help.expensify.com/articles/expensify-classic/reports/Attendee-Tracking,https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses
+https://help.expensify.com/articles/expensify-classic/account-settings/Close-Account,https://help.expensify.com/articles/expensify-classic/settings/Close-or-reopen-account
+https://help.expensify.com/articles/expensify-classic/account-settings/Copilot,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/
+https://help.expensify.com/articles/expensify-classic/account-settings/Notification-Troubleshooting,https://help.expensify.com/articles/expensify-classic/settings/Notification-Troubleshooting
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription,https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Overview
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Billing-Overview,https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Overview
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Billing-Owner,https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Overview
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription,https://help.expensify.com/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing,https://help.expensify.com/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription,https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Overview
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription,https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Overview
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown,https://help.expensify.com/articles/expensify-classic/expensify-billing/Receipt-Breakdown
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt,https://help.expensify.com/articles/expensify-classic/expensify-billing/Tax-Exempt
+https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approving-Reports,https://help.expensify.com/expensify-classic/hubs/reports/
+https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members,https://help.expensify.com/articles/expensify-classic/workspaces/Invite-members-and-assign-roles
+https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members
+https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Attendee-Tracking,https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses
+https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency
+https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules
+https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Expense-Types,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Types
+https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments,https://help.expensify.com/articles/expensify-classic/reports/Report-Audit-Log-and-Comments
+https://help.expensify.com/articles/expensify-classic/expense-and-report-features/The-Expenses-Page,https://help.expensify.com/articles/expensify-classic/expenses/The-Expenses-Page
+https://help.expensify.com/articles/expensify-classic/expense-and-report-features/The-Reports-Page,https://help.expensify.com/articles/expensify-classic/reports/The-Reports-Page
+https://help.expensify.com/articles/expensify-classic/expenses/Per-Diem-Expenses,https://help.expensify.com/articles/expensify-classic/expenses/Track-per-diem-expenses
+https://help.expensify.com/articles/expensify-classic/get-paid-back/Distance-Tracking,https://help.expensify.com/articles/expensify-classic/expenses/Track-mileage-expenses
+https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Apply-Tax,https://help.expensify.com/articles/expensify-classic/expenses/expenses/Apply-Tax
+https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Create-Expenses,https://help.expensify.com/articles/expensify-classic/expenses/expenses/Add-an-expense
+https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses,https://help.expensify.com/articles/expensify-classic/expenses/expenses/Merge-expenses
+https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts,https://help.expensify.com/articles/expensify-classic/expenses/expenses/Add-an-expense
+https://help.expensify.com/articles/expensify-classic/get-paid-back/Per-Diem-Expenses,https://help.expensify.com/articles/expensify-classic/expenses/Track-per-diem-expenses
+https://help.expensify.com/articles/expensify-classic/get-paid-back/Referral-Program,https://help.expensify.com/articles/new-expensify/expenses/Referral-Program
+https://help.expensify.com/articles/expensify-classic/get-paid-back/reports/Create-A-Report,https://help.expensify.com/articles/expensify-classic/expenses/reports/Create-A-Report
+https://help.expensify.com/articles/expensify-classic/get-paid-back/reports/Reimbursements,https://help.expensify.com/articles/expensify-classic/expenses/reports/Reimbursements
+https://help.expensify.com/articles/expensify-classic/get-paid-back/Trips,https://help.expensify.com/articles/expensify-classic/expenses/Trips
+https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates,https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates
+https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates,https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates
+https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits,https://help.expensify.com/articles/expensify-classic/spending-insights/Fringe-Benefits
+https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Insights,https://help.expensify.com/articles/expensify-classic/spending-insights/Insights
+https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options,https://help.expensify.com/articles/expensify-classic/spending-insights/Other-Export-Options
+https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows
+https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approving-Reports
+https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members
+https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members
+https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/
+https://help.expensify.com/articles/expensify-classic/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency
+https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing-Reports,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports
+https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO,https://help.expensify.com/articles/expensify-classic/settings/Enable-two-factor-authentication
+https://help.expensify.com/articles/expensify-classic/workspaces/Budgets,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets
+https://help.expensify.com/articles/expensify-classic/workspaces/Categories,https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories
+https://help.expensify.com/articles/expensify-classic/workspaces/Tags,https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags
+https://help.expensify.com/expensify-classic/hubs/manage-employees-and-report-approvals,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 23ad07539da1..8b25084df439 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.62.0
+ 1.4.62.4
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 8e8588c64720..96d8c25d570c 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.62.0
+ 1.4.62.4
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 8a1bd000fd9a..0781526824b3 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.62
CFBundleVersion
- 1.4.62.0
+ 1.4.62.4
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 2f53f00918bf..f9244c515a2c 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1836,7 +1836,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.38):
+ - RNLiveMarkdown (0.1.47):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1854,9 +1854,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNLiveMarkdown/common (= 0.1.38)
+ - RNLiveMarkdown/common (= 0.1.47)
- Yoga
- - RNLiveMarkdown/common (0.1.38):
+ - RNLiveMarkdown/common (0.1.47):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1945,7 +1945,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - RNScreens (3.29.0):
+ - RNScreens (3.30.1):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1963,9 +1963,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNScreens/common (= 3.29.0)
+ - RNScreens/common (= 3.30.1)
- Yoga
- - RNScreens/common (3.29.0):
+ - RNScreens/common (3.30.1):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2565,13 +2565,13 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: 9d974f060d0bd857f7d96fac0e9a1539363baa5e
+ RNLiveMarkdown: f172c7199283dc9d21bccf7e21ea10741fd19e1d
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d
RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729
RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37
RNReanimated: 605409e0d0ced6f2e194ae585fedc2f8a1935bf2
- RNScreens: f7b8bb892b4957f6f91e5dfd9a191e7f13ce8baa
+ RNScreens: 65a936f4e227b91e4a8e2a7d4c4607355bfefda0
RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58
RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852
RNSVG: db32cfcad0a221fd175e0882eff7bcba7690380a
@@ -2582,7 +2582,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf
- Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312
+ Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70
PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d
diff --git a/package-lock.json b/package-lock.json
index 5315c22d9161..439b14837b1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "new.expensify",
- "version": "1.4.62-0",
+ "version": "1.4.62-4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.62-0",
+ "version": "1.4.62-4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "github:Expensify/react-native-live-markdown#f762be6fa832419dbbecb8a0cf64bf7dce18545b",
+ "@expensify/react-native-live-markdown": "0.1.47",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -114,7 +114,7 @@
"react-native-release-profiler": "^0.1.6",
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.8.2",
- "react-native-screens": "3.29.0",
+ "react-native-screens": "3.30.1",
"react-native-share": "^10.0.2",
"react-native-sound": "^0.11.2",
"react-native-svg": "14.1.0",
@@ -3570,13 +3570,9 @@
}
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.38",
- "resolved": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#f762be6fa832419dbbecb8a0cf64bf7dce18545b",
- "integrity": "sha512-m8+t3y1AtpvFAt3GAwRCiGwcOhUagOTCvwJ87kMGO5q/SKB2GCBHYMQ0QZaHw2QvAzRE6v2kCdqItX5DY+4MPQ==",
- "license": "MIT",
- "workspaces": [
- "example"
- ],
+ "version": "0.1.47",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.47.tgz",
+ "integrity": "sha512-zUfwgg6qq47MnGuynamDpdHSlBYwVKFV4Zc/2wlVzFcBndQOjOyFu04Ns8YDB4Gl80LyGvfAuBT/sU+kvmMU6g==",
"engines": {
"node": ">= 18.0.0"
},
@@ -31562,8 +31558,9 @@
}
},
"node_modules/react-native-screens": {
- "version": "3.29.0",
- "license": "MIT",
+ "version": "3.30.1",
+ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.30.1.tgz",
+ "integrity": "sha512-/muEvjocCtFb+j5J3YmLvB25+f4rIU8hnnxgGTkXcAf2omPBY8uhPjJaaFUlvj64VEoEzJcRpugbXWsjfPPIFg==",
"dependencies": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"
diff --git a/package.json b/package.json
index cca384eeb386..34a15e9e540a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.62-0",
+ "version": "1.4.62-4",
"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.",
@@ -64,7 +64,7 @@
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "github:Expensify/react-native-live-markdown#f762be6fa832419dbbecb8a0cf64bf7dce18545b",
+ "@expensify/react-native-live-markdown": "0.1.47",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -165,7 +165,7 @@
"react-native-release-profiler": "^0.1.6",
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.8.2",
- "react-native-screens": "3.29.0",
+ "react-native-screens": "3.30.1",
"react-native-share": "^10.0.2",
"react-native-sound": "^0.11.2",
"react-native-svg": "14.1.0",
diff --git a/patches/react-native-quick-sqlite+8.0.0-beta.2.patch b/patches/react-native-quick-sqlite+8.0.0-beta.2.patch
new file mode 100644
index 000000000000..b5810c903873
--- /dev/null
+++ b/patches/react-native-quick-sqlite+8.0.0-beta.2.patch
@@ -0,0 +1,12 @@
+diff --git a/node_modules/react-native-quick-sqlite/android/build.gradle b/node_modules/react-native-quick-sqlite/android/build.gradle
+index 323d34e..c2d0c44 100644
+--- a/node_modules/react-native-quick-sqlite/android/build.gradle
++++ b/node_modules/react-native-quick-sqlite/android/build.gradle
+@@ -90,7 +90,6 @@ android {
+ externalNativeBuild {
+ cmake {
+ cppFlags "-O2", "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID"
+- abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
+ arguments '-DANDROID_STL=c++_shared',
+ "-DREACT_NATIVE_DIR=${REACT_NATIVE_DIR}",
+ "-DSQLITE_FLAGS='${SQLITE_FLAGS ? SQLITE_FLAGS : ''}'"
diff --git a/patches/react-native-screens+3.29.0+001+initial.patch b/patches/react-native-screens+3.29.0+001+initial.patch
deleted file mode 100644
index dbe65b2abf3f..000000000000
--- a/patches/react-native-screens+3.29.0+001+initial.patch
+++ /dev/null
@@ -1,49 +0,0 @@
-diff --git a/node_modules/react-native-screens/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt b/node_modules/react-native-screens/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt
-index d9e0e58..2d946c4 100644
---- a/node_modules/react-native-screens/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt
-+++ b/node_modules/react-native-screens/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt
-@@ -3,7 +3,6 @@ package com.swmansion.rnscreens
- import android.view.ViewGroup
- import androidx.annotation.UiThread
- import com.facebook.react.bridge.ReactContext
--import com.facebook.react.bridge.ReadableMap
- import com.facebook.react.bridge.WritableMap
- import com.facebook.react.bridge.WritableNativeMap
- import com.facebook.react.uimanager.FabricViewStateManager
-@@ -13,6 +12,9 @@ import kotlin.math.abs
- abstract class FabricEnabledViewGroup constructor(context: ReactContext?) : ViewGroup(context), FabricViewStateManager.HasFabricViewStateManager {
- private val mFabricViewStateManager: FabricViewStateManager = FabricViewStateManager()
-
-+ private var lastSetWidth = 0f
-+ private var lastSetHeight = 0f
-+
- override fun getFabricViewStateManager(): FabricViewStateManager {
- return mFabricViewStateManager
- }
-@@ -28,17 +30,16 @@ abstract class FabricEnabledViewGroup constructor(context: ReactContext?) : View
-
- // Check incoming state values. If they're already the correct value, return early to prevent
- // infinite UpdateState/SetState loop.
-- val currentState: ReadableMap? = mFabricViewStateManager.getStateData()
-- if (currentState != null) {
-- val delta = 0.9f
-- val stateFrameHeight: Float = if (currentState.hasKey("frameHeight")) currentState.getDouble("frameHeight").toFloat() else 0f
-- val stateFrameWidth: Float = if (currentState.hasKey("frameWidth")) currentState.getDouble("frameWidth").toFloat() else 0f
-- if (abs(stateFrameWidth - realWidth) < delta &&
-- abs(stateFrameHeight - realHeight) < delta
-- ) {
-- return
-- }
-+ val delta = 0.9f
-+ if (abs(lastSetWidth - realWidth) < delta &&
-+ abs(lastSetHeight - realHeight) < delta
-+ ) {
-+ return
- }
-+
-+ lastSetWidth = realWidth
-+ lastSetHeight = realHeight
-+
- mFabricViewStateManager.setState {
- val map: WritableMap = WritableNativeMap()
- map.putDouble("frameWidth", realWidth.toDouble())
diff --git a/patches/react-native-screens+3.29.0+002+fixLayoutIssues.patch b/patches/react-native-screens+3.29.0+002+fixLayoutIssues.patch
deleted file mode 100644
index 9654c9cfcb42..000000000000
--- a/patches/react-native-screens+3.29.0+002+fixLayoutIssues.patch
+++ /dev/null
@@ -1,214 +0,0 @@
-diff --git a/node_modules/react-native-screens/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt b/node_modules/react-native-screens/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt
-index 2d946c4..ccda8f3 100644
---- a/node_modules/react-native-screens/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt
-+++ b/node_modules/react-native-screens/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt
-@@ -12,38 +12,36 @@ import kotlin.math.abs
- abstract class FabricEnabledViewGroup constructor(context: ReactContext?) : ViewGroup(context), FabricViewStateManager.HasFabricViewStateManager {
- private val mFabricViewStateManager: FabricViewStateManager = FabricViewStateManager()
-
-- private var lastSetWidth = 0f
-- private var lastSetHeight = 0f
-+ private var lastHeaderHeight: Double = 0.0
-
- override fun getFabricViewStateManager(): FabricViewStateManager {
- return mFabricViewStateManager
- }
-
-- protected fun updateScreenSizeFabric(width: Int, height: Int) {
-- updateState(width, height)
-+ protected fun updateScreenSizeFabric(width: Int, height: Int, headerHeight: Double) {
-+ updateState(width, height, headerHeight)
- }
-
- @UiThread
-- fun updateState(width: Int, height: Int) {
-+ fun updateState(width: Int, height: Int, headerHeight: Double) {
- val realWidth: Float = PixelUtil.toDIPFromPixel(width.toFloat())
- val realHeight: Float = PixelUtil.toDIPFromPixel(height.toFloat())
-
- // Check incoming state values. If they're already the correct value, return early to prevent
- // infinite UpdateState/SetState loop.
-- val delta = 0.9f
-- if (abs(lastSetWidth - realWidth) < delta &&
-- abs(lastSetHeight - realHeight) < delta
-- ) {
-+ val delta = 0.9
-+ if (abs(lastHeaderHeight - headerHeight) < delta) {
- return
- }
-
-- lastSetWidth = realWidth
-- lastSetHeight = realHeight
-+ lastHeaderHeight = headerHeight
-
- mFabricViewStateManager.setState {
- val map: WritableMap = WritableNativeMap()
- map.putDouble("frameWidth", realWidth.toDouble())
- map.putDouble("frameHeight", realHeight.toDouble())
-+ map.putDouble("contentOffsetX", 0.0)
-+ map.putDouble("contentOffsetY", headerHeight)
- map
- }
- }
-diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt
-index a7d28f9..e0b0d8e 100644
---- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt
-+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt
-@@ -72,9 +72,9 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
- val width = r - l
- val height = b - t
-
-- calculateHeaderHeight()
-+ val headerHeight = if (container is ScreenStack) calculateHeaderHeight().first else 0.0
- if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
-- updateScreenSizeFabric(width, height)
-+ updateScreenSizeFabric(width, height, headerHeight)
- } else {
- updateScreenSizePaper(width, height)
- }
-@@ -246,7 +246,7 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
- mNativeBackButtonDismissalEnabled = enableNativeBackButtonDismissal
- }
-
-- private fun calculateHeaderHeight() {
-+ private fun calculateHeaderHeight(): Pair {
- val actionBarTv = TypedValue()
- val resolvedActionBarSize = context.theme.resolveAttribute(android.R.attr.actionBarSize, actionBarTv, true)
-
-@@ -265,6 +265,8 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
- val totalHeight = actionBarHeight + statusBarHeight
- UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id)
- ?.dispatchEvent(HeaderHeightChangeEvent(id, totalHeight))
-+
-+ return actionBarHeight to statusBarHeight
- }
-
- enum class StackPresentation {
-diff --git a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h
-index 67194d3..c1a1b40 100644
---- a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h
-+++ b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h
-@@ -13,7 +13,7 @@ class RNSScreenComponentDescriptor final
- using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
-
- void adopt(ShadowNode& shadowNode) const override {
-- react_native_assert(
-+ react_native_assert(
- dynamic_cast(&shadowNode));
- auto& screenShadowNode =
- static_cast(shadowNode);
-@@ -28,10 +28,7 @@ class RNSScreenComponentDescriptor final
- shadowNode.getState());
- auto stateData = state->getData();
-
-- if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
-- layoutableShadowNode.setSize(
-- Size{stateData.frameSize.width, stateData.frameSize.height});
-- }
-+ layoutableShadowNode.setPadding({.bottom = stateData.contentOffset.y});
-
- ConcreteComponentDescriptor::adopt(shadowNode);
- }
-diff --git a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp
-index ba61ed8..0e3746e 100644
---- a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp
-+++ b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp
-@@ -5,5 +5,11 @@ namespace react {
-
- extern const char RNSScreenComponentName[] = "RNSScreen";
-
-+Point RNSScreenShadowNode::getContentOriginOffset() const {
-+ auto stateData = getStateData();
-+ auto contentOffset = stateData.contentOffset;
-+ return {contentOffset.x, contentOffset.y};
-+}
-+
- } // namespace react
- } // namespace facebook
-diff --git a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h
-index ef25dd1..bbd7599 100644
---- a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h
-+++ b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h
-@@ -19,9 +19,11 @@ class JSI_EXPORT RNSScreenShadowNode final : public ConcreteViewShadowNode<
- public:
- using ConcreteViewShadowNode::ConcreteViewShadowNode;
-
-+ Point getContentOriginOffset() const override;
-+
- static ShadowNodeTraits BaseTraits() {
- auto traits = ConcreteViewShadowNode::BaseTraits();
-- traits.set(ShadowNodeTraits::Trait::RootNodeKind);
-+ // traits.set(ShadowNodeTraits::Trait::RootNodeKind);
- return traits;
- }
- };
-diff --git a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenState.cpp b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenState.cpp
-index 69c77a6..6c3b8ca 100644
---- a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenState.cpp
-+++ b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenState.cpp
-@@ -6,8 +6,8 @@ namespace react {
- #ifdef ANDROID
- folly::dynamic RNSScreenState::getDynamic() const {
- return folly::dynamic::object("frameWidth", frameSize.width)(
-- "frameHeight", frameSize.height);
--}
-+ "frameHeight", frameSize.height)("contentOffsetX", contentOffset.x)("contentOffsetY", contentOffset.y);
-+ }
- #endif
-
- } // namespace react
-diff --git a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenState.h b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenState.h
-index ce09807..e26d411 100644
---- a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenState.h
-+++ b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenState.h
-@@ -17,7 +17,7 @@ class JSI_EXPORT RNSScreenState final {
- using Shared = std::shared_ptr;
-
- RNSScreenState(){};
-- RNSScreenState(Size frameSize_) : frameSize(frameSize_){};
-+ RNSScreenState(Size frameSize_, Point contentOffset_) : frameSize(frameSize_), contentOffset(contentOffset_){};
-
- #ifdef ANDROID
- RNSScreenState(
-@@ -25,10 +25,14 @@ class JSI_EXPORT RNSScreenState final {
- folly::dynamic data)
- : frameSize(Size{
- (Float)data["frameWidth"].getDouble(),
-- (Float)data["frameHeight"].getDouble()}){};
-+ (Float)data["frameHeight"].getDouble()}),
-+ contentOffset(Point{
-+ (Float)data["contentOffsetX"].getDouble(),
-+ (Float)data["contentOffsetY"].getDouble()}){};
- #endif
-
- const Size frameSize{};
-+ Point contentOffset;
-
- #ifdef ANDROID
- folly::dynamic getDynamic() const;
-diff --git a/node_modules/react-native-screens/ios/RNSScreen.h b/node_modules/react-native-screens/ios/RNSScreen.h
-index f1bd9d8..797fc12 100644
---- a/node_modules/react-native-screens/ios/RNSScreen.h
-+++ b/node_modules/react-native-screens/ios/RNSScreen.h
-@@ -42,6 +42,7 @@ namespace react = facebook::react;
- #ifdef RCT_NEW_ARCH_ENABLED
- - (void)setViewToSnapshot:(UIView *)snapshot;
- - (void)resetViewToScreen;
-+- (CGFloat)calculateHeaderHeightIsModal:(BOOL)isModal;
- #endif
-
- @end
-diff --git a/node_modules/react-native-screens/ios/RNSScreen.mm b/node_modules/react-native-screens/ios/RNSScreen.mm
-index 4b24cff..8f480ca 100644
---- a/node_modules/react-native-screens/ios/RNSScreen.mm
-+++ b/node_modules/react-native-screens/ios/RNSScreen.mm
-@@ -107,7 +107,8 @@ - (void)updateBounds
- {
- #ifdef RCT_NEW_ARCH_ENABLED
- if (_state != nullptr) {
-- auto newState = react::RNSScreenState{RCTSizeFromCGSize(self.bounds.size)};
-+ CGFloat headerHeight = [_controller calculateHeaderHeightIsModal:self.isPresentedAsNativeModal];
-+ auto newState = react::RNSScreenState{RCTSizeFromCGSize(self.bounds.size), RCTPointFromCGPoint(CGPointMake(0, headerHeight))};
- _state->updateState(std::move(newState));
- UINavigationController *navctr = _controller.navigationController;
- [navctr.view setNeedsLayout];
diff --git a/patches/react-native-screens+3.29.0+003+fixIOSHeaderHeight.patch b/patches/react-native-screens+3.29.0+003+fixIOSHeaderHeight.patch
deleted file mode 100644
index ae162204a692..000000000000
--- a/patches/react-native-screens+3.29.0+003+fixIOSHeaderHeight.patch
+++ /dev/null
@@ -1,35 +0,0 @@
-diff --git a/node_modules/react-native-screens/ios/RNSScreen.mm b/node_modules/react-native-screens/ios/RNSScreen.mm
-index 8f480ca..4cc5e7b 100644
---- a/node_modules/react-native-screens/ios/RNSScreen.mm
-+++ b/node_modules/react-native-screens/ios/RNSScreen.mm
-@@ -108,7 +108,7 @@ - (void)updateBounds
- #ifdef RCT_NEW_ARCH_ENABLED
- if (_state != nullptr) {
- CGFloat headerHeight = [_controller calculateHeaderHeightIsModal:self.isPresentedAsNativeModal];
-- auto newState = react::RNSScreenState{RCTSizeFromCGSize(self.bounds.size), RCTPointFromCGPoint(CGPointMake(0, headerHeight))};
-+ auto newState = react::RNSScreenState{RCTSizeFromCGSize(self.bounds.size), RCTPointFromCGPoint(CGPointMake(0, 0))};
- _state->updateState(std::move(newState));
- UINavigationController *navctr = _controller.navigationController;
- [navctr.view setNeedsLayout];
-@@ -1106,17 +1106,11 @@ - (CGFloat)calculateHeaderHeightIsModal:(BOOL)isModal
- {
- UINavigationController *navctr = [self getVisibleNavigationControllerIsModal:isModal];
-
-- // If navigation controller doesn't exists (or it is hidden) we want to handle two possible cases.
-- // If there's no navigation controller for the modal, we simply don't want to return header height, as modal possibly
-- // does not have header and we don't want to count status bar. If there's no navigation controller for the view we
-- // just want to return status bar height (if it's hidden, it will simply return 0).
-+ // If there's no navigation controller for the modal (or the navigation bar is hidden), we simply don't want to
-+ // return header height, as modal possibly does not have header when navigation controller is nil,
-+ // and we don't want to count status bar if navigation bar is hidden (inset could be negative).
- if (navctr == nil || navctr.isNavigationBarHidden) {
-- if (isModal) {
-- return 0;
-- } else {
-- CGSize statusBarSize = [self getStatusBarHeightIsModal:isModal];
-- return MIN(statusBarSize.width, statusBarSize.height);
-- }
-+ return 0;
- }
-
- CGFloat navbarHeight = navctr.navigationBar.frame.size.height;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 42ca7ee7a907..1eafd9d898ec 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -336,9 +336,10 @@ const ONYXKEYS = {
SECURITY_GROUP: 'securityGroup_',
TRANSACTION: 'transactions_',
TRANSACTION_VIOLATIONS: 'transactionViolations_',
+ TRANSACTION_DRAFT: 'transactionsDraft_',
// Holds temporary transactions used during the creation and edit flow
- TRANSACTION_DRAFT: 'transactionsDraft_',
+ TRANSACTION_BACKUP: 'transactionsBackup_',
SPLIT_TRANSACTION_DRAFT: 'splitTransactionDraft_',
PRIVATE_NOTES_DRAFT: 'privateNotesDraft_',
NEXT_STEP: 'reportNextStep_',
@@ -436,8 +437,8 @@ const ONYXKEYS = {
REPORT_FIELD_EDIT_FORM_DRAFT: 'reportFieldEditFormDraft',
REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount',
REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft',
- PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccountForm',
- PERSONAL_BANK_ACCOUNT_FORM_DRAFT: 'personalBankAccountFormDraft',
+ PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccount',
+ PERSONAL_BANK_ACCOUNT_FORM_DRAFT: 'personalBankAccountDraft',
EXIT_SURVEY_REASON_FORM: 'exitSurveyReasonForm',
EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft',
EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm',
@@ -538,6 +539,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup;
[ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction;
+ [ONYXKEYS.COLLECTION.TRANSACTION_BACKUP]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations;
[ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 31c2af8f4e58..7fee9b5497ce 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1,6 +1,7 @@
-import type {IsEqual, ValueOf} from 'type-fest';
+import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
import type {IOURequestType} from './libs/actions/IOU';
+import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
// This is a file containing constants for all the routes we want to be able to go to
@@ -368,16 +369,16 @@ const ROUTES = {
getUrlWithBackToParam(`${action}/${iouType}/scan/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_TAG: {
- route: ':action/:iouType/tag/:tagIndex/:transactionID/:reportID/:reportActionID?',
+ route: ':action/:iouType/tag/:orderWeight/:transactionID/:reportID/:reportActionID?',
getRoute: (
action: ValueOf,
iouType: ValueOf,
- tagIndex: number,
+ orderWeight: number,
transactionID: string,
reportID: string,
backTo = '',
reportActionID?: string,
- ) => getUrlWithBackToParam(`${action}/${iouType}/tag/${tagIndex}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
+ ) => getUrlWithBackToParam(`${action}/${iouType}/tag/${orderWeight}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
MONEY_REQUEST_STEP_WAYPOINT: {
route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
@@ -730,20 +731,18 @@ export default ROUTES;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ExtractRouteName = TRoute extends {getRoute: (...args: any[]) => infer TRouteName} ? TRouteName : TRoute;
-type AllRoutes = {
+/**
+ * Represents all routes in the app as a union of literal strings.
+ */
+type Route = {
[K in keyof typeof ROUTES]: ExtractRouteName<(typeof ROUTES)[K]>;
}[keyof typeof ROUTES];
-type RouteIsPlainString = IsEqual;
+type RoutesValidationError = 'Error: One or more routes defined within `ROUTES` have not correctly used `as const` in their `getRoute` function return value.';
-/**
- * Represents all routes in the app as a union of literal strings.
- *
- * If this type resolves to `never`, it implies that one or more routes defined within `ROUTES` have not correctly used
- * `as const` in their `getRoute` function return value.
- */
-type Route = RouteIsPlainString extends true ? never : AllRoutes;
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+type RouteIsPlainString = AssertTypesNotEqual;
type HybridAppRoute = (typeof HYBRID_APP_ROUTES)[keyof typeof HYBRID_APP_ROUTES];
-export type {Route, HybridAppRoute, AllRoutes};
+export type {Route, HybridAppRoute};
diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx
index 6691c068eb3a..6cea253d5957 100644
--- a/src/components/Composer/index.native.tsx
+++ b/src/components/Composer/index.native.tsx
@@ -27,6 +27,7 @@ function Composer(
// user can read new chats without the keyboard in the way of the view.
// On Android the selection prop is required on the TextInput but this prop has issues on IOS
selection,
+ value,
...props
}: ComposerProps,
ref: ForwardedRef,
@@ -34,7 +35,7 @@ function Composer(
const textInput = useRef(null);
const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput);
const theme = useTheme();
- const markdownStyle = useMarkdownStyle();
+ const markdownStyle = useMarkdownStyle(value);
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -73,6 +74,7 @@ function Composer(
autoComplete="off"
placeholderTextColor={theme.placeholderText}
ref={setTextInputRef}
+ value={value}
onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles)}
rejectResponderTermination={false}
smartInsertDelete={false}
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index 69cc6b208652..23d24a5ae5dd 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -81,7 +81,7 @@ function Composer(
) {
const theme = useTheme();
const styles = useThemeStyles();
- const markdownStyle = useMarkdownStyle();
+ const markdownStyle = useMarkdownStyle(value);
const StyleUtils = useStyleUtils();
const {windowWidth} = useWindowDimensions();
const textRef = useRef(null);
diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx
index 35fa4d02f5e0..2f09b27f3067 100644
--- a/src/components/FixedFooter.tsx
+++ b/src/components/FixedFooter.tsx
@@ -2,6 +2,8 @@ import type {ReactNode} from 'react';
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
+import useKeyboardState from '@hooks/useKeyboardState';
+import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
type FixedFooterProps = {
@@ -13,8 +15,17 @@ type FixedFooterProps = {
};
function FixedFooter({style, children}: FixedFooterProps) {
+ const {isKeyboardShown} = useKeyboardState();
+ const insets = useSafeAreaInsets();
const styles = useThemeStyles();
- return {children};
+
+ if (!children) {
+ return null;
+ }
+
+ const shouldAddBottomPadding = isKeyboardShown || !insets.bottom;
+
+ return {children};
}
FixedFooter.displayName = 'FixedFooter';
diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx
index 27db5687a925..8182ee487a80 100644
--- a/src/components/FormAlertWithSubmitButton.tsx
+++ b/src/components/FormAlertWithSubmitButton.tsx
@@ -79,7 +79,7 @@ function FormAlertWithSubmitButton({
return (
;
+
+ /** Collection of tags attached to a policy */
+ policyTags: OnyxEntry;
+
+ /** The policy of the report */
+ policy: OnyxEntry;
+
+ /** The session of the logged in user */
+ session: OnyxEntry;
+
+ /** Unit and rate used for if the money request is a distance request */
+ mileageRate: OnyxEntry;
+};
+
+type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & {
/** Callback to inform parent modal of success */
- onConfirm: PropTypes.func,
+ onConfirm?: (selectedParticipants: Participant[]) => void;
/** Callback to parent modal to send money */
- onSendMoney: PropTypes.func,
+ onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void;
/** Callback to inform a participant is selected */
- onSelectParticipant: PropTypes.func,
+ onSelectParticipant?: (option: Participant) => void;
/** Should we request a single or multiple participant selection from user */
- hasMultipleParticipants: PropTypes.bool.isRequired,
+ hasMultipleParticipants: boolean;
/** IOU amount */
- iouAmount: PropTypes.number.isRequired,
+ iouAmount: number;
/** IOU comment */
- iouComment: PropTypes.string,
+ iouComment?: string;
/** IOU currency */
- iouCurrencyCode: PropTypes.string,
+ iouCurrencyCode?: string;
/** IOU type */
- iouType: PropTypes.string,
+ iouType?: ValueOf;
/** IOU date */
- iouCreated: PropTypes.string,
+ iouCreated?: string;
/** IOU merchant */
- iouMerchant: PropTypes.string,
+ iouMerchant?: string;
- /** IOU category */
- iouCategory: PropTypes.string,
+ /** IOU Category */
+ iouCategory?: string;
/** IOU isBillable */
- iouIsBillable: PropTypes.bool,
+ iouIsBillable?: boolean;
/** Callback to toggle the billable state */
- onToggleBillable: PropTypes.func,
+ onToggleBillable?: (isOn: boolean) => void;
/** Selected participants from MoneyRequestModal with login / accountID */
- selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired,
+ selectedParticipants: Participant[];
/** Payee of the money request with login */
- payeePersonalDetails: optionPropTypes,
+ payeePersonalDetails?: OnyxTypes.PersonalDetails;
/** Can the participants be modified or not */
- canModifyParticipants: PropTypes.bool,
+ canModifyParticipants?: boolean;
/** Should the list be read only, and not editable? */
- isReadOnly: PropTypes.bool,
-
- /** Whether the money request is a scan request */
- isScanRequest: PropTypes.bool,
+ isReadOnly?: boolean;
/** Depending on expense report or personal IOU report, respective bank account route */
- bankAccountRoute: PropTypes.string,
-
- ...withCurrentUserPersonalDetailsPropTypes,
-
- /** Current user session */
- session: PropTypes.shape({
- email: PropTypes.string.isRequired,
- }),
+ bankAccountRoute?: Route;
/** The policyID of the request */
- policyID: PropTypes.string,
+ policyID?: string;
/** The reportID of the request */
- reportID: PropTypes.string,
+ reportID?: string;
/** File path of the receipt */
- receiptPath: PropTypes.string,
+ receiptPath?: string;
/** File name of the receipt */
- receiptFilename: PropTypes.string,
+ receiptFilename?: string;
/** List styles for OptionsSelector */
- listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
- /** ID of the transaction that represents the money request */
- transactionID: PropTypes.string,
+ listStyles?: StyleProp;
- /** Unit and rate used for if the money request is a distance request */
- mileageRate: PropTypes.shape({
- /** Unit used to represent distance */
- unit: PropTypes.oneOf([CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]),
-
- /** Rate used to calculate the distance request amount */
- rate: PropTypes.number,
-
- /** The currency of the rate */
- currency: PropTypes.string,
- }),
+ /** Transaction that represents the money request */
+ transaction?: OnyxEntry;
/** Whether the money request is a distance request */
- isDistanceRequest: PropTypes.bool,
+ isDistanceRequest?: boolean;
+
+ /** Whether the money request is a scan request */
+ isScanRequest?: boolean;
/** Whether we're editing a split bill */
- isEditingSplitBill: PropTypes.bool,
+ isEditingSplitBill?: boolean;
/** Whether we should show the amount, date, and merchant fields. */
- shouldShowSmartScanFields: PropTypes.bool,
+ shouldShowSmartScanFields?: boolean;
/** A flag for verifying that the current report is a sub-report of a workspace chat */
- isPolicyExpenseChat: PropTypes.bool,
-
- /* Onyx Props */
- /** Collection of categories attached to a policy */
- policyCategories: PropTypes.objectOf(categoryPropTypes),
-
- /** Collection of tags attached to a policy */
- policyTags: tagPropTypes,
+ isPolicyExpenseChat?: boolean;
- /* Onyx Props */
- /** The policy of the report */
- policy: policyPropTypes.policy,
+ /** Whether smart scan failed */
+ hasSmartScanFailed?: boolean;
- /** Transaction that represents the money request */
- transaction: transactionPropTypes,
-};
-
-const defaultProps = {
- onConfirm: () => {},
- onSendMoney: () => {},
- onSelectParticipant: () => {},
- iouType: CONST.IOU.TYPE.REQUEST,
- iouCategory: '',
- iouIsBillable: false,
- onToggleBillable: () => {},
- payeePersonalDetails: null,
- canModifyParticipants: false,
- isReadOnly: false,
- bankAccountRoute: '',
- session: {
- email: null,
- },
- policyID: '',
- reportID: '',
- ...withCurrentUserPersonalDetailsDefaultProps,
- receiptPath: '',
- receiptFilename: '',
- listStyles: [],
- policy: {},
- policyCategories: {},
- policyTags: {},
- transactionID: '',
- transaction: {},
- mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'},
- isDistanceRequest: false,
- shouldShowSmartScanFields: true,
- isPolicyExpenseChat: false,
+ reportActionID?: string;
};
-const getTaxAmount = (transaction, defaultTaxValue) => {
- const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || '';
- return TransactionUtils.calculateTaxAmount(percentage, transaction.amount);
+const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) || '';
+ return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0);
};
function MoneyTemporaryForRefactorRequestConfirmationList({
- bankAccountRoute,
- canModifyParticipants,
- currentUserPersonalDetails,
- hasMultipleParticipants,
- hasSmartScanFailed,
+ transaction = null,
+ onSendMoney,
+ onConfirm,
+ onSelectParticipant,
+ iouType = CONST.IOU.TYPE.REQUEST,
+ isScanRequest = false,
iouAmount,
- iouCategory,
- iouComment,
- iouCreated,
+ policyCategories,
+ mileageRate,
+ isDistanceRequest = false,
+ policy,
+ isPolicyExpenseChat = false,
+ iouCategory = '',
+ shouldShowSmartScanFields = true,
+ isEditingSplitBill,
+ policyTags,
iouCurrencyCode,
- iouIsBillable,
iouMerchant,
- iouType,
- isDistanceRequest,
- isEditingSplitBill,
- isPolicyExpenseChat,
- isReadOnly,
- isScanRequest,
+ hasMultipleParticipants,
+ selectedParticipants: pickedParticipants,
+ payeePersonalDetails,
+ canModifyParticipants = false,
+ session,
+ isReadOnly = false,
+ bankAccountRoute = '',
+ policyID = '',
+ reportID = '',
+ receiptPath = '',
+ iouComment,
+ receiptFilename = '',
listStyles,
- mileageRate,
- onConfirm,
- onSelectParticipant,
- onSendMoney,
+ iouCreated,
+ iouIsBillable = false,
onToggleBillable,
- payeePersonalDetails,
- policy,
- policyCategories,
- policyID,
- policyTags,
- receiptFilename,
- receiptPath,
+ hasSmartScanFailed,
reportActionID,
- reportID,
- selectedParticipants: pickedParticipants,
- session: {accountID},
- shouldShowSmartScanFields,
- transaction,
-}) {
+}: MoneyRequestConfirmationListProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const {canUseViolations} = usePermissions();
const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
@@ -260,13 +217,17 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE;
- const {unit, rate, currency} = mileageRate;
- const distance = lodashGet(transaction, 'routes.route0.distance', 0);
+ const {unit, rate, currency} = mileageRate ?? {
+ unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES,
+ rate: 0,
+ currency: 'USD',
+ };
+ const distance = transaction?.routes?.route0.distance ?? 0;
const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0;
- const taxRates = lodashGet(policy, 'taxRates', {});
+ const taxRates = policy?.taxRates;
// A flag for showing the categories field
- const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(_.values(policyCategories)));
+ const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {})));
// A flag and a toggler for showing the rest of the form fields
const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false);
@@ -286,21 +247,20 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy);
// A flag for showing the billable field
- const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true);
+ const shouldShowBillable = policy?.disabledFields?.defaultBillable === false;
const hasRoute = TransactionUtils.hasRoute(transaction);
const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate);
const formattedAmount = isDistanceRequestWithPendingRoute
? ''
: CurrencyUtils.convertToDisplayString(
- shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount,
+ shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount,
isDistanceRequest ? currency : iouCurrencyCode,
);
- const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode);
+ const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode);
+ const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : '';
- const taxRateTitle = TransactionUtils.getDefaultTaxName(taxRates, transaction);
-
- const previousTransactionAmount = usePrevious(transaction.amount);
+ const previousTransactionAmount = usePrevious(transaction?.amount);
const isFocused = useIsFocused();
const [formError, setFormError] = useState('');
@@ -313,21 +273,21 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
const navigateBack = () => {
- Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID));
};
- const shouldDisplayFieldError = useMemo(() => {
+ const shouldDisplayFieldError: boolean = useMemo(() => {
if (!isEditingSplitBill) {
return false;
}
- return (hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction));
+ return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction));
}, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]);
const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant;
- const isCategoryRequired = canUseViolations && lodashGet(policy, 'requiresCategory', false);
+ const isCategoryRequired = canUseViolations && !!policy?.requiresCategory;
useEffect(() => {
if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) {
@@ -363,30 +323,28 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return;
}
- const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate);
- IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency);
+ const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0);
+ IOU.setMoneyRequestAmount_temporaryForRefactor(transaction?.transactionID ?? '', amount, currency ?? '');
}, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]);
// Calculate and set tax amount in transaction draft
useEffect(() => {
- const taxAmount = getTaxAmount(transaction, taxRates.defaultValue);
+ const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString();
const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));
- if (transaction.taxAmount && previousTransactionAmount === transaction.amount) {
- return IOU.setMoneyRequestTaxAmount(transaction.transactionID, transaction.taxAmount, true);
+ if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) {
+ return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true);
}
- IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits, true);
- }, [taxRates.defaultValue, transaction, previousTransactionAmount]);
+ IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', amountInSmallestCurrencyUnits, true);
+ }, [taxRates?.defaultValue, transaction, previousTransactionAmount]);
/**
* Returns the participants with amount
- * @param {Array} participants
- * @returns {Array}
*/
const getParticipantsWithAmount = useCallback(
- (participantsList) => {
- const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode);
+ (participantsList: Participant[]) => {
+ const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? '');
return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '');
},
[iouAmount, iouCurrencyCode],
@@ -397,7 +355,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
setDidConfirm(false);
}
- const splitOrRequestOptions = useMemo(() => {
+ const splitOrRequestOptions: Array> = useMemo(() => {
let text;
if (isTypeTrackExpense) {
text = translate('iou.trackExpense');
@@ -420,8 +378,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
];
}, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]);
- const selectedParticipants = useMemo(() => _.filter(pickedParticipants, (participant) => participant.selected), [pickedParticipants]);
- const personalDetailsOfPayee = useMemo(() => payeePersonalDetails || currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]);
+ const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]);
+ const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]);
const userCanModifyParticipants = useRef(!isReadOnly && canModifyParticipants && hasMultipleParticipants);
useEffect(() => {
userCanModifyParticipants.current = !isReadOnly && canModifyParticipants && hasMultipleParticipants;
@@ -430,19 +388,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const optionSelectorSections = useMemo(() => {
const sections = [];
- const unselectedParticipants = _.filter(pickedParticipants, (participant) => !participant.selected);
+ const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected);
if (hasMultipleParticipants) {
const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants);
- let formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants);
+ let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])];
- if (!userCanModifyParticipants.current) {
- formattedParticipantsList = _.map(formattedParticipantsList, (participant) => ({
+ if (!canModifyParticipants) {
+ formattedParticipantsList = formattedParticipantsList.map((participant) => ({
...participant,
- isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID),
+ isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
}));
}
- const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode, true);
+ const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true);
const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(
personalDetailsOfPayee,
iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '',
@@ -462,9 +420,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
},
);
} else {
- const formattedSelectedParticipants = _.map(selectedParticipants, (participant) => ({
+ const formattedSelectedParticipants = selectedParticipants.map((participant) => ({
...participant,
- isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID),
+ isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
}));
sections.push({
title: translate('common.to'),
@@ -483,7 +441,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
personalDetailsOfPayee,
translate,
shouldDisablePaidBySection,
- userCanModifyParticipants,
+ canModifyParticipants,
]);
const selectedOptions = useMemo(() => {
@@ -503,56 +461,54 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page.
In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending.
*/
- IOU.setMoneyRequestPendingFields(transaction.transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
+ IOU.setMoneyRequestPendingFields(transaction?.transactionID ?? '', {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
- const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit);
- IOU.setMoneyRequestMerchant(transaction.transactionID, distanceMerchant, true);
+ const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? 'USD', translate, toLocaleDigit);
+ IOU.setMoneyRequestMerchant(transaction?.transactionID ?? '', distanceMerchant, true);
}, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]);
// Auto select the category if there is only one enabled category and it is required
useEffect(() => {
- const enabledCategories = _.filter(policyCategories, (category) => category.enabled);
+ const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled);
if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) {
return;
}
- IOU.setMoneyRequestCategory(transaction.transactionID, enabledCategories[0].name);
+ IOU.setMoneyRequestCategory(transaction?.transactionID ?? '', enabledCategories[0].name);
}, [iouCategory, shouldShowCategories, policyCategories, transaction, isCategoryRequired]);
// Auto select the tag if there is only one enabled tag and it is required
useEffect(() => {
let updatedTagsString = TransactionUtils.getTag(transaction);
policyTagLists.forEach((tagList, index) => {
- const enabledTags = _.filter(tagList.tags, (tag) => tag.enabled);
- const isTagListRequired = isUndefined(tagList.required) ? false : tagList.required && canUseViolations;
+ const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled);
+ const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations;
if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) {
return;
}
updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index);
});
if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) {
- IOU.setMoneyRequestTag(transaction.transactionID, updatedTagsString);
+ IOU.setMoneyRequestTag(transaction?.transactionID ?? '', updatedTagsString);
}
}, [policyTagLists, transaction, policyTags, canUseViolations]);
/**
- * @param {Object} option
*/
const selectParticipant = useCallback(
- (option) => {
+ (option: Participant) => {
// Return early if selected option is currently logged in user.
- if (option.accountID === accountID) {
+ if (option.accountID === session?.accountID) {
return;
}
- onSelectParticipant(option);
+ onSelectParticipant?.(option);
},
- [accountID, onSelectParticipant],
+ [session?.accountID, onSelectParticipant],
);
/**
* Navigate to report details or profile of selected user
- * @param {Object} option
*/
- const navigateToReportOrUserDetail = (option) => {
+ const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => {
const activeRoute = Navigation.getActiveRouteWithoutParams();
if (option.isSelfDM) {
@@ -571,11 +527,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
* @param {String} paymentMethod
*/
const confirm = useCallback(
- (paymentMethod) => {
- if (_.isEmpty(selectedParticipants)) {
+ (paymentMethod: PaymentMethodType | undefined) => {
+ if (selectedParticipants.length === 0) {
return;
}
- if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction))) {
+ if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) {
setMerchantError(true);
return;
}
@@ -588,7 +544,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
setDidConfirm(true);
Log.info(`[IOU] Sending money via: ${paymentMethod}`);
- onSendMoney(paymentMethod);
+ onSendMoney?.(paymentMethod);
} else {
// validate the amount for distance requests
const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode);
@@ -597,7 +553,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return;
}
- if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) {
+ if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) {
setDidConfirmSplit(true);
setFormError('iou.error.genericSmartscanFailureMessage');
return;
@@ -605,7 +561,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
playSound(SOUNDS.DONE);
setDidConfirm(true);
- onConfirm(selectedParticipants);
+ onConfirm?.(selectedParticipants);
}
},
[
@@ -659,7 +615,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
success
pressOnEnter
isDisabled={shouldDisableButton}
- onPress={(_event, value) => confirm(value)}
+ onPress={(event, value) => confirm(value as PaymentMethodType)}
options={splitOrRequestOptions}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
enterKeyEventListenerPriority={1}
@@ -668,13 +624,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return (
<>
- {!_.isEmpty(formError) && (
+ {!!formError && (
)}
+
{button}
>
);
@@ -696,18 +653,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return;
}
if (isEditingSplitBill) {
- Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID, CONST.EDIT_REQUEST_FIELD.AMOUNT));
+ Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT));
return;
}
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
);
}}
style={[styles.moneyRequestMenuItem, styles.mt2]}
titleStyle={styles.moneyRequestConfirmationAmount}
disabled={didConfirm}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
- error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''}
+ brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''}
/>
),
shouldShow: shouldShowSmartScanFields,
@@ -723,7 +680,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
description={translate('common.description')}
onPress={() => {
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
);
}}
style={[styles.moneyRequestMenuItem]}
@@ -747,7 +710,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
titleStyle={styles.flex1}
onPress={() =>
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
)
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -769,12 +738,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
titleStyle={styles.flex1}
onPress={() => {
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
);
}}
disabled={didConfirm}
interactive={!isReadOnly}
- brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
error={merchantError ? translate('common.error.fieldRequired') : ''}
rightLabel={isMerchantRequired ? translate('common.required') : ''}
/>
@@ -787,18 +762,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
{
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
);
}}
disabled={didConfirm}
interactive={!isReadOnly}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''}
/>
),
@@ -815,7 +791,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
numberOfLinesTitle={2}
onPress={() =>
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
)
}
style={[styles.moneyRequestMenuItem]}
@@ -828,8 +810,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
shouldShow: shouldShowCategories,
isSupplementary: !isCategoryRequired,
},
- ..._.map(policyTagLists, ({name, required}, index) => {
- const isTagRequired = isUndefined(required) ? false : canUseViolations && required;
+ ...policyTagLists.map(({name, required}, index) => {
+ const isTagRequired = required === undefined ? false : canUseViolations && required;
return {
item: (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
)
}
disabled={didConfirm}
@@ -884,7 +872,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
{
item: (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
)
}
disabled={didConfirm}
@@ -909,7 +903,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
onToggleBillable?.(isOn)}
/>
),
@@ -918,15 +912,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
},
];
- const primaryFields = _.map(
- _.filter(classifiedFields, (classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary),
- (primaryField) => primaryField.item,
- );
+ const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item);
- const supplementaryFields = _.map(
- _.filter(classifiedFields, (classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary),
- (supplementaryField) => supplementaryField.item,
- );
+ const supplementaryFields = classifiedFields
+ .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary)
+ .map((supplementaryField) => supplementaryField.item);
const {
image: receiptImage,
@@ -934,13 +924,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
isThumbnail,
fileExtension,
isLocalFile,
- } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {};
+ } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI);
const receiptThumbnailContent = useMemo(
() =>
isLocalFile && Str.isPDF(receiptFilename) ? (
),
@@ -962,6 +954,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
);
return (
+ // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q)
{isDistanceRequest && (
-
+
)}
- {receiptImage || receiptThumbnail
- ? receiptThumbnailContent
- : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
- PolicyUtils.isPaidGroupPolicy(policy) &&
- !isDistanceRequest &&
- iouType === CONST.IOU.TYPE.REQUEST && (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
- )
- }
- />
- )}
+ {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ receiptImage || receiptThumbnail
+ ? receiptThumbnailContent
+ : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
+ PolicyUtils.isPaidGroupPolicy(policy) &&
+ !isDistanceRequest &&
+ iouType === CONST.IOU.TYPE.REQUEST && (
+
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
+ )
+ }
+ />
+ )
+ }
{primaryFields}
{!shouldShowAllFields && (
@@ -1029,28 +1031,23 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
);
}
-MoneyTemporaryForRefactorRequestConfirmationList.propTypes = propTypes;
-MoneyTemporaryForRefactorRequestConfirmationList.defaultProps = defaultProps;
MoneyTemporaryForRefactorRequestConfirmationList.displayName = 'MoneyTemporaryForRefactorRequestConfirmationList';
-export default compose(
- withCurrentUserPersonalDetails,
- withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
- policyCategories: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
- },
- policyTags: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
- },
- mileageRate: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- selector: DistanceRequestUtils.getDefaultMileageRate,
- },
- policy: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- },
- }),
-)(MoneyTemporaryForRefactorRequestConfirmationList);
+export default withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ policyCategories: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ },
+ policyTags: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ },
+ mileageRate: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ selector: DistanceRequestUtils.getDefaultMileageRate,
+ },
+ policy: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ },
+})(MoneyTemporaryForRefactorRequestConfirmationList);
diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx
index 0bc9130ea4a8..af16b7300e1a 100644
--- a/src/components/OnyxProvider.tsx
+++ b/src/components/OnyxProvider.tsx
@@ -16,6 +16,7 @@ const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = crea
const [withFrequentlyUsedEmojis, FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS);
const [withPreferredEmojiSkinTone, PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
const [, SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION);
+const [, AccountProvider, , useAccount] = createOnyxContext(ONYXKEYS.ACCOUNT);
type OnyxProviderProps = {
/** Rendered child component */
@@ -37,6 +38,7 @@ function OnyxProvider(props: OnyxProviderProps) {
FrequentlyUsedEmojisProvider,
PreferredEmojiSkinToneProvider,
SessionProvider,
+ AccountProvider,
]}
>
{props.children}
@@ -69,4 +71,5 @@ export {
useBlockedFromConcierge,
useReportActionsDrafts,
useSession,
+ useAccount,
};
diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx
index 08892f11b021..f4aa2de090f7 100644
--- a/src/components/ReceiptImage.tsx
+++ b/src/components/ReceiptImage.tsx
@@ -46,7 +46,7 @@ type ReceiptImageProps = (
isEReceipt?: boolean;
isThumbnail?: boolean;
source: string;
- isPDFThumbnail: string;
+ isPDFThumbnail?: string;
}
) & {
/** Whether we should display the receipt with ThumbnailImage component */
diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx
index c93b75bf11ad..0588f31a0a8c 100644
--- a/src/components/ReferralProgramCTA.tsx
+++ b/src/components/ReferralProgramCTA.tsx
@@ -1,43 +1,49 @@
-import React from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import React, {useEffect} from 'react';
+import type {ViewStyle} from 'react-native';
+import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as User from '@userActions/User';
import CONST from '@src/CONST';
import Navigation from '@src/libs/Navigation/Navigation';
-import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type * as OnyxTypes from '@src/types/onyx';
import Icon from './Icon';
import {Close} from './Icon/Expensicons';
import {PressableWithoutFeedback} from './Pressable';
import Text from './Text';
import Tooltip from './Tooltip';
-type ReferralProgramCTAOnyxProps = {
- dismissedReferralBanners: OnyxEntry;
-};
-
-type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & {
+type ReferralProgramCTAProps = {
referralContentType:
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND;
+ style?: ViewStyle;
+ onDismiss?: () => void;
};
-function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: ReferralProgramCTAProps) {
+function ReferralProgramCTA({referralContentType, style, onDismiss}: ReferralProgramCTAProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
+ const {isDismissed, setAsDismissed} = useDismissedReferralBanners({referralContentType});
const handleDismissCallToAction = () => {
- User.dismissReferralBanner(referralContentType);
+ setAsDismissed();
+ onDismiss?.();
};
- if (!referralContentType || dismissedReferralBanners?.[referralContentType]) {
+ const shouldShowBanner = referralContentType && !isDismissed;
+
+ useEffect(() => {
+ if (shouldShowBanner) {
+ return;
+ }
+ onDismiss?.();
+ }, [onDismiss, shouldShowBanner]);
+
+ if (!shouldShowBanner) {
return null;
}
@@ -46,7 +52,7 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref
onPress={() => {
Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType, Navigation.getActiveRouteWithoutParams()));
}}
- style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]}
+ style={[styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5, style]}
accessibilityLabel="referral"
role={CONST.ACCESSIBILITY_ROLE.BUTTON}
>
@@ -81,8 +87,4 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref
);
}
-export default withOnyx({
- dismissedReferralBanners: {
- key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
- },
-})(ReferralProgramCTA);
+export default ReferralProgramCTA;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index dd34d0ca2540..f6c937b72653 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -408,7 +408,7 @@ function MoneyRequestView({
)}
{shouldShowTag &&
- policyTagLists.map(({name}, index) => (
+ policyTagLists.map(({name, orderWeight}, index) => (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, index, transaction?.transactionID ?? '', report.reportID),
+ ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, orderWeight, transaction?.transactionID ?? '', report.reportID),
)
}
brickRoadIndicator={getErrorForField('tag', {tagListIndex: index, tagListName: name}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index eae8169de046..e53823860ce0 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -1,7 +1,7 @@
import {useNavigation} from '@react-navigation/native';
import type {StackNavigationProp} from '@react-navigation/stack';
import type {ForwardedRef, ReactNode} from 'react';
-import React, {forwardRef, useEffect, useRef, useState} from 'react';
+import React, {createContext, forwardRef, useEffect, useMemo, useRef, useState} from 'react';
import type {DimensionValue, StyleProp, ViewStyle} from 'react-native';
import {Keyboard, PanResponder, View} from 'react-native';
import {PickerAvoidingView} from 'react-native-picker-select';
@@ -99,6 +99,8 @@ type ScreenWrapperProps = {
shouldShowOfflineIndicatorInWideScreen?: boolean;
};
+const ScreenWrapperStatusContext = createContext({didScreenTransitionEnd: false});
+
function ScreenWrapper(
{
shouldEnableMaxHeight = false,
@@ -201,6 +203,7 @@ function ScreenWrapper(
}, []);
const isAvoidingViewportScroll = useTackInputFocus(shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileSafari());
+ const contextValue = useMemo(() => ({didScreenTransitionEnd}), [didScreenTransitionEnd]);
return (
@@ -251,16 +254,18 @@ function ScreenWrapper(
{isDevelopment && }
- {
- // If props.children is a function, call it to provide the insets to the children.
- typeof children === 'function'
- ? children({
- insets,
- safeAreaPaddingBottomStyle,
- didScreenTransitionEnd,
- })
- : children
- }
+
+ {
+ // If props.children is a function, call it to provide the insets to the children.
+ typeof children === 'function'
+ ? children({
+ insets,
+ safeAreaPaddingBottomStyle,
+ didScreenTransitionEnd,
+ })
+ : children
+ }
+
{isSmallScreenWidth && shouldShowOfflineIndicator && }
{!isSmallScreenWidth && shouldShowOfflineIndicatorInWideScreen && (
(
showConfirmButton = false,
shouldPreventDefaultFocusOnSelectRow = false,
containerStyle,
- isKeyboardShown = false,
disableKeyboardShortcuts = false,
children,
shouldStopPropagation = false,
@@ -88,6 +88,7 @@ function BaseSelectionList(
const isFocused = useIsFocused();
const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT);
const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true);
+ const {isKeyboardShown} = useKeyboardState();
const [itemsToHighlight, setItemsToHighlight] = useState | null>(null);
const itemFocusTimeoutRef = useRef(null);
const [currentPage, setCurrentPage] = useState(1);
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 38c5f03fcae6..af2ea3469408 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -284,8 +284,8 @@ type BaseSelectionListProps = Partial & {
/** Styles to apply to SelectionList container */
containerStyle?: StyleProp;
- /** Whether keyboard is visible on the screen */
- isKeyboardShown?: boolean;
+ /** Whether focus event should be delayed */
+ shouldDelayFocus?: boolean;
/** Component to display on the right side of each child */
rightHandSideComponent?: ((item: ListItem) => ReactElement | null) | ReactElement | null;
diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx
index 684d5e416471..1693bafe323d 100644
--- a/src/components/Switch.tsx
+++ b/src/components/Switch.tsx
@@ -1,8 +1,11 @@
import React, {useEffect, useRef} from 'react';
import {Animated} from 'react-native';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useNativeDriver from '@libs/useNativeDriver';
import CONST from '@src/CONST';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
type SwitchProps = {
@@ -27,6 +30,7 @@ const OFFSET_X = {
function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) {
const styles = useThemeStyles();
const offsetX = useRef(new Animated.Value(isOn ? OFFSET_X.ON : OFFSET_X.OFF));
+ const theme = useTheme();
useEffect(() => {
Animated.timing(offsetX.current, {
@@ -49,7 +53,16 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) {
hoverDimmingValue={1}
pressDimmingValue={0.8}
>
-
+
+ {disabled && (
+
+ )}
+
);
}
diff --git a/src/hooks/useDismissedReferralBanners.ts b/src/hooks/useDismissedReferralBanners.ts
new file mode 100644
index 000000000000..94ccd0a0b567
--- /dev/null
+++ b/src/hooks/useDismissedReferralBanners.ts
@@ -0,0 +1,29 @@
+import {useOnyx} from 'react-native-onyx';
+import * as User from '@userActions/User';
+import type CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+type UseDismissedReferralBannersProps = {
+ referralContentType:
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND;
+};
+
+function useDismissedReferralBanners({referralContentType}: UseDismissedReferralBannersProps): {isDismissed: boolean; setAsDismissed: () => void} {
+ const [dismissedReferralBanners] = useOnyx(ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS);
+ const isDismissed = dismissedReferralBanners?.[referralContentType] ?? false;
+
+ const setAsDismissed = () => {
+ if (!referralContentType) {
+ return;
+ }
+ // Set the banner as dismissed
+ User.dismissReferralBanner(referralContentType);
+ };
+
+ return {isDismissed, setAsDismissed};
+}
+
+export default useDismissedReferralBanners;
diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts
index 72e2734a4744..21c8d02e9194 100644
--- a/src/hooks/useMarkdownStyle.ts
+++ b/src/hooks/useMarkdownStyle.ts
@@ -1,11 +1,13 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import {useMemo} from 'react';
+import {containsOnlyEmojis} from '@libs/EmojiUtils';
import FontUtils from '@styles/utils/FontUtils';
import variables from '@styles/variables';
import useTheme from './useTheme';
-function useMarkdownStyle(): MarkdownStyle {
+function useMarkdownStyle(message: string | null = null): MarkdownStyle {
const theme = useTheme();
+ const emojiFontSize = containsOnlyEmojis(message ?? '') ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal;
const markdownStyle = useMemo(
() => ({
@@ -18,6 +20,9 @@ function useMarkdownStyle(): MarkdownStyle {
h1: {
fontSize: variables.fontSizeLarge,
},
+ emoji: {
+ fontSize: emojiFontSize,
+ },
blockquote: {
borderColor: theme.border,
borderWidth: 4,
@@ -45,7 +50,7 @@ function useMarkdownStyle(): MarkdownStyle {
backgroundColor: theme.mentionBG,
},
}),
- [theme],
+ [theme, emojiFontSize],
);
return markdownStyle;
diff --git a/src/hooks/useScreenWrapperTransitionStatus.ts b/src/hooks/useScreenWrapperTransitionStatus.ts
new file mode 100644
index 000000000000..b9e94abfc024
--- /dev/null
+++ b/src/hooks/useScreenWrapperTransitionStatus.ts
@@ -0,0 +1,17 @@
+import {useContext} from 'react';
+import {ScreenWrapperStatusContext} from '@components/ScreenWrapper';
+
+/**
+ * Hook to get the transition status of a screen inside a ScreenWrapper.
+ * Use this hook if you can't get the transition status from the ScreenWrapper itself. Usually when ScreenWrapper is used inside TopTabNavigator.
+ * @returns `didScreenTransitionEnd` flag to indicate if navigation transition ended.
+ */
+export default function useScreenWrapperTranstionStatus() {
+ const value = useContext(ScreenWrapperStatusContext);
+
+ if (value === undefined) {
+ throw new Error("Couldn't find values for screen ScreenWrapper transition status. Are you inside a screen in ScreenWrapper?");
+ }
+
+ return value;
+}
diff --git a/src/hooks/useSubStep/index.ts b/src/hooks/useSubStep/index.ts
index ad4cf032858d..2d9069abff88 100644
--- a/src/hooks/useSubStep/index.ts
+++ b/src/hooks/useSubStep/index.ts
@@ -6,8 +6,9 @@ import type {SubStepProps, UseSubStep} from './types';
* @param bodyContent - array of components to display in particular step
* @param onFinished - callback triggered after finish last step
* @param startFrom - initial index for bodyContent array
+ * @param onNextSubStep - callback triggered after finish each step
*/
-export default function useSubStep({bodyContent, onFinished, startFrom = 0}: UseSubStep) {
+export default function useSubStep({bodyContent, onFinished, startFrom = 0, onNextSubStep = () => {}}: UseSubStep) {
const [screenIndex, setScreenIndex] = useState(startFrom);
const isEditing = useRef(false);
@@ -35,9 +36,10 @@ export default function useSubStep({bodyContent, on
if (nextScreenIndex === bodyContent.length) {
onFinished();
} else {
+ onNextSubStep();
setScreenIndex(nextScreenIndex);
}
- }, [screenIndex, bodyContent.length, onFinished]);
+ }, [screenIndex, bodyContent.length, onFinished, onNextSubStep]);
const moveTo = useCallback((step: number) => {
isEditing.current = true;
diff --git a/src/hooks/useSubStep/types.ts b/src/hooks/useSubStep/types.ts
index ffdee5825197..f3b7939502a8 100644
--- a/src/hooks/useSubStep/types.ts
+++ b/src/hooks/useSubStep/types.ts
@@ -21,6 +21,9 @@ type UseSubStep = {
/** array of components that will become sub steps */
bodyContent: Array>;
+ /** called after each sub step */
+ onNextSubStep?: () => void;
+
/** called on last sub step */
onFinished: () => void;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index d9c96e51ed64..dbdda0d35635 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -670,7 +670,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `approved ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`,
- adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager} cancelled the ${amount} payment.`,
+ adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}cancelled the ${amount} payment.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
diff --git a/src/languages/es.ts b/src/languages/es.ts
index f6c649fdac72..e81efa07a58c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -666,7 +666,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `aprobó ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`,
- adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager} canceló el pago de ${amount}.`,
+ adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}canceló el pago de ${amount}.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó su billetera Expensify en un plazo de 30 días.`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
diff --git a/src/libs/API/parameters/AcceptACHContractForBankAccount.ts b/src/libs/API/parameters/AcceptACHContractForBankAccount.ts
index de4ce4e86857..11ea73bd3a34 100644
--- a/src/libs/API/parameters/AcceptACHContractForBankAccount.ts
+++ b/src/libs/API/parameters/AcceptACHContractForBankAccount.ts
@@ -1,5 +1,5 @@
import type {ACHContractStepProps} from '@src/types/form/ReimbursementAccountForm';
-type AcceptACHContractForBankAccount = ACHContractStepProps & {bankAccountID: number; policyID: string; canUseNewVbbaFlow?: boolean};
+type AcceptACHContractForBankAccount = ACHContractStepProps & {bankAccountID: number; policyID: string};
export default AcceptACHContractForBankAccount;
diff --git a/src/libs/API/parameters/ConnectBankAccountParams.ts b/src/libs/API/parameters/ConnectBankAccountParams.ts
index fb0e3422d08c..b4a4f1d71150 100644
--- a/src/libs/API/parameters/ConnectBankAccountParams.ts
+++ b/src/libs/API/parameters/ConnectBankAccountParams.ts
@@ -8,7 +8,6 @@ type ConnectBankAccountParams = {
plaidMask?: string;
isSavings?: boolean;
policyID?: string;
- canUseNewVbbaFlow?: boolean;
};
export default ConnectBankAccountParams;
diff --git a/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts
index 31eb443ce80e..8e4ae5208a2e 100644
--- a/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts
+++ b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts
@@ -7,7 +7,6 @@ type OpenReimbursementAccountPageParams = {
stepToOpen: ReimbursementAccountStep;
subStep: ReimbursementAccountSubStep;
localCurrentStep: ReimbursementAccountStep;
- canUseNewVbbaFlow?: boolean;
policyID: string;
};
diff --git a/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts b/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts
index dedc45d0365f..310565573454 100644
--- a/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts
+++ b/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts
@@ -1,5 +1,5 @@
-import type {ACHContractStepProps} from '@src/types/form/ReimbursementAccountForm';
+import type {BeneficialOwnersStepProps} from '@src/types/form/ReimbursementAccountForm';
-type UpdateBeneficialOwnersForBankAccountParams = Partial & {bankAccountID: number; policyID: string; canUseNewVbbaFlow?: boolean};
+type UpdateBeneficialOwnersForBankAccountParams = Partial & {bankAccountID: number; policyID: string};
export default UpdateBeneficialOwnersForBankAccountParams;
diff --git a/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts b/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts
index 6421fe02f571..c427e26d6c92 100644
--- a/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts
+++ b/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts
@@ -2,6 +2,6 @@ import type {BankAccountStepProps, CompanyStepProps, ReimbursementAccountProps}
type BankAccountCompanyInformation = BankAccountStepProps & CompanyStepProps & ReimbursementAccountProps;
-type UpdateCompanyInformationForBankAccountParams = Partial & {bankAccountID: number; policyID: string; canUseNewVbbaFlow?: boolean};
+type UpdateCompanyInformationForBankAccountParams = Partial & {bankAccountID: number; policyID: string; confirm: boolean};
export default UpdateCompanyInformationForBankAccountParams;
diff --git a/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts b/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts
index c1a29ddd9cec..4b4876b2863f 100644
--- a/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts
+++ b/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts
@@ -1,5 +1,5 @@
import type {RequestorStepProps} from '@src/types/form/ReimbursementAccountForm';
-type UpdatePersonalInformationForBankAccountParams = RequestorStepProps & {bankAccountID: number; policyID: string; canUseNewVbbaFlow: boolean};
+type UpdatePersonalInformationForBankAccountParams = RequestorStepProps & {bankAccountID: number; policyID: string; confirm: boolean};
export default UpdatePersonalInformationForBankAccountParams;
diff --git a/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
index c11aec9be239..6ef6b3712439 100644
--- a/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
+++ b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
@@ -2,6 +2,5 @@ type VerifyIdentityForBankAccountParams = {
bankAccountID: number;
onfidoData: string;
policyID: string;
- canUseNewVbbaFlow?: boolean;
};
export default VerifyIdentityForBankAccountParams;
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index fc19ba60693c..7c814608dc08 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -81,6 +81,7 @@ const WRITE_COMMANDS = {
TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate',
ADD_COMMENT: 'AddComment',
ADD_ATTACHMENT: 'AddAttachment',
+ ADD_TEXT_AND_ATTACHMENT: 'AddTextAndAttachment',
CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid',
ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount',
RESTART_BANK_ACCOUNT_SETUP: 'RestartBankAccountSetup',
@@ -268,6 +269,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams;
[WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachementParams;
[WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
+ [WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
[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;
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index b94c2c5fad4a..7b1960261182 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -13,6 +13,7 @@ import type {Report} from '@src/types/onyx';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import originalDismissModal from './dismissModal';
import originalDismissModalWithReport from './dismissModalWithReport';
+import originalDismissRHP from './dismissRHP';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import originalGetTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
@@ -61,6 +62,11 @@ const dismissModal = (reportID?: string, ref = navigationRef) => {
originalDismissModalWithReport({reportID, ...report}, ref);
};
+// Re-exporting the dismissRHP here to fill in default value for navigationRef. The dismissRHP isn't defined in this file to avoid cyclic dependencies.
+const dismissRHP = (ref = navigationRef) => {
+ originalDismissRHP(ref);
+};
+
// Re-exporting the dismissModalWithReport here to fill in default value for navigationRef. The dismissModalWithReport isn't defined in this file to avoid cyclic dependencies.
// This method is needed because it allows to dismiss the modal and then open the report. Within this method is checked whether the report belongs to a specific workspace. Sometimes the report we want to check, hasn't been added to the Onyx yet.
// Then we can pass the report as a param without getting it from the Onyx.
@@ -363,6 +369,7 @@ export default {
setShouldPopAllStateOnUP,
navigate,
setParams,
+ dismissRHP,
dismissModal,
dismissModalWithReport,
isActiveRoute,
diff --git a/src/libs/Navigation/dismissRHP.ts b/src/libs/Navigation/dismissRHP.ts
new file mode 100644
index 000000000000..1c497a79600c
--- /dev/null
+++ b/src/libs/Navigation/dismissRHP.ts
@@ -0,0 +1,25 @@
+import type {NavigationContainerRef} from '@react-navigation/native';
+import {StackActions} from '@react-navigation/native';
+import NAVIGATORS from '@src/NAVIGATORS';
+import type {RootStackParamList} from './types';
+
+// This function is in a separate file than Navigation.ts to avoid cyclic dependency.
+
+/**
+ * Dismisses the RHP modal stack if there is any
+ *
+ * @param targetReportID - The reportID to navigate to after dismissing the modal
+ */
+function dismissRHP(navigationRef: NavigationContainerRef) {
+ if (!navigationRef.isReady()) {
+ return;
+ }
+
+ const state = navigationRef.getState();
+ const lastRoute = state.routes.at(-1);
+ if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ navigationRef.dispatch({...StackActions.pop(), target: state.key});
+ }
+}
+
+export default dismissRHP;
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index c768f3b9c07f..e40289f1348f 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -397,7 +397,7 @@ type MoneyRequestNavigatorParamList = {
reportID: string;
backTo: Routes;
reportActionID: string;
- tagIndex: string;
+ orderWeight: string;
};
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: {
action: ValueOf;
@@ -454,6 +454,14 @@ type MoneyRequestNavigatorParamList = {
pageIndex?: string;
backTo?: string;
};
+ [SCREENS.MONEY_REQUEST.STEP_SCAN]: {
+ action: ValueOf;
+ iouType: ValueOf;
+ transactionID: string;
+ reportID: string;
+ pageIndex: number;
+ backTo: Routes;
+ };
[SCREENS.MONEY_REQUEST.STEP_CURRENCY]: {
action: ValueOf;
iouType: ValueOf;
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 98828205d3fe..280ba825761f 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -611,7 +611,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
} else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) {
lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report);
} else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) {
- lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report);
+ lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report, true);
} else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) {
lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction);
} else if (ReportActionUtils.isPendingRemove(lastReportAction) && ReportActionUtils.isThreadParentMessage(lastReportAction, report?.reportID ?? '')) {
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 665830ca7167..1b9423c70ee7 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -181,19 +181,15 @@ function getSortedTagKeys(policyTagList: OnyxEntry): Array, tagIndex: number): string {
+function getTagListName(policyTagList: OnyxEntry, orderWeight: number): string {
if (isEmptyObject(policyTagList)) {
return '';
}
- const policyTagKeys = getSortedTagKeys(policyTagList ?? {});
- const policyTagKey = policyTagKeys[tagIndex] ?? '';
-
- return policyTagList?.[policyTagKey]?.name ?? '';
+ return Object.values(policyTagList).find((tag) => tag.orderWeight === orderWeight)?.name ?? '';
}
-
/**
* Gets all tag lists of a policy
*/
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index b09f58b969f0..69917ce35c6b 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -130,6 +130,10 @@ function isReportPreviewAction(reportAction: OnyxEntry): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
}
+function isReportActionSubmitted(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED;
+}
+
function isModifiedExpenseAction(reportAction: OnyxEntry | ReportAction | Record): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE;
}
@@ -216,7 +220,13 @@ function isTransactionThread(parentReportAction: OnyxEntry | Empty
/**
* Returns the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions with a childReportID. Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
*/
-function getOneTransactionThreadReportID(reportActions: OnyxEntry | ReportAction[]): string | null {
+function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[]): string | null {
+ // If the report is not an IOU or Expense report, it shouldn't be treated as one-transaction report.
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE) {
+ return null;
+ }
+
const reportActionsArray = Object.values(reportActions ?? {});
if (!reportActionsArray.length) {
@@ -444,6 +454,18 @@ function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[] |
return false;
}
+ if (isReportActionSubmitted(currentAction)) {
+ const currentActionAdminAccountID = currentAction.adminAccountID;
+
+ return currentActionAdminAccountID === previousAction.actorAccountID || currentActionAdminAccountID === previousAction.adminAccountID;
+ }
+
+ if (isReportActionSubmitted(previousAction)) {
+ return typeof previousAction.adminAccountID === 'number'
+ ? currentAction.actorAccountID === previousAction.adminAccountID
+ : currentAction.actorAccountID === previousAction.actorAccountID;
+ }
+
return currentAction.actorAccountID === previousAction.actorAccountID;
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 85f5c414dbe4..6197a29cd4a6 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -209,7 +209,19 @@ type OptimisticApprovedReportAction = Pick<
type OptimisticSubmittedReportAction = Pick<
ReportAction,
- 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
+ | 'actionName'
+ | 'actorAccountID'
+ | 'adminAccountID'
+ | 'automatic'
+ | 'avatar'
+ | 'isAttachment'
+ | 'originalMessage'
+ | 'message'
+ | 'person'
+ | 'reportActionID'
+ | 'shouldShow'
+ | 'created'
+ | 'pendingAction'
>;
type OptimisticHoldReportAction = Pick<
@@ -418,7 +430,7 @@ type OptionData = {
notificationPreference?: NotificationPreference | null;
isDisabled?: boolean | null;
name?: string | null;
- isSelfDM?: boolean | null;
+ isSelfDM?: boolean;
reportID?: string;
enabled?: boolean;
data?: Partial;
@@ -1305,7 +1317,7 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | EmptyObject | stri
*/
function isOneTransactionReport(reportID: string): boolean {
const reportActions = reportActionsByReport?.[reportID] ?? ([] as ReportAction[]);
- return ReportActionsUtils.getOneTransactionThreadReportID(reportActions) !== null;
+ return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null;
}
/**
@@ -1313,7 +1325,7 @@ function isOneTransactionReport(reportID: string): boolean {
*/
function isOneTransactionThread(reportID: string, parentReportID: string): boolean {
const parentReportActions = reportActionsByReport?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]);
- const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportActions);
+ const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportID, parentReportActions);
return reportID === transactionThreadReportID;
}
@@ -1956,13 +1968,17 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry, report: OnyxEntry | EmptyObject): string {
+function getReimbursementDeQueuedActionMessage(
+ reportAction: OnyxEntry,
+ report: OnyxEntry | EmptyObject,
+ isLHNPreview = false,
+): string {
const originalMessage = reportAction?.originalMessage as ReimbursementDeQueuedMessage | undefined;
const amount = originalMessage?.amount;
const currency = originalMessage?.currency;
const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency);
if (originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN) {
- const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false) : getDisplayNameForParticipant(report?.managerID) ?? '';
+ const payerOrApproverName = report?.managerID === currentUserAccountID || !isLHNPreview ? '' : getDisplayNameForParticipant(report?.managerID, true);
return Localize.translateLocal('iou.adminCanceledRequest', {manager: payerOrApproverName, amount: formattedAmount});
}
const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? '';
@@ -3082,13 +3098,27 @@ function getPolicyDescriptionText(policy: OnyxEntry): string {
function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number): OptimisticReportAction {
const parser = new ExpensiMark();
const commentText = getParsedComment(text ?? '');
+ const isAttachmentOnly = file && !text;
+ const isTextOnly = text && !file;
+
+ let htmlForNewComment;
+ let textForNewComment;
+ if (isAttachmentOnly) {
+ htmlForNewComment = CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML;
+ textForNewComment = CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML;
+ } else if (isTextOnly) {
+ htmlForNewComment = commentText;
+ textForNewComment = parser.htmlToText(htmlForNewComment);
+ } else {
+ htmlForNewComment = `${commentText}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
+ textForNewComment = `${commentText}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
+ }
+
const isAttachment = !text && file !== undefined;
- const attachmentInfo = isAttachment ? file : {};
- const htmlForNewComment = isAttachment ? CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML : commentText;
+ const attachmentInfo = file ?? {};
const accountID = actorAccountID ?? currentUserAccountID;
// Remove HTML from text when applying optimistic offline comment
- const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment);
return {
commentText,
reportAction: {
@@ -3107,7 +3137,7 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject,
created: DateUtils.getDBTimeWithSkew(),
message: [
{
- translationKey: isAttachment ? CONST.TRANSLATION_KEYS.ATTACHMENT : '',
+ translationKey: isAttachmentOnly ? CONST.TRANSLATION_KEYS.ATTACHMENT : '',
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
html: htmlForNewComment,
text: textForNewComment,
@@ -3559,7 +3589,7 @@ function buildOptimisticMovedReportAction(fromPolicyID: string, toPolicyID: stri
* Builds an optimistic SUBMITTED report action with a randomly generated reportActionID.
*
*/
-function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticSubmittedReportAction {
+function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID: string, adminAccountID: number | undefined): OptimisticSubmittedReportAction {
const originalMessage = {
amount,
currency,
@@ -3569,6 +3599,7 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
return {
actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
actorAccountID: currentUserAccountID,
+ adminAccountID,
automatic: false,
avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
@@ -5014,7 +5045,7 @@ function canUserPerformWriteAction(report: OnyxEntry) {
function getOriginalReportID(reportID: string, reportAction: OnyxEntry): string | undefined {
const reportActions = reportActionsByReport?.[reportID];
const currentReportAction = reportActions?.[reportAction?.reportActionID ?? ''] ?? null;
- const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportActions ?? ([] as ReportAction[]));
+ const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions ?? ([] as ReportAction[]));
if (transactionThreadReportID !== null) {
return Object.keys(currentReportAction ?? {}).length === 0 ? transactionThreadReportID : reportID;
}
@@ -5726,6 +5757,19 @@ function hasActionsWithErrors(reportID: string): boolean {
return Object.values(reportActions ?? {}).some((action) => !isEmptyObject(action.errors));
}
+function getReportActionActorAccountID(reportAction: OnyxEntry, iouReport: OnyxEntry | undefined): number | undefined {
+ switch (reportAction?.actionName) {
+ case CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW:
+ return iouReport ? iouReport.managerID : reportAction?.actorAccountID;
+
+ case CONST.REPORT.ACTIONS.TYPE.SUBMITTED:
+ return reportAction?.adminAccountID ?? reportAction?.actorAccountID;
+
+ default:
+ return reportAction?.actorAccountID;
+ }
+}
+
/**
* @returns the object to update `report.hasOutstandingChildRequest`
*/
@@ -5977,6 +6021,7 @@ export {
isGroupChat,
isTrackExpenseReport,
hasActionsWithErrors,
+ getReportActionActorAccountID,
getGroupChatName,
getOutstandingChildRequest,
};
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index c5439d687089..5a6e31a56e58 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -93,9 +93,9 @@ function getOrderedReportIDs(
);
const isHidden = report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
const isFocused = report.reportID === currentReportId;
- const hasErrors = Object.keys(OptionsListUtils.getAllReportErrors(report, reportActions) ?? {}).length !== 0;
- const hasBrickError = hasErrors || doesReportHaveViolations ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '';
- const shouldOverrideHidden = hasBrickError || isFocused || report.isPinned;
+ const allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) ?? {};
+ const hasErrorsOtherThanFailedReceipt = doesReportHaveViolations || Object.values(allReportErrors).some((error) => error?.[0] !== 'report.genericSmartscanFailureMessage');
+ const shouldOverrideHidden = hasErrorsOtherThanFailedReceipt || isFocused || report.isPinned;
if (isHidden && !shouldOverrideHidden) {
return false;
}
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
index 234868f8322c..fb18d77c779b 100644
--- a/src/libs/actions/BankAccounts.ts
+++ b/src/libs/actions/BankAccounts.ts
@@ -113,7 +113,7 @@ function setPersonalBankAccountContinueKYCOnSuccess(onSuccessFallbackRoute: Rout
function clearPersonalBankAccount() {
clearPlaid();
- Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {});
+ Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, null);
Onyx.set(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, null);
clearPersonalBankAccountSetupType();
}
@@ -188,7 +188,6 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc
plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken,
plaidMask: selectedPlaidBankAccount.mask,
isSavings: selectedPlaidBankAccount.isSavings,
- canUseNewVbbaFlow: true,
policyID,
};
@@ -282,15 +281,17 @@ function deletePaymentBankAccount(bankAccountID: number) {
* This action is called by the requestor step in the Verified Bank Account flow
* @param bankAccountID - ID for bank account
* @param params - User personal data
+ * @param policyID - ID of the policy we're setting the bank account on
+ * @param isConfirmPage - If we're submitting from the confirmation substep, to trigger all external checks
*/
-function updatePersonalInformationForBankAccount(bankAccountID: number, params: RequestorStepProps, policyID: string) {
+function updatePersonalInformationForBankAccount(bankAccountID: number, params: RequestorStepProps, policyID: string, isConfirmPage: boolean) {
API.write(
WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT,
{
...params,
bankAccountID,
policyID,
- canUseNewVbbaFlow: true,
+ confirm: isConfirmPage,
},
getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR),
);
@@ -384,7 +385,6 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS
subStep,
localCurrentStep,
policyID,
- canUseNewVbbaFlow: true,
};
return API.read(READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE, parameters, onyxData);
@@ -393,15 +393,17 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS
/**
* Updates the bank account in the database with the company step data
* @param params - Business step form data
+ * @param policyID - ID of the policy we're setting the bank account on
+ * @param isConfirmPage - If we're submitting from the confirmation substep, to trigger all external checks
*/
-function updateCompanyInformationForBankAccount(bankAccountID: number, params: Partial, policyID: string) {
+function updateCompanyInformationForBankAccount(bankAccountID: number, params: Partial, policyID: string, isConfirmPage: boolean) {
API.write(
WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT,
{
...params,
bankAccountID,
policyID,
- canUseNewVbbaFlow: true,
+ confirm: isConfirmPage,
},
getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY),
);
@@ -418,7 +420,6 @@ function updateBeneficialOwnersForBankAccount(bankAccountID: number, params: Par
...params,
bankAccountID,
policyID,
- canUseNewVbbaFlow: true,
},
getVBBADataForOnyx(),
);
@@ -435,7 +436,6 @@ function acceptACHContractForBankAccount(bankAccountID: number, params: ACHContr
...params,
bankAccountID,
policyID,
- canUseNewVbbaFlow: true,
},
getVBBADataForOnyx(),
);
@@ -454,7 +454,6 @@ function connectBankAccountManually(bankAccountID: number, bankAccount: PlaidBan
plaidAccessToken: bankAccount.plaidAccessToken,
plaidMask: bankAccount.mask,
isSavings: bankAccount.isSavings,
- canUseNewVbbaFlow: true,
policyID,
};
@@ -469,7 +468,6 @@ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoD
bankAccountID,
onfidoData: JSON.stringify(onfidoData),
policyID: policyID ?? '',
- canUseNewVbbaFlow: true,
};
API.write(WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx());
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index c2d462bbc4a8..55dd2eb4fe39 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -1491,7 +1491,7 @@ function getTrackExpenseInformation(
/** Requests money based on a distance (e.g. mileage from a map) */
function createDistanceRequest(
- report: OnyxTypes.Report,
+ report: OnyxEntry,
participant: Participant,
comment: string,
created: string,
@@ -1508,8 +1508,8 @@ function createDistanceRequest(
) {
// If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function
const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report);
- const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report;
- const moneyRequestReportID = isMoneyRequestReport ? report.reportID : '';
+ const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report?.chatReportID) : report;
+ const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : '';
const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created);
const optimisticReceipt: Receipt = {
@@ -1569,7 +1569,7 @@ function createDistanceRequest(
};
API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData);
- Navigation.dismissModal(isMoneyRequestReport ? report.reportID : chatReport.reportID);
+ Navigation.dismissModal(isMoneyRequestReport ? report?.reportID : chatReport.reportID);
Report.notifyNewAction(chatReport.reportID, userAccountID);
}
@@ -4313,7 +4313,7 @@ function deleteTrackExpense(chatReportID: string, transactionID: string, reportA
* @param recipient - The user receiving the money
*/
function getSendMoneyParams(
- report: OnyxTypes.Report,
+ report: OnyxEntry | EmptyObject,
amount: number,
currency: string,
comment: string,
@@ -4332,11 +4332,8 @@ function getSendMoneyParams(
idempotencyKey: Str.guid(),
});
- let chatReport = report.reportID ? report : null;
+ let chatReport = !isEmptyObject(report) && report?.reportID ? report : ReportUtils.getChatByParticipants([recipientAccountID]);
let isNewChat = false;
- if (!chatReport) {
- chatReport = ReportUtils.getChatByParticipants([recipientAccountID]);
- }
if (!chatReport) {
chatReport = ReportUtils.buildOptimisticChatReport([recipientAccountID]);
isNewChat = true;
@@ -4768,7 +4765,7 @@ function getPayMoneyRequestParams(
* @param managerID - Account ID of the person sending the money
* @param recipient - The user receiving the money
*/
-function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant) {
+function sendMoneyElsewhere(report: OnyxEntry, amount: number, currency: string, comment: string, managerID: number, recipient: Participant) {
const {params, optimisticData, successData, failureData} = getSendMoneyParams(report, amount, currency, comment, CONST.IOU.PAYMENT_TYPE.ELSEWHERE, managerID, recipient);
API.write(WRITE_COMMANDS.SEND_MONEY_ELSEWHERE, params, {optimisticData, successData, failureData});
@@ -4782,7 +4779,7 @@ function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency:
* @param managerID - Account ID of the person sending the money
* @param recipient - The user receiving the money
*/
-function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant | ReportUtils.OptionData) {
+function sendMoneyWithWallet(report: OnyxEntry, amount: number, currency: string, comment: string, managerID: number, recipient: Participant | ReportUtils.OptionData) {
const {params, optimisticData, successData, failureData} = getSendMoneyParams(report, amount, currency, comment, CONST.IOU.PAYMENT_TYPE.EXPENSIFY, managerID, recipient);
API.write(WRITE_COMMANDS.SEND_MONEY_WITH_WALLET, params, {optimisticData, successData, failureData});
@@ -4969,11 +4966,12 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full
function submitReport(expenseReport: OnyxTypes.Report) {
const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null;
- const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport?.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID);
const parentReport = ReportUtils.getReport(expenseReport.parentReportID);
const policy = getPolicy(expenseReport.policyID);
const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID;
const isSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy);
+ const adminAccountID = policy.role === CONST.POLICY.ROLE.ADMIN ? currentUserPersonalDetails.accountID : undefined;
+ const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport?.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID, adminAccountID);
const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, isSubmitAndClosePolicy ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.SUBMITTED);
const optimisticData: OnyxUpdate[] = !isSubmitAndClosePolicy
@@ -5277,9 +5275,9 @@ function replaceReceipt(transactionID: string, file: File, source: string) {
* @param transactionID of the transaction to set the participants of
* @param report attached to the transaction
*/
-function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report) {
+function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry) {
// If the report is iou or expense report, we should get the chat report to set participant for request money
- const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report;
+ const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report?.chatReportID) : report;
const currentUserAccountID = currentUserPersonalDetails.accountID;
const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport);
const participants: Participant[] =
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index d2f85362baf8..096125215a32 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -373,9 +373,9 @@ function addActions(reportID: string, text = '', file?: FileObject) {
let reportCommentText = '';
let reportCommentAction: OptimisticAddCommentReportAction | undefined;
let attachmentAction: OptimisticAddCommentReportAction | undefined;
- let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT;
+ let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT | typeof WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT;
- if (text) {
+ if (text && !file) {
const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text);
reportCommentAction = reportComment.reportAction;
reportCommentText = reportComment.commentText;
@@ -385,10 +385,18 @@ function addActions(reportID: string, text = '', file?: FileObject) {
// When we are adding an attachment we will call AddAttachment.
// It supports sending an attachment with an optional comment and AddComment supports adding a single text comment only.
commandName = WRITE_COMMANDS.ADD_ATTACHMENT;
- const attachment = ReportUtils.buildOptimisticAddCommentReportAction('', file);
+ const attachment = ReportUtils.buildOptimisticAddCommentReportAction(text, file);
attachmentAction = attachment.reportAction;
}
+ if (text && file) {
+ // When there is both text and a file, the text for the report comment needs to be parsed)
+ reportCommentText = ReportUtils.getParsedComment(text ?? '');
+
+ // And the API command needs to go to the new API which supports combining both text and attachments in a single report action
+ commandName = WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT;
+ }
+
// Always prefer the file as the last action over text
const lastAction = attachmentAction ?? reportCommentAction;
const currentTime = DateUtils.getDBTimeWithSkew();
@@ -412,7 +420,9 @@ function addActions(reportID: string, text = '', file?: FileObject) {
// Optimistically add the new actions to the store before waiting to save them to the server
const optimisticReportActions: OnyxCollection = {};
- if (text && reportCommentAction?.reportActionID) {
+
+ // Only add the reportCommentAction when there is no file attachment. If there is both a file attachment and text, that will all be contained in the attachmentAction.
+ if (text && reportCommentAction?.reportActionID && !file) {
optimisticReportActions[reportCommentAction.reportActionID] = reportCommentAction;
}
if (file && attachmentAction?.reportActionID) {
diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts
index b1710aa72cbb..26219d72920e 100644
--- a/src/libs/actions/TransactionEdit.ts
+++ b/src/libs/actions/TransactionEdit.ts
@@ -16,24 +16,24 @@ function createBackupTransaction(transaction: OnyxEntry) {
};
// Use set so that it will always fully overwrite any backup transaction that could have existed before
- Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction);
+ Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transaction.transactionID}`, newTransaction);
}
/**
* Removes a transaction from Onyx that was only used temporary in the edit flow
*/
function removeBackupTransaction(transactionID: string) {
- Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, null);
+ Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`, null);
}
-function restoreOriginalTransactionFromBackup(transactionID: string) {
+function restoreOriginalTransactionFromBackup(transactionID: string, isDraft: boolean) {
const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`,
callback: (backupTransaction) => {
Onyx.disconnect(connectionID);
// Use set to completely overwrite the original transaction
- Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, backupTransaction);
+ Onyx.set(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, backupTransaction);
removeBackupTransaction(transactionID);
},
});
diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts
index f8c92351d36c..b59a656d0ac2 100644
--- a/src/libs/fileDownload/types.ts
+++ b/src/libs/fileDownload/types.ts
@@ -8,7 +8,7 @@ type GetImageResolution = (url: File | Asset) => Promise;
type ExtensionAndFileName = {fileName: string; fileExtension: string};
type SplitExtensionFromFileName = (fileName: string) => ExtensionAndFileName;
-type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure: (error?: unknown) => void, fileType?: string) => Promise;
+type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure?: (error?: unknown) => void, fileType?: string) => Promise;
type AttachmentDetails = {
previewSourceURL: null | string;
diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx
index bb6230fbb6a4..5cd0f3ef8026 100644
--- a/src/pages/AddPersonalBankAccountPage.tsx
+++ b/src/pages/AddPersonalBankAccountPage.tsx
@@ -102,6 +102,7 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona
AddPersonalBankAccountPage.displayName = 'AddPersonalBankAccountPage';
export default withOnyx({
+ // @ts-expect-error: ONYXKEYS.PERSONAL_BANK_ACCOUNT is conflicting with ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM
personalBankAccount: {
key: ONYXKEYS.PERSONAL_BANK_ACCOUNT,
},
diff --git a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx
index c07ad9aba587..314f5da988fd 100644
--- a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx
+++ b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx
@@ -119,6 +119,7 @@ export default withOnyx void;
+
+ shouldForceFullScreen?: boolean;
};
// eslint-disable-next-line rulesdir/no-negated-variables
-function NotFoundPage({onBackButtonPress}: NotFoundPageProps) {
+function NotFoundPage({onBackButtonPress, shouldForceFullScreen}: NotFoundPageProps) {
return (
);
diff --git a/src/pages/ReimbursementAccount/BusinessInfo/BusinessInfo.tsx b/src/pages/ReimbursementAccount/BusinessInfo/BusinessInfo.tsx
index f63cf72f8a4f..593408b60589 100644
--- a/src/pages/ReimbursementAccount/BusinessInfo/BusinessInfo.tsx
+++ b/src/pages/ReimbursementAccount/BusinessInfo/BusinessInfo.tsx
@@ -71,22 +71,34 @@ function BusinessInfo({reimbursementAccount, reimbursementAccountDraft, onBackBu
const policyID = reimbursementAccount?.achData?.policyID ?? '';
const values = useMemo(() => getSubstepValues(BUSINESS_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]);
- const submit = useCallback(() => {
- BankAccounts.updateCompanyInformationForBankAccount(
- Number(reimbursementAccount?.achData?.bankAccountID ?? '0'),
- {
- ...values,
- ...getBankAccountFields(['routingNumber', 'accountNumber', 'bankName', 'plaidAccountID', 'plaidAccessToken', 'isSavings']),
- companyTaxID: values.companyTaxID?.replace(CONST.REGEX.NON_NUMERIC, ''),
- companyPhone: parsePhoneNumber(values.companyPhone ?? '', {regionCode: CONST.COUNTRY.US}).number?.significant,
- },
- policyID,
- );
- }, [reimbursementAccount, values, getBankAccountFields, policyID]);
+ const submit = useCallback(
+ (isConfirmPage: boolean) => {
+ BankAccounts.updateCompanyInformationForBankAccount(
+ Number(reimbursementAccount?.achData?.bankAccountID ?? '0'),
+ {
+ ...values,
+ ...getBankAccountFields(['routingNumber', 'accountNumber', 'bankName', 'plaidAccountID', 'plaidAccessToken', 'isSavings']),
+ companyTaxID: values.companyTaxID?.replace(CONST.REGEX.NON_NUMERIC, ''),
+ companyPhone: parsePhoneNumber(values.companyPhone ?? '', {regionCode: CONST.COUNTRY.US}).number?.significant,
+ },
+ policyID,
+ isConfirmPage,
+ );
+ },
+ [reimbursementAccount, values, getBankAccountFields, policyID],
+ );
const startFrom = useMemo(() => getInitialSubstepForBusinessInfo(values), [values]);
- const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo, goToTheLastStep} = useSubStep({bodyContent, startFrom, onFinished: submit});
+ const {
+ componentToRender: SubStep,
+ isEditing,
+ screenIndex,
+ nextScreen,
+ prevScreen,
+ moveTo,
+ goToTheLastStep,
+ } = useSubStep({bodyContent, startFrom, onFinished: () => submit(true), onNextSubStep: () => submit(false)});
const handleBackButtonPress = () => {
if (isEditing) {
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.tsx b/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.tsx
index b259a4ee7e3c..0ce1ae048d17 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.tsx
@@ -47,12 +47,23 @@ function PersonalInfo({reimbursementAccount, reimbursementAccountDraft, onBackBu
const policyID = reimbursementAccount?.achData?.policyID ?? '';
const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]);
const bankAccountID = Number(reimbursementAccount?.achData?.bankAccountID ?? '0');
- const submit = useCallback(() => {
- BankAccounts.updatePersonalInformationForBankAccount(bankAccountID, {...values}, policyID);
- }, [values, bankAccountID, policyID]);
+ const submit = useCallback(
+ (isConfirmPage: boolean) => {
+ BankAccounts.updatePersonalInformationForBankAccount(bankAccountID, {...values}, policyID, isConfirmPage);
+ },
+ [values, bankAccountID, policyID],
+ );
const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(values), [values]);
- const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo, goToTheLastStep} = useSubStep({bodyContent, startFrom, onFinished: submit});
+ const {
+ componentToRender: SubStep,
+ isEditing,
+ screenIndex,
+ nextScreen,
+ prevScreen,
+ moveTo,
+ goToTheLastStep,
+ } = useSubStep({bodyContent, startFrom, onFinished: () => submit(true), onNextSubStep: () => submit(false)});
const handleBackButtonPress = () => {
if (isEditing) {
diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx
index 49e53381e040..ab5bd10317be 100644
--- a/src/pages/RoomInvitePage.tsx
+++ b/src/pages/RoomInvitePage.tsx
@@ -192,6 +192,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) {
-
-
+
);
}
diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx
index 7d2a5bfecbb8..5576f64ba67a 100644
--- a/src/pages/SearchPage/index.tsx
+++ b/src/pages/SearchPage/index.tsx
@@ -1,7 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import isEmpty from 'lodash/isEmpty';
import React, {useEffect, useMemo, useState} from 'react';
-import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -10,6 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/UserListItem';
import useDebouncedState from '@hooks/useDebouncedState';
+import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -37,8 +37,6 @@ type SearchPageOnyxProps = {
type SearchPageProps = SearchPageOnyxProps & StackScreenProps;
-type Options = OptionsListUtils.Options & {headerMessage: string};
-
type SearchPageSectionItem = {
data: OptionData[];
shouldShow: boolean;
@@ -51,7 +49,7 @@ const setPerformanceTimersEnd = () => {
Performance.markEnd(CONST.TIMING.SEARCH_RENDER);
};
-const SearchPageFooterInstance = ;
+const SerachPageFooterInstance = ;
function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) {
const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false);
@@ -75,8 +73,8 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps)
Report.searchInServer(debouncedSearchValue.trim());
}, [debouncedSearchValue]);
- const searchOptions: Options = useMemo(() => {
- if (!areOptionsInitialized) {
+ const searchOptions = useMemo(() => {
+ if (!areOptionsInitialized || !isScreenTransitionEnd) {
return {
recentReports: [],
personalDetails: [],
@@ -91,7 +89,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps)
const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []);
const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), '');
return {...optionList, headerMessage: header};
- }, [areOptionsInitialized, betas, options]);
+ }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]);
const filteredOptions = useMemo(() => {
if (debouncedSearchValue.trim() === '') {
@@ -159,6 +157,8 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps)
setIsScreenTransitionEnd(true);
};
+ const {isDismissed} = useDismissedReferralBanners({referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND});
+
return (
- {({safeAreaPaddingBottomStyle}) => (
- <>
-
-
-
- sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY}
- ListItem={UserListItem}
- textInputValue={searchValue}
- textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
- textInputHint={offlineMessage}
- onChangeText={setSearchValue}
- headerMessage={headerMessage}
- headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined}
- onLayout={setPerformanceTimersEnd}
- onSelectRow={selectReport}
- showLoadingPlaceholder={!areOptionsInitialized}
- footerContent={SearchPageFooterInstance}
- isLoadingNewOptions={isSearchingForReports ?? undefined}
- />
-
- >
- )}
+
+
+ sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY}
+ ListItem={UserListItem}
+ textInputValue={searchValue}
+ textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
+ textInputHint={offlineMessage}
+ onChangeText={setSearchValue}
+ headerMessage={headerMessage}
+ headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined}
+ onLayout={setPerformanceTimersEnd}
+ onSelectRow={selectReport}
+ showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd}
+ footerContent={!isDismissed && SerachPageFooterInstance}
+ isLoadingNewOptions={isSearchingForReports ?? undefined}
+ />
);
}
diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx
index 4f1bac01b556..ce2d8e005e4d 100644
--- a/src/pages/ShareCodePage.tsx
+++ b/src/pages/ShareCodePage.tsx
@@ -1,14 +1,10 @@
-import React, {useMemo, useRef} from 'react';
+import React from 'react';
import {View} from 'react-native';
-import type {ImageSourcePropType} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
import ContextMenuItem from '@components/ContextMenuItem';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
-import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
-import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
@@ -16,11 +12,8 @@ import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Clipboard from '@libs/Clipboard';
-import getPlatform from '@libs/getPlatform';
import Navigation from '@libs/Navigation/Navigation';
-import * as ReportUtils from '@libs/ReportUtils';
import * as Url from '@libs/Url';
-import * as UserUtils from '@libs/UserUtils';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Report} from '@src/types/onyx';
@@ -36,36 +29,14 @@ function ShareCodePage({report}: ShareCodePageProps) {
const themeStyles = useThemeStyles();
const {translate} = useLocalize();
const {environmentURL} = useEnvironment();
- const qrCodeRef = useRef(null);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const isReport = !!report?.reportID;
- const subtitle = useMemo(() => {
- if (isReport) {
- if (ReportUtils.isExpenseReport(report)) {
- return ReportUtils.getPolicyName(report);
- }
- if (ReportUtils.isMoneyRequestReport(report)) {
- // generate subtitle from participants
- return ReportUtils.getVisibleMemberIDs(report)
- .map((accountID) => ReportUtils.getDisplayNameForParticipant(accountID))
- .join(' & ');
- }
-
- return ReportUtils.getParentNavigationSubtitle(report).workspaceName ?? ReportUtils.getChatRoomSubtitle(report);
- }
-
- return currentUserPersonalDetails.login;
- }, [report, currentUserPersonalDetails, isReport]);
-
- const title = isReport ? ReportUtils.getReportName(report) : currentUserPersonalDetails.displayName ?? '';
const urlWithTrailingSlash = Url.addTrailingForwardSlash(environmentURL);
const url = isReport
? `${urlWithTrailingSlash}${ROUTES.REPORT_WITH_ID.getRoute(report.reportID)}`
: `${urlWithTrailingSlash}${ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID ?? '')}`;
- const platform = getPlatform();
- const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID;
return (
@@ -75,17 +46,13 @@ function ShareCodePage({report}: ShareCodePageProps) {
shouldShowBackButton
/>
-
-
-
+ {/*
+ Right now QR code download button is not shown anymore
+ This is a temporary measure because right now it's broken because of the Fabric update.
+ We need to wait for react-native v0.74 to be released so react-native-view-shot gets fixed.
+
+ Please see https://github.com/Expensify/App/issues/40110 to see if it can be re-enabled.
+ */}
- {isNative && (
-
);
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index 57b86bebcde5..dbde94b60e96 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback, useMemo, useState} from 'react';
+import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -13,6 +13,8 @@ import ReferralProgramCTA from '@components/ReferralProgramCTA';
import SelectCircle from '@components/SelectCircle';
import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/UserListItem';
+import useDebouncedState from '@hooks/useDebouncedState';
+import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePermissions from '@hooks/usePermissions';
@@ -36,9 +38,6 @@ const propTypes = {
/** Callback to add participants in MoneyRequestModal */
onAddParticipants: PropTypes.func.isRequired,
- /** An object that holds data about which referral banners have been dismissed */
- dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool),
-
/** Selected participants from MoneyRequestModal with login */
participants: PropTypes.arrayOf(
PropTypes.shape({
@@ -50,43 +49,32 @@ const propTypes = {
}),
),
- /** padding bottom style of safe area */
- safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
/** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string.isRequired,
/** Whether the money request is a distance request or not */
isDistanceRequest: PropTypes.bool,
+
+ /** Whether the screen transition has ended */
+ didScreenTransitionEnd: PropTypes.bool,
};
const defaultProps = {
- dismissedReferralBanners: {},
participants: [],
- safeAreaPaddingBottomStyle: {},
betas: [],
isDistanceRequest: false,
+ didScreenTransitionEnd: false,
};
-function MoneyRequestParticipantsSelector({
- betas,
- dismissedReferralBanners,
- participants,
- navigateToRequest,
- navigateToSplit,
- onAddParticipants,
- safeAreaPaddingBottomStyle,
- iouType,
- isDistanceRequest,
-}) {
+function MoneyRequestParticipantsSelector({betas, participants, navigateToRequest, navigateToSplit, onAddParticipants, iouType, isDistanceRequest, didScreenTransitionEnd}) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const [searchTerm, setSearchTerm] = useState('');
+ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST;
const {isOffline} = useNetwork();
const personalDetails = usePersonalDetails();
+ const {options, areOptionsInitialized} = useOptionsList({shouldInitialize: didScreenTransitionEnd});
const {canUseP2PDistanceRequests} = usePermissions(iouType);
- const {options, areOptionsInitialized} = useOptionsList();
const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached);
@@ -98,7 +86,7 @@ function MoneyRequestParticipantsSelector({
options.reports,
options.personalDetails,
betas,
- searchTerm,
+ debouncedSearchTerm,
participants,
CONST.EXPENSIFY_EMAILS,
@@ -123,7 +111,7 @@ function MoneyRequestParticipantsSelector({
personalDetails: chatOptions.personalDetails,
userToInvite: chatOptions.userToInvite,
};
- }, [options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]);
+ }, [options.reports, options.personalDetails, betas, debouncedSearchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]);
/**
* Returns the sections needed for the OptionsSelector
@@ -134,7 +122,7 @@ function MoneyRequestParticipantsSelector({
const newSections = [];
const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(
- searchTerm,
+ debouncedSearchTerm,
participants,
newChatOptions.recentReports,
newChatOptions.personalDetails,
@@ -172,7 +160,7 @@ function MoneyRequestParticipantsSelector({
}
return newSections;
- }, [maxParticipantsReached, newChatOptions.personalDetails, newChatOptions.recentReports, newChatOptions.userToInvite, participants, personalDetails, searchTerm, translate]);
+ }, [maxParticipantsReached, newChatOptions.personalDetails, newChatOptions.recentReports, newChatOptions.userToInvite, participants, personalDetails, debouncedSearchTerm, translate]);
/**
* Adds a single participant to the request
@@ -247,11 +235,11 @@ function MoneyRequestParticipantsSelector({
OptionsListUtils.getHeaderMessage(
_.get(newChatOptions, 'personalDetails', []).length + _.get(newChatOptions, 'recentReports', []).length !== 0,
Boolean(newChatOptions.userToInvite),
- searchTerm.trim(),
+ debouncedSearchTerm.trim(),
maxParticipantsReached,
- _.some(participants, (participant) => participant.searchText.toLowerCase().includes(searchTerm.trim().toLowerCase())),
+ _.some(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())),
),
- [maxParticipantsReached, newChatOptions, participants, searchTerm],
+ [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm],
);
// Right now you can't split a request with a workspace and other additional participants
@@ -281,13 +269,19 @@ function MoneyRequestParticipantsSelector({
[shouldShowSplitBillErrorMessage, navigateToSplit, addSingleParticipant, participants.length],
);
- const footerContent = useMemo(
- () => (
+ const {isDismissed} = useDismissedReferralBanners({referralContentType});
+
+ const footerContent = useMemo(() => {
+ if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) {
+ return null;
+ }
+ return (
- {!dismissedReferralBanners[referralContentType] && (
-
-
-
+ {!isDismissed && (
+
)}
{shouldShowSplitBillErrorMessage && (
@@ -309,9 +303,8 @@ function MoneyRequestParticipantsSelector({
/>
)}
- ),
- [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate],
- );
+ );
+ }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate]);
const itemRightSideComponent = useCallback(
(item) => {
@@ -345,23 +338,21 @@ function MoneyRequestParticipantsSelector({
);
return (
- 0 ? safeAreaPaddingBottomStyle : {}]}>
-
-
+
);
}
@@ -370,9 +361,6 @@ MoneyRequestParticipantsSelector.displayName = 'MoneyRequestParticipantsSelector
MoneyRequestParticipantsSelector.defaultProps = defaultProps;
export default withOnyx({
- dismissedReferralBanners: {
- key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
- },
betas: {
key: ONYXKEYS.BETAS,
},
diff --git a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx
index 5c8456366c6b..4e74ef3b4b20 100644
--- a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx
@@ -50,7 +50,12 @@ function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFound
}
if (shouldShowNotFoundPage) {
- return Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />;
+ return (
+ Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))}
+ shouldForceFullScreen
+ />
+ );
}
return typeof props.children === 'function' ? props.children(props) : props.children;
diff --git a/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx
index 9b6047493561..7361fc77536b 100644
--- a/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx
@@ -50,7 +50,12 @@ function PaidPolicyAccessOrNotFoundComponent(props: PaidPolicyAccessOrNotFoundCo
}
if (shouldShowNotFoundPage) {
- return Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />;
+ return (
+ Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))}
+ shouldForceFullScreen
+ />
+ );
}
return typeof props.children === 'function' ? props.children(props) : props.children;
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 2e9094f565de..a6a131f5372c 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -247,6 +247,20 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
// We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace
(PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy));
+ // We are checking if the user can access the route.
+ // If user can't access the route, we are dismissing any modals that are open when the NotFound view is shown
+ const canAccessRoute = activeRoute && menuItems.some((item) => item.routeName === activeRoute);
+
+ useEffect(() => {
+ if (!shouldShowNotFoundPage && canAccessRoute) {
+ return;
+ }
+ // We are dismissing any modals that are open when the NotFound view is shown
+ Navigation.isNavigationReady().then(() => {
+ Navigation.dismissRHP();
+ });
+ }, [canAccessRoute, policy, shouldShowNotFoundPage]);
+
const policyAvatar = useMemo(() => {
if (!policy) {
return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR};
diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
index a9d8860ae12a..e18b315b2dd0 100644
--- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx
+++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
@@ -81,6 +81,9 @@ function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsT
setWelcomeNote(parser.htmlToMarkdown(getDefaultWelcomeNote()));
return;
}
+ if (isEmptyObject(policy)) {
+ return;
+ }
Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx
index 3f95c3e02a5b..4a85e01d973a 100644
--- a/src/pages/workspace/WorkspaceInvitePage.tsx
+++ b/src/pages/workspace/WorkspaceInvitePage.tsx
@@ -1,7 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useEffect, useMemo, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {SectionListData} from 'react-native';
-import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -64,7 +63,6 @@ function WorkspaceInvitePage({
invitedEmailsToAccountIDsDraft,
policy,
isLoadingReportData = true,
- didScreenTransitionEnd,
}: WorkspaceInvitePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -72,6 +70,7 @@ function WorkspaceInvitePage({
const [selectedOptions, setSelectedOptions] = useState([]);
const [personalDetails, setPersonalDetails] = useState([]);
const [usersToInvite, setUsersToInvite] = useState([]);
+ const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const openWorkspaceInvitePage = () => {
const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp);
Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs));
@@ -223,18 +222,16 @@ function WorkspaceInvitePage({
setSelectedOptions(newSelectedOptions);
};
- const validate = (): boolean => {
+ const inviteUser = useCallback(() => {
const errors: Errors = {};
if (selectedOptions.length <= 0) {
errors.noUserSelected = 'true';
}
Policy.setWorkspaceErrors(route.params.policyID, errors);
- return isEmptyObject(errors);
- };
+ const isValid = isEmptyObject(errors);
- const inviteUser = () => {
- if (!validate()) {
+ if (!isValid) {
return;
}
@@ -249,7 +246,7 @@ function WorkspaceInvitePage({
});
Policy.setWorkspaceInviteMembersDraft(route.params.policyID, invitedEmailsToAccountIDs);
Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(route.params.policyID));
- };
+ }, [route.params.policyID, selectedOptions]);
const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]);
@@ -271,11 +268,29 @@ function WorkspaceInvitePage({
return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue);
}, [excludedUsers, translate, searchTerm, policyName, usersToInvite, personalDetails.length]);
+ const footerContent = useMemo(
+ () => (
+
+ ),
+ [inviteUser, policy?.alertMessage, selectedOptions.length, shouldShowAlertPrompt, styles, translate],
+ );
+
return (
setDidScreenTransitionEnd(true)}
>
-
-
-
);
diff --git a/src/pages/workspace/WorkspaceJoinUserPage.tsx b/src/pages/workspace/WorkspaceJoinUserPage.tsx
index 09f8e9425c74..7cc8e63da2ee 100644
--- a/src/pages/workspace/WorkspaceJoinUserPage.tsx
+++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx
@@ -39,10 +39,10 @@ function WorkspaceJoinUserPage({route, policy}: WorkspaceJoinUserPageProps) {
}, []);
useEffect(() => {
- if (!policy || isUnmounted.current || isJoinLinkUsed) {
+ if (isUnmounted.current || isJoinLinkUsed) {
return;
}
- if (!isEmptyObject(policy)) {
+ if (!isEmptyObject(policy) && !policy?.isJoinRequestPending) {
Navigation.isNavigationReady().then(() => {
Navigation.goBack(undefined, false, true);
Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID ?? ''));
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 1a76eecb533f..dfaf50c0bcf6 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -7,16 +7,13 @@ import {InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Badge from '@components/Badge';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types';
import ConfirmModal from '@components/ConfirmModal';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import MessagesRow from '@components/MessagesRow';
-import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import TableListItem from '@components/SelectionList/TableListItem';
import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
@@ -47,6 +44,7 @@ import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
+import WorkspacePageWithSections from './WorkspacePageWithSections';
type WorkspaceMembersPageOnyxProps = {
/** Personal details of all users */
@@ -75,16 +73,7 @@ function invertObject(object: Record): Record {
type MemberOption = Omit & {accountID: number};
-function WorkspaceMembersPage({
- policyMembers,
- personalDetails,
- invitedEmailsToAccountIDsDraft,
- route,
- policy,
- session,
- currentUserPersonalDetails,
- isLoadingReportData = true,
-}: WorkspaceMembersPageProps) {
+function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails}: WorkspaceMembersPageProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [selectedEmployees, setSelectedEmployees] = useState([]);
@@ -544,71 +533,63 @@ function WorkspaceMembersPage({
};
return (
-
-
- {
- Navigation.goBack();
- }}
- shouldShowBackButton={isSmallScreenWidth}
- guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
- >
- {!isSmallScreenWidth && getHeaderButtons()}
-
- {isSmallScreenWidth && {getHeaderButtons()}}
- setRemoveMembersConfirmModalVisible(false)}
- prompt={translate('workspace.people.removeMembersPrompt')}
- confirmText={translate('common.remove')}
- cancelText={translate('common.cancel')}
- onModalHide={() => {
- InteractionManager.runAfterInteractions(() => {
- if (!textInputRef.current) {
- return;
- }
- textInputRef.current.focus();
- });
- }}
- />
-
- toggleUser(item.accountID)}
- onSelectAll={() => toggleAllUsers(data)}
- onDismissError={dismissError}
- showLoadingPlaceholder={isLoading}
- showScrollIndicator
- shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
- textInputRef={textInputRef}
- customListHeader={getCustomListHeader()}
- listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ {() => (
+ <>
+ {isSmallScreenWidth && {getHeaderButtons()}}
+ setRemoveMembersConfirmModalVisible(false)}
+ prompt={translate('workspace.people.removeMembersPrompt')}
+ confirmText={translate('common.remove')}
+ cancelText={translate('common.cancel')}
+ onModalHide={() => {
+ InteractionManager.runAfterInteractions(() => {
+ if (!textInputRef.current) {
+ return;
+ }
+ textInputRef.current.focus();
+ });
+ }}
/>
-
-
-
+
+
+ toggleUser(item.accountID)}
+ onSelectAll={() => toggleAllUsers(data)}
+ onDismissError={dismissError}
+ showLoadingPlaceholder={isLoading}
+ showScrollIndicator
+ shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
+ textInputRef={textInputRef}
+ customListHeader={getCustomListHeader()}
+ listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ />
+
+ >
+ )}
+
);
}
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index 4889c1dbe350..4b9b39458312 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -82,6 +82,12 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
* */
icon?: IconAsset;
+ /** Content to be added to the header */
+ headerContent?: ReactNode;
+
+ /** TestID of the component */
+ testID?: string;
+
/** Whether the page is loading, example any other API call in progres */
isLoading?: boolean;
};
@@ -112,6 +118,8 @@ function WorkspacePageWithSections({
shouldShowLoading = true,
shouldShowOfflineIndicatorInWideScreen = false,
shouldShowNonAdmin = false,
+ headerContent,
+ testID,
shouldShowNotFoundPage = false,
isLoading: isPageLoading = false,
}: WorkspacePageWithSectionsProps) {
@@ -160,7 +168,7 @@ function WorkspacePageWithSections({
includeSafeAreaPaddingBottom={false}
shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
- testID={WorkspacePageWithSections.displayName}
+ testID={testID ?? WorkspacePageWithSections.displayName}
shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow}
>
Navigation.goBack(backButtonRoute)}
icon={icon ?? undefined}
style={styles.headerBarDesktopHeight}
- />
+ >
+ {headerContent}
+
{(isLoading || firstRender.current) && shouldShowLoading && isFocused ? (
) : (
diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx
index 340c63c19ea7..916723f60c9f 100644
--- a/src/pages/workspace/WorkspaceProfileSharePage.tsx
+++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx
@@ -1,14 +1,9 @@
-import React, {useRef} from 'react';
+import React from 'react';
import {View} from 'react-native';
-import type {ImageSourcePropType} from 'react-native';
-import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
import ContextMenuItem from '@components/ContextMenuItem';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
-import MenuItem from '@components/MenuItem';
import {useSession} from '@components/OnyxProvider';
-import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
-import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useEnvironment from '@hooks/useEnvironment';
@@ -17,9 +12,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Clipboard from '@libs/Clipboard';
import Navigation from '@libs/Navigation/Navigation';
-import shouldAllowDownloadQRCode from '@libs/shouldAllowDownloadQRCode';
import * as Url from '@libs/Url';
-import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import withPolicy from './withPolicy';
import type {WithPolicyProps} from './withPolicy';
@@ -28,11 +21,9 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) {
const themeStyles = useThemeStyles();
const {translate} = useLocalize();
const {environmentURL} = useEnvironment();
- const qrCodeRef = useRef(null);
const {isSmallScreenWidth} = useWindowDimensions();
const session = useSession();
- const policyName = policy?.name ?? '';
const id = policy?.id ?? '';
const adminEmail = session?.email ?? '';
const urlWithTrailingSlash = Url.addTrailingForwardSlash(environmentURL);
@@ -50,16 +41,13 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) {
/>
-
-
-
+ {/*
+ Right now QR code download button is not shown anymore
+ This is a temporary measure because right now it's broken because of the Fabric update.
+ We need to wait for react-native v0.74 to be released so react-native-view-shot gets fixed.
+
+ Please see https://github.com/Expensify/App/issues/40110 to see if it can be re-enabled.
+ */}
- {shouldAllowDownloadQRCode && (
- qrCodeRef.current?.download?.()}
- wrapperStyle={themeStyles.sectionMenuItemTopDescription}
- />
- )}
diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx
index 392fb90bbd22..c062b6a13f62 100644
--- a/src/pages/workspace/taxes/ValuePage.tsx
+++ b/src/pages/workspace/taxes/ValuePage.tsx
@@ -82,7 +82,7 @@ function ValuePage({
disablePressOnEnter={false}
shouldHideFixErrorsAlert
submitFlexEnabled={false}
- submitButtonStyles={[styles.mh5]}
+ submitButtonStyles={[styles.mh5, styles.mt0]}
>
void;
+ /** Whether the toggle should be disabled */
+ disabled?: boolean;
};
const ICON_SIZE = 48;
-function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction, errors, onCloseError}: ToggleSettingOptionRowProps) {
+function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction, errors, onCloseError, disabled = false}: ToggleSettingOptionRowProps) {
const styles = useThemeStyles();
return (
@@ -77,6 +79,7 @@ function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems,
accessibilityLabel={subtitle}
onToggle={onToggle}
isOn={isActive}
+ disabled={disabled}
/>
{isActive && subMenuItems}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index f165974119ff..f472834c4b33 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -2861,7 +2861,7 @@ const styles = (theme: ThemeColors) =>
},
switchInactive: {
- backgroundColor: theme.border,
+ backgroundColor: theme.icon,
},
switchThumb: {
@@ -2870,6 +2870,8 @@ const styles = (theme: ThemeColors) =>
borderRadius: 11,
position: 'absolute',
left: 4,
+ justifyContent: 'center',
+ alignItems: 'center',
backgroundColor: theme.appBG,
},
@@ -2889,6 +2891,11 @@ const styles = (theme: ThemeColors) =>
alignItems: 'center',
},
+ toggleSwitchLockIcon: {
+ width: variables.iconSizeExtraSmall,
+ height: variables.iconSizeExtraSmall,
+ },
+
checkedContainer: {
backgroundColor: theme.checkBox,
},
diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts
index 98ce460a7669..5b9470c6ca6f 100644
--- a/src/types/onyx/Account.ts
+++ b/src/types/onyx/Account.ts
@@ -1,5 +1,6 @@
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
+import type DismissedReferralBanners from './DismissedReferralBanners';
import type * as OnyxCommon from './OnyxCommon';
type TwoFactorAuthStep = ValueOf | '';
@@ -60,6 +61,7 @@ type Account = {
success?: string;
codesAreCopied?: boolean;
twoFactorAuthStep?: TwoFactorAuthStep;
+ dismissedReferralBanners?: DismissedReferralBanners;
};
export default Account;
diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts
index 6bbcb174a617..7e1827f73954 100644
--- a/src/types/onyx/IOU.ts
+++ b/src/types/onyx/IOU.ts
@@ -21,6 +21,7 @@ type Participant = {
phoneNumber?: string;
text?: string;
isSelected?: boolean;
+ isSelfDM?: boolean;
};
type Split = {
diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts
index 6133f35afa47..dade2052e052 100644
--- a/src/types/onyx/ReportAction.ts
+++ b/src/types/onyx/ReportAction.ts
@@ -226,6 +226,9 @@ type ReportActionBase = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Flag for checking if data is from optimistic data */
isOptimisticAction?: boolean;
+
+ /** The admins's ID */
+ adminAccountID?: number;
}>;
type ReportAction = ReportActionBase & OriginalMessage;
diff --git a/src/types/utils/AssertTypesNotEqual.ts b/src/types/utils/AssertTypesNotEqual.ts
new file mode 100644
index 000000000000..237f54ec2921
--- /dev/null
+++ b/src/types/utils/AssertTypesNotEqual.ts
@@ -0,0 +1,11 @@
+import type {IsEqual} from 'type-fest';
+
+type MatchError = 'Error: Types do match';
+
+/**
+ * The 'AssertTypesNotEqual' type here enforces that `T1` and `T2` do not match.
+ * If `T1` or `T2` are the same this type will cause a compile-time error.
+ */
+type AssertTypesNotEqual extends false ? T1 : TMatchError, TMatchError = MatchError> = T1 & T2;
+
+export default AssertTypesNotEqual;
diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/SearchPage.perf-test.tsx
index ea759a1201b2..33ee900f8b6c 100644
--- a/tests/perf-test/SearchPage.perf-test.tsx
+++ b/tests/perf-test/SearchPage.perf-test.tsx
@@ -9,6 +9,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {measurePerformance} from 'reassure';
import {LocaleContextProvider} from '@components/LocaleContextProvider';
import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
import type {WithNavigationFocusProps} from '@components/withNavigationFocus';
import type {RootStackParamList} from '@libs/Navigation/types';
import {createOptionList} from '@libs/OptionsListUtils';
@@ -75,6 +76,15 @@ jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType
return WithNavigationFocus;
});
+// mock of useDismissedReferralBanners
+jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ __esModule: true,
+ default: jest.fn(() => ({
+ isDismissed: false,
+ setAsDismissed: () => {},
+ })),
+}));
const getMockedReports = (length = 100) =>
createCollection(
@@ -124,7 +134,7 @@ type SearchPageProps = StackScreenProps
+
({
createNavigationContainerRef: jest.fn(),
}));
+jest.mock('../../src/hooks/useKeyboardState', () => ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ __esModule: true,
+ default: jest.fn(() => ({
+ isKeyboardShown: false,
+ keyboardHeight: 0,
+ })),
+}));
+
function SelectionListWrapper({canSelectMultiple}: SelectionListWrapperProps) {
const [selectedIds, setSelectedIds] = useState([]);
diff --git a/web/index.html b/web/index.html
index fb97293ebda5..115803573bbd 100644
--- a/web/index.html
+++ b/web/index.html
@@ -101,7 +101,7 @@
left: 0;
right: 0;
top: 0;
- background-color: #061B09;
+ background-color: #03D47C;
width: 100%;
height: 100%;
display: flex;