-
-To set different domain rules for different members, you can place them into groups. For example, many organizations create different groups for employees and managers since they generally need different domain permissions.
-
-To create a group,
-
-1. Hover over Settings, then click **Domains**.
-2. Click the name of the domain.
-3. Click the **Groups** tab on the left.
-4. Click **Create Group**.
-5. Select all of the group settings and permissions.
- - **Permission Group Name**: Enter a name for the group
- - **Default Group**: Determine if new domain members will be automatically added to this group.
- - **Strictly enforce expense workspace rules**: Determine if all expense rules must be met before people in this group can submit a report.
- - **Restrict primary login selection**: Determine if members of this group will be restricted from using a personal email address to access their Expensify account.
- - **Restrict expense workspace creation/removal**: Determine if members of this group will be allowed to create new workspaces.
- - **Preferred workspace**: Determine if this group will automatically have their expenses and reports posted to a specific workspace.
- - **Set preferred workspace to**: If preferred workspace is enabled, select which workspace members of this group will have set as their preferred workspace.
- - **Expensify Card Preferred Workspace**: If preferred workspace is enabled, determine if Expensify Card transactions for this group will be posted to the preferred workspace listed for the Expensify Card instead of the preferred workspace listed in the above settings.
-6. Click **Save**.
-
-
\ No newline at end of file
diff --git a/docs/articles/expensify-classic/domains/Domain-Groups.md b/docs/articles/expensify-classic/domains/Domain-Groups.md
new file mode 100644
index 000000000000..97f67427ece3
--- /dev/null
+++ b/docs/articles/expensify-classic/domains/Domain-Groups.md
@@ -0,0 +1,42 @@
+---
+title: Domain Groups
+description: How to set different rules for different members of your domain
+---
+
+To set different domain rules for different members, you can place them into groups. This allows organizations to customize permissions based on roles, such as employees and managers, ensuring they have the appropriate access and settings.
+
+---
+# Configuring Domain Groups
+
+1. Hover over **Settings**, then click **Domains**.
+2. Click the name of the domain.
+3. Click the **Groups** tab.
+4. Click **Create Group**.
+5. Configure the group settings and permissions:
+ - **Permission Group Name**: Enter a name for the group.
+ - **Default Group**: Choose if new domain members will be automatically added to this group.
+ - **Strictly enforce expense workspace rules**: Decide if all expense rules must be met before group members can submit a report.
+ - **Restrict primary login selection**: Choose whether members can access their Expensify account using a personal email.
+ - **Restrict expense workspace creation/removal**: Set whether members can create or remove workspaces.
+ - **Preferred workspace**: Select a default workspace for group members' expenses and reports.
+ - **Set preferred workspace to**: If enabled, specify which workspace will be set as preferred.
+ - **Expensify Card Preferred Workspace**: If the preferred workspace is enabled, check whether Expensify Card transactions for this group will be posted to the preferred workspace for the Expensify Card instead of the one in the above settings.
+6. Click **Save**.
+
+---
+# When to Use Different Domain Group Permissions
+
+## Strictly enforce expense workspace rules
+This setting ensures all workspace-level rules are followed before an expense report is submitted. This prevents expense reports from being submitted with missing receipts, incorrect categories, or violations.
+
+## Restrict primary login selection
+Enable this setting to ensure employees use their company email instead of a personal email account when submitting expense reports. This helps to maintain security and compliance within your organization.
+
+## Restrict expense workspace creation/removal
+Set this to prevent employees from creating additional workspaces outside of the company workspace they're already a member of. This will ensure expense reports are always routed through the correct company-approved channels.
+
+## Preferred workspace
+If you have multiple workspaces across several teams, use this setting to assign an employee to their corresponding workspace. For instance, use this for sales teams so their expenses automatically post to the "Sales Department" workspace, reducing the need for manual adjustments.
+
+## Expensify Card Preferred Workspace
+Enable this if your team uses the Expensify Cards for business expenses. This will ensure that all transactions are posted directly to the correct workspace without additional setup.
diff --git a/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md b/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md
deleted file mode 100644
index e83640403ce4..000000000000
--- a/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-title: Create Expense Rules
-description: Automatically categorize, tag, and report expenses based on the merchant's name
----
-
-Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant’s name.
-
-# Create expense rules
-
-1. Hover over **Settings** and click **Account**.
-2. Click **Expense Rules**.
-2. Click **New Rule**.
-3. Add what the merchant name should contain in order for the rule to be applied. *Note: If you enter just a period, the rule will apply to all expenses regardless of the merchant name. Universal Rules will always take precedence over all other expense rules.*
-4. Choose from the following rules:
-- **Merchant:** Updates the merchant name (e.g., “Starbucks #238” could be changed to “Starbucks”)
-- **Category:** Applies a workspace category to the expense
-- **Tag:** Applies a tag to the expense (e.g., a Department or Location)
-- **Description:** Adds a description to the description field on the expense
-- **Reimbursability:** Determines whether the expense will be marked as reimbursable or non-reimbursable
-- **Billable**: Determines whether the expense is billable
-- **Add to a report named:** Adds the expense to a report with the name you type into the field. If no report with that name exists, a new report will be created if the "Create report if necessary" checkbox is selected.
-
-{:width="100%"}
-
-{:start="6"}
-6. (Optional) To apply the rule to previously entered expenses, select the **Apply to existing matching expenses** checkbox. You can also click **Preview Matching Expenses** to see if your rule matches the intended expenses.
-
-# How rules are applied
-
-In general, your expense rules will be applied in order, from **top to bottom**, (i.e., from the first rule). However, other settings can impact how expense rules are applied. Here is the hierarchy that determines how these are applied:
-
-1. A Universal Rule will **always** be applied over any other expense category rules. Rules that would otherwise change the expense category will **not** override the Universal Rule.
-2. If Scheduled Submit and the setting “Enforce Default Report Title” are enabled on the workspace, this will take precedence over any rules trying to add the expense to a report.
-3. If the expense is from a company card that is forced to a workspace with strict rule enforcement, those rules will take precedence over individual expense rules.
-4. If you belong to a workspace that is tied to an accounting integration, the configuration settings for this connection may update your expense details upon export, even if the expense rules were successfully applied to the expense.
-
-# Create an expense rule from changes made to an expense
-
-If you open an expense and change it, you can then create an expense rule based on those changes by selecting the “Create a rule based on your changes" checkbox. *Note: The expense must be saved, reopened, and edited for this option to appear.*
-
-{:width="100%"}
-
-# Delete an expense rule
-
-To delete an expense rule,
-
-1. Hover over **Settings** and click **Account**.
-2. Click **Expense Rules**.
-3. Scroll down to the rule you’d like to remove and click the trash can icon.
-
-{:width="100%"}
-
-{% include faq-begin.md %}
-
-## How can I use expense rules to vendor match when exporting to an accounting package?
-
-When exporting non-reimbursable expenses to your connected accounting package, the payee field will list "Credit Card Misc." if the merchant name on the expense in Expensify is not an exact match to a vendor in the accounting package. When an exact match is unavailable, "Credit Card Misc." prevents multiple variations of the same vendor (e.g., Starbucks and Starbucks #1234, as is often seen in credit card statements) from being created in your accounting package.
-
-For repeated expenses, the best practice is to use Expense Rules, which will automatically update the merchant name without having to do it manually each time. This only works for connections to QuickBooks Online, Desktop, and Xero. Vendor matching cannot be performed in this manner for NetSuite or Sage Intacct due to limitations in the API of the accounting package.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/expenses/Expense-Rules.md b/docs/articles/expensify-classic/expenses/Expense-Rules.md
new file mode 100644
index 000000000000..190c0f70c486
--- /dev/null
+++ b/docs/articles/expensify-classic/expenses/Expense-Rules.md
@@ -0,0 +1,76 @@
+---
+title: Expense Rules
+description: Automatically categorize, tag, and report expenses based on the merchant's name.
+---
+
+Expense rules in Expensify help automate the categorization, tagging, and reporting of expenses based on merchant names, reducing manual work. By setting up these rules at the account level, employees can streamline expense management and ensure consistency across reports.
+
+---
+
+# Create an Expense Rule
+
+1. Hover over **Settings** and click **Account**.
+2. Click **Expense Rules**.
+3. Click **New Rule**.
+4. In the **Merchant Name Contains** field, enter part of the merchant name that should trigger the rule.
+ - **Note:** If you enter only a period (`.`), the rule applies to all expenses. Universal Rules take precedence over all other expense rules.
+5. Select the rules to apply when a matching expense is detected:
+ - **Merchant:** Standardizes the merchant name (e.g., "Starbucks #238" → "Starbucks").
+ - **Category:** Assigns a workspace category to the expense.
+ - **Tag:** Adds a tag (e.g., Department or Location).
+ - **Description:** Updates the description field of the expense.
+ - **Reimbursability:** Marks the expense as reimbursable or non-reimbursable.
+ - **Billable:** Flags the expense as billable.
+ - **Add to a report named:** Assigns the expense to a specific report. If **Create report if necessary** is selected, a new one is created if the report does not exist.
+
+{:width="100%"}
+
+
+6. (Optional) Select **Apply to existing matching expenses** to update past expenses.
+7. Click **Preview Matching Expenses** to check if the rule applies correctly.
+
+---
+
+# How Rules Are Applied
+
+Expense rules are processed from **top to bottom** in the list. However, other settings may override them. The rule hierarchy is:
+
+1. **Universal Rules** always override other expense category rules.
+2. **Scheduled Submit** with **Enforce Default Report Title** enabled takes precedence over expense rule-based report assignments.
+3. **Company Card Rules** for enforced workspaces take priority over individual expense rules.
+4. **Accounting Integrations** may override expense rule settings when expenses are exported.
+
+---
+
+# Create an Expense Rule from an Edited Expense
+
+If you modify an expense manually, you can create a rule based on those changes:
+
+1. Open the expense.
+2. Make the necessary edits.
+3. Select **Create a rule based on your changes** before saving.
+ - **Note:** The option appears only after saving, reopening, and editing an expense.
+
+{:width="100%"}
+
+---
+
+# Delete an Expense Rule
+
+1. Hover over **Settings** and click **Account**.
+2. Click **Expense Rules**.
+3. Find the rule you want to remove and click the **Trash** icon.
+
+{:width="100%"}
+
+---
+
+# FAQ
+
+## How can I use expense rules for vendor matching in an accounting integration?
+
+When exporting non-reimbursable expenses, the **Payee** field in the accounting software will show "Credit Card Misc." if there is no exact match for the merchant name. This prevents multiple variations of the same vendor (e.g., "Starbucks" vs. "Starbucks #1234") from being created.
+
+To avoid this, use **Expense Rules** to standardize vendor names before export.
+- **Supported integrations:** QuickBooks Online, QuickBooks Desktop, Xero.
+- **Not supported for:** NetSuite, Sage Intacct (due to API limitations).
diff --git a/docs/articles/expensify-classic/expenses/Expense-Types.md b/docs/articles/expensify-classic/expenses/Expense-Types.md
index faf670469362..d42d2636db4a 100644
--- a/docs/articles/expensify-classic/expenses/Expense-Types.md
+++ b/docs/articles/expensify-classic/expenses/Expense-Types.md
@@ -1,43 +1,52 @@
---
title: Expense Types
-description: Details of the different Expense filters and Expense Types
+description: Learn how to organize reports by expense type and identify different expense categories in Expensify.
---
-## Organize a Report by Expense Type
-Organizing a report by expense type can make it easier to review expenses on a report.
+Understanding expense types in Expensify helps you track and categorize business spending more effectively. This guide covers how to organize reports by expense type and explains the differences between reimbursable, non-reimbursable, and billable expenses.
+
+# Organize a Report by Expense Type
+
+Organizing reports by expense type helps streamline expense review:
1. Open the desired report.
-2. Click Details in the upper right corner of the report.
-3. Click the View dropdown and select Detailed.
-4. Click the Split by dropdown and select Reimbursable or Billable.
+2. Click **Details** in the upper-right corner.
+3. Click the **View** dropdown and select **Detailed**.
+4. Click the **Split by** dropdown and select **Reimbursable** or **Billable**.
+5. To group expenses further, use the **Group by** dropdown to select **Category** or **Tags**.
-To group the expenses by category or tag, you can also click the Group by dropdown and select Category or Tags.
+---
-## Identify Expense Types
-The right side of every report provides the total for all the expenses. Under the total, there is a breakdown of reimbursable, billable, and non-reimbursable amounts (depending on the expense types that exist on the report).
+# Identify Expense Types
+The right side of every report displays total expenses, broken down by **reimbursable**, **billable**, and **non-reimbursable** amounts.
-- Reimbursable expenses: Expenses paid to the employee, including:
- - Cash & personal card: Expenses paid for by the employee on behalf of the business.
- - Per diem: Expenses for a daily or partial daily rate [configured in your Workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses).
- - Time: An hourly rate for your employees or jobs as [set for your workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Set-time-and-distance-rates). This expense type is usually used by contractors or small businesses billing the customer via [Expensify Invoicing](https://help.expensify.com/articles/expensify-classic/workspaces/Set-Up-Invoicing).
- - Distance: Expenses related to business travel.
-- Non-reimbursable expenses: Expenses directly covered by the business, typically on company cards.
-- Billable expenses: Business or employee expenses that must be billed to a specific client or vendor. This option is for tracking expenses for invoicing to customers, clients, or other departments. Any kind of expense can be billable, in _addition_ to being either reimbursable or non-reimbursable.
+## Reimbursable Expenses
+Expenses paid by employees on behalf of the business, including:
+- **Cash & Personal Card:** Out-of-pocket business expenses.
+- **Per Diem:** Daily expense allowances configured in your [workspace settings](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses).
+- **Time:** Hourly wages for jobs, typically used for contractor invoicing. Configure rates [here](https://help.expensify.com/articles/expensify-classic/workspaces/Set-time-and-distance-rates).
+- **Distance:** Mileage-related expenses.
-{:width="100%"}
+## Non-Reimbursable Expenses
+Expenses that are directly covered by the business, usually on company cards.
-{% include faq-begin.md %}
+## Billable Expenses
+Expenses billed to a client or vendor. Any expense—reimbursable or non-reimbursable—can also be billable.
-**What’s the difference between an expense, a receipt, and a report attachment?**
+{:width="100%"}
-- **Expense:** Created when you SmartScan or manually upload a receipt from a purchase.
-- **Receipt:** A picture file that is automatically attached to the expense during the SmartScan process.
-- **Report Attachments:** Additional documents that need to be submitted to your approver (e.g., supplemental documents to the purchase) can be added to a report any time by clicking the paperclip icon in the comments at the bottom of the report.
+---
-**How are credits or refunds displayed in Expensify?**
+# FAQ
-In Expensify, a credit is displayed as an expense with a minus in front of it (e.g., -$1.00). Expensify defaults all expenses as something that needs to be paid by the company. So a credit that is returned to the company is displayed as a negative expense.
+## What’s the difference between an expense, a receipt, and a report attachment?
+- **Expense:** Created when you SmartScan or manually upload a receipt.
+- **Receipt:** Image file automatically attached to an expense via SmartScan.
+- **Report Attachment:** Additional documents (e.g., supporting documents) added via the paperclip icon in report comments.
-If a report includes a credit or a refund expense, it will offset the total amount on the report. For example, if the report has two reimbursable expenses, one for $400 and one for $500, then the total reimbursable amount is $900. Conversely, an expense for -$400 and one for $500 will be a total reimbursable amount of $500.
+## How are credits or refunds displayed in Expensify?
+Credits appear as **negative expenses** (e.g., -$1.00). They offset the total report amount.
-{% include faq-end.md %}
+For example:
+- A report with **$400** and **$500** reimbursable expenses shows a total of **$900**.
+- A report with **-$400** and **$500** expenses results in a **$100** total.
diff --git a/docs/articles/expensify-classic/getting-started/Join-Your-Company's-Workspace.md b/docs/articles/expensify-classic/getting-started/Join-Your-Company's-Workspace.md
new file mode 100644
index 000000000000..cd58d5d6b64d
--- /dev/null
+++ b/docs/articles/expensify-classic/getting-started/Join-Your-Company's-Workspace.md
@@ -0,0 +1,132 @@
+---
+title: Join Your Company's Workspace
+description: Get started with Expensify as an employee or company member.
+---
+
+Getting started with Expensify is quick and easy, whether you're a new employee or an existing team member joining a company workspace. This guide walks you through downloading the app, setting up your profile, managing expenses, and securing your account.
+
+---
+
+# 1. Download the Mobile App
+
+Upload expenses and check reports from your phone by downloading the Expensify mobile app.
+
+- [iOS](https://apps.apple.com/us/app/expensify-expense-tracker/id471713959)
+- [Android](https://play.google.com/store/apps/details?id=org.me.mobiexpensifyg&hl=en_US&gl=US)
+
+For a full walkthrough on creating and submitting expenses via the mobile app, click [here](https://expensify.navattic.com/fl150n1n)!
+
+---
+
+# 2. Add Your Name and Photo
+
+**Desktop:**
+1. Click your profile image in the main menu.
+2. Hover over the profile picture and click **Change**.
+3. Update your profile:
+ - **Name**: Enter your first and last name, then click **Update**.
+ - **Photo**: Click **Add Photo** to upload an image.
+
+**Mobile:**
+1. Tap the ☰ menu icon in the top left.
+2. Tap your profile picture.
+3. Tap the **Edit** icon to update your name or photo.
+ - **Name**: Enter your first and/or last name, then tap **Update**.
+ - **Photo**: Tap **Upload Photo**, then:
+ - Tap the camera button to take a new photo.
+ - Tap the photo icon to select an existing image.
+
+---
+
+# 3. Meet Concierge
+
+Concierge is your personal assistant, available on both desktop and mobile. It helps by:
+
+- Reminding you to submit expenses.
+- Alerting you when more information is needed.
+- Providing updates on new features.
+
+For support, click the green chat bubble at the bottom of the screen to chat with Concierge.
+
+---
+
+# 4. Learn How to Add an Expense
+
+Employees can document reimbursable and non-reimbursable expenses using SmartScan or manual entry.
+
+## SmartScan a Receipt
+
+**Desktop:**
+1. Click the **Expenses** tab.
+2. Click the **+** icon and select **Scan Receipt**.
+3. Upload a saved receipt image.
+
+**Mobile:**
+1. Tap the camera icon in the bottom right.
+2. Upload or take a receipt photo:
+ - **Upload**: Tap the photo icon and select an image.
+ - **Take a photo**: Ensure details are clear, then capture the image.
+
+*You can also email receipts to receipts@expensify.com from any email address associated with your Expensify account.*
+
+## Manually Enter an Expense
+
+Desktop:
+1. Click the **Expenses** tab.
+2. Click the **+** icon.
+3. Select the expense type and enter details.
+4. Click **Save**.
+
+Mobile:
+1. Tap the ☰ menu icon and select **Expenses**.
+2. Tap the **+** icon.
+3. Select the expense type and enter details.
+4. Tap **Save**.
+
+---
+
+# 5. Create & Submit an Expense Report
+
+Your expenses may be automatically added to a report. If not, follow these steps to create and submit one.
+
+**Desktop:**
+1. Click the **Reports** tab.
+2. Click **New Report** > **Expense Report**.
+3. Click **Add Expenses** and select expenses.
+4. Click **Submit**, enter approver details, and click **Send**.
+
+**Mobile:**
+1. Tap ☰ > **Reports**.
+2. Tap the **+** icon and select **Expense Report**.
+3. Tap **Add Expenses**, then select expenses.
+4. Tap **Submit Report**, add approver details, and tap **Submit**.
+
+---
+
+# 6. Add a Secondary Login
+
+Connect a personal email to ensure access to Expensify, even if your employer changes.
+
+*Setting up this feature is available only on the Expensify website.*
+
+1. Go to **Settings** > **Account**.
+2. Under **Account Details**, click **Add Secondary Login**.
+3. Enter your email or phone number.
+4. Verify your new login with the Magic Code sent to you.
+
+---
+
+# 7. Secure Your Account
+
+Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication (2FA). Setting this up requires you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) to log in.
+
+*Setting up this feature is available only on the Expensify website.*
+
+1. Go to **Settings** > **Account**.
+2. Under **Two-Factor Authentication**, enable the toggle.
+3. Save a copy of your backup codes.
+4. Use an authenticator app to scan the QR code.
+5. Enter the 6-digit code from the app and click **Verify**.
+
+---
+
diff --git a/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md b/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md
deleted file mode 100644
index 6067df6874d4..000000000000
--- a/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md
+++ /dev/null
@@ -1,259 +0,0 @@
----
-title: Join your company's workspace
-description: Get started with Expensify as an employee or other company member
----
-
-
-# Overview
-
-Welcome to Expensify! If you received an invitation to join your company’s Expensify workspace, follow the steps below to get started.
-
-# 1. Download the mobile app
-
-Upload your expenses and check your reports right from your phone by downloading the Expensify mobile app. You can search for “Expensify” in the app store, or tap one of the links below.
-
-[iOS](https://apps.apple.com/us/app/expensify-expense-tracker/id471713959)
-| [Android](https://play.google.com/store/apps/details?id=org.me.mobiexpensifyg&hl=en_US&gl=US)
-
-For a full walkthrough on creating and submitting expenses via the mobile app, click [here](https://expensify.navattic.com/fl150n1n)!
-
-# 2. Add your name and photo
-
-{% include selector.html values="desktop, mobile" %}
-{% include option.html value="desktop" %}
-
-
Click the profile image at the top of the main menu.
-
Hover over the profile picture and click Change.
-
Update your profile picture and name.
-
-
Name: Enter your first and last name into the fields and click Update. Note that this name will be visible to anyone in your company workspace.
-
Photo: Click Add Photo.
-
-
-
-
-{% include end-option.html %}
-
-{% include option.html value="mobile" %}
-
-
-
Tap the ☰ menu icon in the top left.
-
Tap the profile picture icon.
-
Tap the Edit icon next to your name and update your name or photo.
-
-
Name: Enter your first and/or last name into the fields and tap Update. Note that this name will be visible to anyone in your company workspace.
-
Photo: Tap Upload Photo and either:
-
-
Tap the capture button to take a new photo.
-
Tap the photo icon on the left to select a saved photo.
-
-
-
-
-
-{% include end-option.html %}
-{% include end-selector.html %}
-
-
-# 3. Meet Concierge
-Your personal assistant, Concierge, lives on your Expensify Home page on both desktop and the mobile app.
-
-Concierge will walk you through setting up your account and also provide:
-
-
Reminders to do things like submit your expenses
-
Alerts when more information is needed on an expense report
-
Updates on new and improved account features
-
-
-You can also get support at any time by clicking the green chat bubble in the right corner. This will open a chat with Concierge where you can ask questions and receive direct support.
-
-# 4. Learn how to add an expense
-As an employee, you may need to document reimbursable expenses (like business travel paid for with personal funds) or non-reimbursable expenses (like a lunch paid for with a company card). You can create an expense automatically by SmartScanning a receipt, or you can enter it manually.
-
-## SmartScan a receipt
-
-You can upload pictures of your receipts to Expensify, and SmartScan will automatically capture the receipt details, including the merchant, date, total, and currency.
-
-{% include selector.html values="desktop, mobile" %}
-{% include option.html value="desktop" %}
-
-
Click the Expenses tab.
-
Click the + icon in the top right and select Scan Receipt.
-
Upload a saved image of a receipt.
-
-
-{% include end-option.html %}
-
-{% include option.html value="mobile" %}
-
-
Open the mobile app and tap the camera icon in the bottom right corner.
-
Upload or take a photo of your receipt.
-
-
Upload a photo: Click the photo icon in the left corner and select the image from your device.
-
Take a photo: Click the camera icon in the right corner to select the mode, make sure all of the transaction details are clearly visible, and then take the photo.
-
-
Normal Mode: Upload one receipt.
-
Rapid Fire Mode: Upload multiple receipts at once.
-
-{% include end-option.html %}
-{% include end-selector.html %}
-
-You can open any receipt and select **Fill out details myself** to add or edit the merchant, date, total, description, category, or add attendees who took part in the expense. You can also check that the expense is correctly labeled as reimbursable or non-reimbursable and split the expense if multiple expenses are included on one receipt.
-
-*Note: You can also email receipts to SmartScan by sending them to receipts@expensify.com from an email address tied to your Expensify account (either a primary or secondary email). SmartScan will automatically pull all of the details from the receipt, fill them in for you, and add the receipt to the Expenses tab on your account.*
-
-## Manually enter an expense
-
-{% include selector.html values="desktop, mobile" %}
-
-{% include option.html value="desktop" %}
-
-
Click the Expenses tab.
-
Click the + icon in the top right.
-
Select the type of expense and enter the expense details.
-
-
Manually create: Manually enter receipt details.
-
Scan receipt: Upload a saved image of a receipt.
-
Create multiple: Upload expenses in bulk.
-
Time: Create an expense based on hours.
-
Distance: Create an expense based on distance.
-
-
Manually Create: Manually enter the distance details for the expense.
-
Create from Map: Enter the start and end destination and Expensify will help you create a receipt for the trip.
-
-
-
Click Save.
-
-{% include end-option.html %}
-
-{% include option.html value="mobile" %}
-
-
Tap the ☰ menu icon in the top left.
-
Tap Expenses.
-
Tap the + icon in the top right.
-
Tap the correct expense type and enter the expense details.
-
-
Manually create: Manually enter receipt details.
-
Time: Enter work time and rate.
-
Manually create (Distance): Manually enter trip details by total distance.
-
Odometer: Manually enter trip details by start and end odometer readings.
-
Start GPS: Track distance while using the Expensify app to automatically calculate the distance in real time during the trip.
-
-
Tap Save.
-
-{% include end-option.html %}
-
-{% include end-selector.html %}
-
-# 5. Learn how to create & submit an expense report
-
-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.
-
-{% include selector.html values="Desktop, Mobile" %}
-
-{% include option.html value="desktop" %}
-
-
-
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.
-
-
Click New Report, or click the New Report dropdown and select Expense Report.
-
Click Add Expenses.
-
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.
-
-
Once all your expenses are added to the report, click the X to close the pop-up.
-
(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.
-
-
When the report is ready to send for approval, click Submit.
-
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.
-
-
Click Send.
-
-
-{% include end-option.html %}
-
-{% include option.html value="mobile" %}
-
-
Tap the ☰ menu icon in the top left.
-
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.
-
-
Tap the + icon and tap Expense Report.
-
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.
-
(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.
-
Tap the Attachments icon to add additional attachments.
-
-
When the report is ready to send for approval, tap Submit Report.
-
Add any additional sending details and tap Submit.
-
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.
-
-
Tap Submit.
-
-{% include end-option.html %}
-
-{% include end-selector.html %}
-
-# 6. Add a secondary login
-
-Connect your personal email address as a secondary login so you always have access to your Expensify account, even if your employer changes.
-
-*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.*
-
-
-
Hover over Settings, then click Account.
-
Under the Account Details tab, scroll down to the Secondary Logins section and click Add Secondary Login.
-
Enter the email address or phone number you wish to use as a secondary login. For phone numbers, be sure to include the international code, if applicable.
-
Find the email or text message from Expensify containing the Magic Code and enter it into the field to add the secondary login.
-
-
-# 7. Secure your account
-
-Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication. This will require you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) when you log in.
-
-*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.*
-
-
-
Hover over Settings, then click Account.
-
Under the Account Details tab, scroll down to the Two Factor Authentication section and enable the toggle.
-
Save a copy of your backup codes. This step is critical—You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes.
-
-
Click Download to save a copy of your backup codes to your computer.
-
Click Copy to paste the codes into a document or other secure location.
-
-
Click Continue.
-
Download or open your authenticator app and either:
-
-
Scan the QR code shown on your computer screen.
-
Enter the 6-digit code from your authenticator app into Expensify and click Verify.
-
-
-
-When you log in to Expensify in the future, you’ll open your authenticator app to get the code and enter it into Expensify. A new code regenerates every few seconds, so the code is always different. If the code time runs out, you can generate a new code as needed.
-
-
diff --git a/docs/articles/new-expensify/connections/netsuite/Configure-Netsuite.md b/docs/articles/new-expensify/connections/netsuite/Configure-Netsuite.md
index 26db42df9e5b..5f5a54818330 100644
--- a/docs/articles/new-expensify/connections/netsuite/Configure-Netsuite.md
+++ b/docs/articles/new-expensify/connections/netsuite/Configure-Netsuite.md
@@ -4,20 +4,20 @@ description: Configure the Import, Export, and Advanced settings for Expensify's
order: 2
---
-# Best Practices Using NetSuite
+# Best practices using NetSuite
Using Expensify with NetSuite brings a seamless, efficient approach to managing expenses. With automatic syncing, expense reports flow directly into NetSuite, reducing manual entry and errors while giving real-time visibility into spending. This integration speeds up approvals, simplifies reimbursements, and provides clear insights for smarter budgeting and compliance. Together, Expensify and NetSuite make expense management faster, more accurate, and stress-free.
-# Accessing the NetSuite Configuration Settings
+# Accessing the NetSuite configuration settings
NetSuite is connected at the workspace level, and each workspace can have a unique configuration that dictates how the connection functions. To access the connection settings:
-1. Click your profile image or icon in the bottom left menu.
+1. Click Settings in the bottom left menu.
2. Scroll down and click **Workspaces** in the left menu.
3. Select the workspace you want to access settings for.
4. Click **Accounting** in the left menu.
-# Step 1: Configure Import Settings
+# Step 1: Configure import settings
The following steps help you determine how data will be imported from NetSuite to Expensify.
@@ -25,10 +25,10 @@ The following steps help you determine how data will be imported from NetSuite t
2. In the right-hand menu, review each of the following import settings:
- _Categories_: Your NetSuite Expense Categories are automatically imported into Expensify as categories. This is enabled by default and cannot be disabled.
- _Department, Classes, and Locations_: The NetSuite connection allows you to import each independently and utilize tags, report fields, or employee defaults as the coding method.
- - Tags are applied at the expense level and apply to single expense.
+ - Tags are applied at the expense level and apply to a single expense.
- Report Fields are applied at the report header level and apply to all expenses on the report.
- The employee default is applied when the expense is exported to NetSuite and comes from the default on the submitter’s employee record in NetSuite.
- - _Customers and Projects_: The NetSuite connections allows you to import customers and projects into Expensify as Tags or Report Fields.
+ - _Customers and Projects_: The NetSuite connection allows you to import customers and projects into Expensify as Tags or Report Fields.
-_Cross-subsidiary customers/projects_: Enable to import Customers and Projects across all NetSuite subsidiaries to a single Expensify workspace. This setting requires you to enable “Intercompany Time and Expense” in NetSuite. To enable that feature in NetSuite, go to **Setup > Company > Setup Tasks: Enable Features > Advanced Features**.
-_Tax_: Enable to import NetSuite Tax Groups and configure further on the Taxes tab of your workspace settings menu.
-_Custom Segments and Records_: Enable to import segments and records are tags or report fields.
@@ -36,13 +36,13 @@ The following steps help you determine how data will be imported from NetSuite t
- If configuring Custom Records as Tags, use the Field ID on the Transaction Columns tab (under **Custom Segments > Transaction Columns**).
- Don’t use the “Filtered by” feature available for Custom Segments. Expensify can’t make these dependent on other fields. If you do have a filter selected, we suggest switching that filter in NetSuite to “Subsidiary” and enabling all subsidiaries to ensure you don’t receive any errors upon exporting reports.
-_Custom Lists_: Enable to import lists as tags or reports fields.
-3. Sync the connection by closing the right-hand menu and clicking the three-dot icon > Sync Now option. Once the sync completes, you should see the values for any enabled tags or report fields in the corresponding Tag or Report Field tabs in the workspace settings menu.
+3. Sync the connection by closing the right-hand menu and clicking the **three-dot icon** > **Sync Now** option. Once the sync completes, you should see the values for any enabled tags or report fields in the corresponding Tag or Report Field tabs in the workspace settings menu.
{% include info.html %}
When you’re done configuring the settings, or anytime you make changes in the future, sync the NetSuite connection. This will ensure changes are saved and updated across both systems.
{% include end-info.html %}
-# Step 2: Configure Export Settings
+# Step 2: Configure export settings
The following steps help you determine how data will be exported from Expensify to NetSuite.
@@ -59,7 +59,7 @@ The following steps help you determine how data will be exported from Expensify
- _Journal Entries_: Out-of-pocket expenses will be exported to NetSuite as journal entries. All the transactions will be posted to the payable account specified in the workspace. You can also set an approval level in NetSuite for the journal entries.
- By default, journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option. Also, the credit line and header-level classifications are pulled from the employee record.
- _Export company card expenses as_:
- - _Expense Reports_:To export company card expenses as expense reports, you will need to configure your default corporate cards in NetSuite.
+ - _Expense Reports_: To export company card expenses as expense reports, you must configure your default corporate cards in NetSuite.
- _Vendor Bills_: Company card expenses will be posted as a vendor bill payable to the default vendor specified in your workspace Accounting settings. You can also set an approval level in NetSuite for the bills.
- _Journal Entries_: Company Card expenses will be posted to the Journal Entries posting account selected in your workspace Accounting settings.
- Important Notes:
@@ -70,9 +70,9 @@ The following steps help you determine how data will be exported from Expensify
- _Invoice item_: Choose whether Expensify creates an "Expensify invoice line item" for you upon export (if one doesn’t exist already) or select an existing invoice item.
- _Export foreign currency amount_: Enabling this feature allows you to send the original amount of the expense rather than the converted total when exporting to NetSuite. This option is only available when exporting out-of-pocket expenses as Expense Reports.
- _Export to next open period_: When this feature is enabled and you try exporting an expense report to a closed NetSuite period, we will automatically export to the next open period instead of returning an error.
-3. Sync the connection by closing the right-hand menu and clicking the three-dot icon > Sync Now option.
+3. Sync the connection by closing the right-hand menu and clicking the **three-dot icon** > **Sync Now** option.
-# Step 3: Configure Advanced Settings
+# Step 3: Configure advanced settings
The following steps help you determine the advanced settings for your NetSuite connection.
@@ -84,8 +84,8 @@ The following steps help you determine the advanced settings for your NetSuite c
- _Sync reimbursed reports_: Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the NetSuite.
- _Reimbursments account_: Select the account that matches the default account for Bill Payments in your NetSuite account.
- _Collections account_: When exporting invoices, once marked as Paid, the payment is marked against the account selected.
- - _Invite employees and set approvals_: Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will send them an email letting them know they’ve been added to a workspace.
- - In addition to inviting employees, this feature enables a custom set of approval workflow options, which you can manage in Expensify Classic. (Click Switch to Expensify Classic from the Settings menu.)
+ - _Invite employees and set approvals_: Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will email them to let them know they’ve been added to a workspace.
+ This feature invites employees and enables a custom set of approval workflow options, which you can manage in Expensify Classic. (Click Switch to Expensify Classic from the Settings menu.)
- _Auto create employees/vendors_: With this feature enabled, Expensify will automatically create a new employee or vendor in NetSuite (if one doesn’t already exist) using the name and email of the report submitter.
- _Enable newly imported categories_: Toggle to enable this feature and anytime a new Expense Category is created in NetSuite, it will be imported into Expensify as an enabled category. Otherwise, it will import disabled and employees will be unable to see it as an option to code to an expense.
- _Setting approval levels_: You can set the NetSuite approval level for each different export type; Expense report, Vendor bill, and Journal entry.
@@ -93,7 +93,7 @@ The following steps help you determine the advanced settings for your NetSuite c
- _Custom form ID_: By default, Expensify creates entries using the preferred transaction form set in NetSuite. Enabling this setting allows you to designate a specific transaction form.
- _Out-of-pocket expense_:
- _Company card expense_:
-3. Sync the connection by closing the right-hand menu and clicking the three-dot icon > Sync Now option.
+3. Sync the connection by closing the right-hand menu and clicking the **three-dot icon** > **Sync Now** option.
{% include faq-begin.md %}
@@ -107,20 +107,21 @@ Once imported, you can turn specific tags on or off under **Settings > Workspace
Yes, you can automatically import your employees and set their approval workflow with your connection between NetSuite and Expensify.
-Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will send them an email letting them know they’ve been added to a workspace.
+Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will email them to let them know they’ve been added to a workspace.
In addition to inviting employees, this feature enables a custom set of approval workflow options, which you can manage in Expensify Classic. (Click Switch to Expensify Classic from the Settings menu.) Your options for approval include:
-- **Basic Approval:** A single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page.
+- **Basic Approval:** This is a single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page.
- **Manager Approval (default):** Two levels of approval route reports first to an employee’s NetSuite expense approver or supervisor, and second to a workspace-wide Final Approver. By NetSuite convention, Expensify will map to the supervisor if no expense approver exists. The Final Approver defaults to the workspace owner but can be edited on the people page.
- **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured on the workspace’s People settings page. If you enable this setting, it’s recommended you review the newly imported employees and managers on the **Settings > Workspaces > Group > [Workspace Name] > People** page.
## I notice that company card expenses export to NetSuite right away when I approve a report, but reimbursable expenses don’t, why is that?
-When Auto Sync is enabled and you reimburse employees through Expensify, we help to automatically send finalized expenses to NetSuite. The timing of the export depends on the type of expense it is.
- - **If you reimburse members through Expensify:** Reimbursing an expense report will trigger auto-export to NetSuite. When the expense report is exported to NetSuite, a corresponding bill payment will also be created in NetSuite.
- - **If you reimburse members outside of Expensify:** Expense reports will be exported to NetSuite at the time of final approval. After you mark the report as paid in NetSuite, the reimbursed status will be synced back to Expensify the next time the integration syncs.
+When Auto Sync is enabled and you reimburse employees through Expensify, we help you automatically send finalized expenses to NetSuite. The timing of the export depends on the type of expense.
+
+- **If you reimburse members through Expensify:** Reimbursing an expense report will trigger auto-export to NetSuite. When the expense report is exported to NetSuite, a corresponding bill payment will also be created in NetSuite.
+- **If you reimburse members outside of Expensify:** Expense reports will be exported to NetSuite at the time of final approval. After you mark the report as paid in NetSuite, the reimbursed status will be synced back to Expensify the next time the integration syncs.
## How do I configure my default corporate cards in NetSuite?
@@ -147,7 +148,7 @@ You can also select the default account on your employee record to use individua
If a report is reimbursed via ACH or marked as reimbursed in Expensify and then exported to NetSuite, the report is automatically marked as paid in NetSuite.
-If a report is exported to NetSuite, and then marked as paid in NetSuite, the report will automatically be marked as reimbursed in Expensify during the next sync.
+If a report is exported to NetSuite and then marked as paid in NetSuite, it will automatically be marked as reimbursed in Expensify during the next sync.
## Will enabling auto-sync affect existing approved and reimbursed reports?
@@ -157,6 +158,6 @@ Auto-sync will only export newly approved reports to NetSuite. Reports that were
When using multi-currency features with NetSuite, remember these points:
-**Employee/Vendor currency:** The currency set for a NetSuite vendor or employee record must match the subsidiary currency for whichever subsidiary you export that user's reports to. A currency mismatch will cause export errors.
-**Bank Account Currency:** When synchronizing bill payments, your bank account’s currency must match the subsidiary’s currency. Failure to do so will result in an “Invalid Account” error.
+ - **Employee/Vendor currency:** The currency set for a NetSuite vendor or employee record must match the subsidiary currency for whichever subsidiary you export that user's reports to. A currency mismatch will cause export errors.
+ - **Bank Account Currency:** When synchronizing bill payments, your bank account’s currency must match the subsidiary’s currency. Failure to do so will result in an “Invalid Account” error.
{% include faq-end.md %}
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 7775a05a1411..fb692d257c90 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -126,7 +126,6 @@ https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-c
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting
-https://help.expensify.com/articles/expensify-classic/reports/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules
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
@@ -145,7 +144,6 @@ https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/App
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/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
@@ -592,7 +590,6 @@ https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments
https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription
https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page
https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-Overview
-https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Create-Expense-Rules
https://help.expensify.com/articles/expensify-classic/expenses/The-Expenses-Page,https://help.expensify.com/articles/expensify-classic/expenses/Navigate-the-Expenses-Page
https://help.expensify.com/articles/expensify-classic/expenses/Add-expenses-in-bulk,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense
https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense
@@ -651,8 +648,11 @@ https://help.expensify.com/articles/expensify-classic/workspaces/Create-a-group-
https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-company-workspace,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-Company-Workspace
https://help.expensify.com/articles/expensify-classic/workspaces/Set-up-your-individual-workspace,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself
https://help.expensify.com/articles/expensify-classic/travel/Book-with-Expensify-Travel,https://help.expensify.com/articles/new-expensify/travel/Book-with-Expensify-Travel
+https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace,https://help.expensify.com/articles/expensify-classic/getting-started/Join-Your-Company's-Workspace
https://help.expensify.com/articles/expensify-classic/domains/Add-Domain-Members-and-Admins,https://help.expensify.com/articles/expensify-classic/domains/Domain-Members
https://help.expensify.com/articles/expensify-classic/domains/Switch-Domain-Member-to-Admin,https://help.expensify.com/articles/expensify-classic/domains/Domain-Members
+https://help.expensify.com/articles/expensify-classic/domains/Create-A-Group,https://help.expensify.com/articles/expensify-classic/domains/Domain-Groups
https://help.expensify.com/articles/expensify-classic/expenses/Navigate-the-Expenses-Page,https://help.expensify.com/articles/expensify-classic/expenses/Export-Expenses-from-the-Expenses-Page
https://help.expensify.com/articles/expensify-classic/expenses/Export-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Export-Expenses-from-the-Expenses-Page
https://help.expensify.com/articles/expensify-classic/reports/Print-or-download-a-report,https://help.expensify.com/articles/expensify-classic/expenses/Export-Expenses-from-the-Expenses-Page
+https://help.expensify.com/articles/expensify-classic/expenses/Create-Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 754aad26679e..4f85a21d3a14 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -23,7 +23,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 9.1.6
+ 9.1.7CFBundleSignature????CFBundleURLTypes
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.1.6.0
+ 9.1.7.0FullStoryOrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 3b820bfc9d5a..d81a54efbefd 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 9.1.6
+ 9.1.7CFBundleSignature????CFBundleVersion
- 9.1.6.0
+ 9.1.7.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 8db6346d0067..2df8e2a311bd 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.1.6
+ 9.1.7CFBundleVersion
- 9.1.6.0
+ 9.1.7.0NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index d3e395b5b442..51273e15925b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.1.6-0",
+ "version": "9.1.7-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.1.6-0",
+ "version": "9.1.7-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 94acee81e628..f5188ea8fc63 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.1.6-0",
+ "version": "9.1.7-0",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.ts b/src/CONST.ts
index ee924eb883ff..75db0a61a28e 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -90,7 +90,7 @@ const onboardingChoices = {
...backendOnboardingChoices,
} as const;
-const combinedTrackSubmitOnboardingChoices = {
+const createExpenseOnboardingChoices = {
PERSONAL_SPEND: selectableOnboardingChoices.PERSONAL_SPEND,
EMPLOYER: selectableOnboardingChoices.EMPLOYER,
SUBMIT: backendOnboardingChoices.SUBMIT,
@@ -124,16 +124,6 @@ const createWorkspaceTask: OnboardingTask = {
`*Your new workspace is ready!* [Check it out](${workspaceSettingsLink}).`,
};
-const meetGuideTask: OnboardingTask = {
- type: 'meetGuide',
- autoCompleted: false,
- title: 'Meet your setup specialist',
- description: ({adminsRoomLink}) =>
- `Meet your setup specialist, who can answer any questions as you get started with Expensify. Yes, a real human!\n` +
- '\n' +
- `Chat with the specialist in your [#admins room](${adminsRoomLink}).`,
-};
-
const setupCategoriesTask: OnboardingTask = {
type: 'setupCategories',
autoCompleted: false,
@@ -756,7 +746,6 @@ const CONST = {
PREVENT_SPOTNANA_TRAVEL: 'preventSpotnanaTravel',
REPORT_FIELDS_FEATURE: 'reportFieldsFeature',
NETSUITE_USA_TAX: 'netsuiteUsaTax',
- COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit',
PER_DIEM: 'newDotPerDiem',
NEWDOT_MERGE_ACCOUNTS: 'newDotMergeAccounts',
NEWDOT_MANAGER_MCTEST: 'newDotManagerMcTest',
@@ -2961,6 +2950,7 @@ const CONST = {
BREX: 'oauth.brex.com',
WELLS_FARGO: 'oauth.wellsfargo.com',
AMEX_DIRECT: 'oauth.americanexpressfdx.com',
+ CSV: '_ccupload',
},
STEP_NAMES: ['1', '2', '3', '4'],
STEP: {
@@ -3041,6 +3031,7 @@ const CONST = {
VISA: 'visa',
MASTERCARD: 'mastercard',
STRIPE: 'stripe',
+ CSV: 'CSV',
},
FEED_TYPE: {
CUSTOM: 'customFeed',
@@ -3330,7 +3321,7 @@ const CONST = {
GUIDES_CALL_TASK_IDS: {
CONCIERGE_DM: 'NewExpensifyConciergeDM',
WORKSPACE_INITIAL: 'WorkspaceHome',
- WORKSPACE_PROFILE: 'WorkspaceProfile',
+ WORKSPACE_OVERVIEW: 'WorkspaceOverview',
WORKSPACE_INVOICES: 'WorkspaceSendInvoices',
WORKSPACE_MEMBERS: 'WorkspaceManageMembers',
WORKSPACE_EXPENSIFY_CARD: 'WorkspaceExpensifyCard',
@@ -5163,7 +5154,7 @@ const CONST = {
ONBOARDING_CHOICES: {...onboardingChoices},
SELECTABLE_ONBOARDING_CHOICES: {...selectableOnboardingChoices},
- COMBINED_TRACK_SUBMIT_ONBOARDING_CHOICES: {...combinedTrackSubmitOnboardingChoices},
+ CREATE_EXPENSE_ONBOARDING_CHOICES: {...createExpenseOnboardingChoices},
ONBOARDING_SIGNUP_QUALIFIERS: {...signupQualifiers},
ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes},
ONBOARDING_COMPANY_SIZE: {...onboardingCompanySize},
@@ -5184,7 +5175,6 @@ const CONST = {
tasks: [
createWorkspaceTask,
selfGuidedTourTask,
- meetGuideTask,
{
type: 'setupCategoriesAndTags',
autoCompleted: false,
@@ -5283,7 +5273,6 @@ const CONST = {
},
tasks: [
createWorkspaceTask,
- meetGuideTask,
setupCategoriesTask,
{
type: 'inviteAccountant',
@@ -5352,7 +5341,6 @@ const CONST = {
[onboardingChoices.ADMIN]: {
message: "As an admin, learn how to manage your team's workspace and submit expenses yourself.",
tasks: [
- meetGuideTask,
{
type: 'reviewWorkspaceSettings',
autoCompleted: false,
@@ -5390,11 +5378,11 @@ const CONST = {
},
} satisfies Record,
- COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES: {
- [combinedTrackSubmitOnboardingChoices.PERSONAL_SPEND]: combinedTrackSubmitOnboardingPersonalSpendMessage,
- [combinedTrackSubmitOnboardingChoices.EMPLOYER]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage,
- [combinedTrackSubmitOnboardingChoices.SUBMIT]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage,
- } satisfies Record, OnboardingMessage>,
+ CREATE_EXPENSE_ONBOARDING_MESSAGES: {
+ [createExpenseOnboardingChoices.PERSONAL_SPEND]: combinedTrackSubmitOnboardingPersonalSpendMessage,
+ [createExpenseOnboardingChoices.EMPLOYER]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage,
+ [createExpenseOnboardingChoices.SUBMIT]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage,
+ } satisfies Record, OnboardingMessage>,
REPORT_FIELD_TITLE_FIELD_ID: 'text_title',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 50a768955b25..86504d344c96 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -809,35 +809,35 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/invite-message',
getRoute: (policyID: string, backTo?: string) => `${getUrlWithBackToParam(`settings/workspaces/${policyID}/invite-message`, backTo)}` as const,
},
- WORKSPACE_PROFILE: {
- route: 'settings/workspaces/:policyID/profile',
+ WORKSPACE_OVERVIEW: {
+ route: 'settings/workspaces/:policyID/overview',
getRoute: (policyID: string | undefined, backTo?: string) => {
if (!policyID) {
- Log.warn('Invalid policyID is used to build the WORKSPACE_PROFILE route');
+ Log.warn('Invalid policyID is used to build the WORKSPACE_OVERVIEW route');
}
- return getUrlWithBackToParam(`settings/workspaces/${policyID}/profile` as const, backTo);
+ return getUrlWithBackToParam(`settings/workspaces/${policyID}/overview` as const, backTo);
},
},
- WORKSPACE_PROFILE_ADDRESS: {
- route: 'settings/workspaces/:policyID/profile/address',
+ WORKSPACE_OVERVIEW_ADDRESS: {
+ route: 'settings/workspaces/:policyID/overview/address',
getRoute: (policyID: string | undefined, backTo?: string) => {
if (!policyID) {
- Log.warn('Invalid policyID is used to build the WORKSPACE_PROFILE_ADDRESS route');
+ Log.warn('Invalid policyID is used to build the WORKSPACE_OVERVIEW_ADDRESS route');
}
- return getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo);
+ return getUrlWithBackToParam(`settings/workspaces/${policyID}/overview/address` as const, backTo);
},
},
- WORKSPACE_PROFILE_PLAN: {
- route: 'settings/workspaces/:policyID/profile/plan',
- getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/plan` as const, backTo),
+ WORKSPACE_OVERVIEW_PLAN: {
+ route: 'settings/workspaces/:policyID/overview/plan',
+ getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/overview/plan` as const, backTo),
},
WORKSPACE_ACCOUNTING: {
route: 'settings/workspaces/:policyID/accounting',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const,
},
- WORKSPACE_PROFILE_CURRENCY: {
- route: 'settings/workspaces/:policyID/profile/currency',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/currency` as const,
+ WORKSPACE_OVERVIEW_CURRENCY: {
+ route: 'settings/workspaces/:policyID/overview/currency',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/overview/currency` as const,
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export',
@@ -987,22 +987,22 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/import/items',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/import/items` as const,
},
- WORKSPACE_PROFILE_NAME: {
- route: 'settings/workspaces/:policyID/profile/name',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/name` as const,
+ WORKSPACE_OVERVIEW_NAME: {
+ route: 'settings/workspaces/:policyID/overview/name',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/overview/name` as const,
},
- WORKSPACE_PROFILE_DESCRIPTION: {
- route: 'settings/workspaces/:policyID/profile/description',
+ WORKSPACE_OVERVIEW_DESCRIPTION: {
+ route: 'settings/workspaces/:policyID/overview/description',
getRoute: (policyID: string | undefined) => {
if (!policyID) {
- Log.warn('Invalid policyID is used to build the WORKSPACE_PROFILE_DESCRIPTION route');
+ Log.warn('Invalid policyID is used to build the WORKSPACE_OVERVIEW_DESCRIPTION route');
}
- return `settings/workspaces/${policyID}/profile/description` as const;
+ return `settings/workspaces/${policyID}/overview/description` as const;
},
},
- WORKSPACE_PROFILE_SHARE: {
- route: 'settings/workspaces/:policyID/profile/share',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/share` as const,
+ WORKSPACE_OVERVIEW_SHARE: {
+ route: 'settings/workspaces/:policyID/overview/share',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/overview/share` as const,
},
WORKSPACE_AVATAR: {
route: 'settings/workspaces/:policyID/avatar',
@@ -1473,7 +1473,7 @@ const ROUTES = {
},
WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT: {
route: 'settings/workspaces/:policyID/expensify-card/settings/account',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/account` as const,
+ getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/settings/account`, backTo),
},
WORKSPACE_EXPENSIFY_CARD_SETTINGS_FREQUENCY: {
route: 'settings/workspaces/:policyID/expensify-card/settings/frequency',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index d950fb1cd5db..5a8b0c75d5c0 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -468,7 +468,7 @@ const SCREENS = {
MULTI_CONNECTION_SELECTOR: 'Policy_Accounting_Multi_Connection_Selector',
},
INITIAL: 'Workspace_Initial',
- PROFILE: 'Workspace_Profile',
+ PROFILE: 'Workspace_Overview',
COMPANY_CARDS: 'Workspace_CompanyCards',
COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard',
COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed',
@@ -531,9 +531,9 @@ const SCREENS = {
TAG_APPROVER: 'Tag_Approver',
TAG_LIST_VIEW: 'Tag_List_View',
TAG_GL_CODE: 'Tag_GL_Code',
- CURRENCY: 'Workspace_Profile_Currency',
- ADDRESS: 'Workspace_Profile_Address',
- PLAN: 'Workspace_Profile_Plan_Type',
+ CURRENCY: 'Workspace_Overview_Currency',
+ ADDRESS: 'Workspace_Overview_Address',
+ PLAN: 'Workspace_Overview_Plan_Type',
WORKFLOWS: 'Workspace_Workflows',
WORKFLOWS_PAYER: 'Workspace_Workflows_Payer',
WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New',
@@ -542,9 +542,9 @@ const SCREENS = {
WORKFLOWS_APPROVALS_APPROVER: 'Workspace_Workflows_Approvals_Approver',
WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency',
WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset',
- DESCRIPTION: 'Workspace_Profile_Description',
- SHARE: 'Workspace_Profile_Share',
- NAME: 'Workspace_Profile_Name',
+ DESCRIPTION: 'Workspace_Overview_Description',
+ SHARE: 'Workspace_Overview_Share',
+ NAME: 'Workspace_Overview_Name',
CATEGORY_CREATE: 'Category_Create',
CATEGORY_EDIT: 'Category_Edit',
CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code',
diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx
index 846cf23e4c8a..2c442df75060 100644
--- a/src/components/BookTravelButton.tsx
+++ b/src/components/BookTravelButton.tsx
@@ -73,7 +73,7 @@ function BookTravelButton({text}: BookTravelButtonProps) {
// Spotnana requires an address anytime an entity is created for a policy
if (isEmptyObject(policy?.address)) {
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy?.id, Navigation.getActiveRoute()));
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_ADDRESS.getRoute(policy?.id, Navigation.getActiveRoute()));
return;
}
diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx
index 0bc3dba642cf..bffac55007fa 100644
--- a/src/components/BrokenConnectionDescription.tsx
+++ b/src/components/BrokenConnectionDescription.tsx
@@ -3,8 +3,8 @@ import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionViolations from '@hooks/useTransactionViolations';
-import {isInstantSubmitEnabled, isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils';
-import {isCurrentUserSubmitter, isProcessingReport, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils';
+import {isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils';
+import {isCurrentUserSubmitter, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -52,7 +52,7 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn
);
}
- if (isReportApproved({report}) || isReportManuallyReimbursed(report) || (isProcessingReport(report) && !isInstantSubmitEnabled(policy))) {
+ if (isReportApproved({report}) || isReportManuallyReimbursed(report)) {
return translate('violations.memberBrokenConnectionError');
}
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
index 69e542e44798..058b2e564f89 100644
--- a/src/components/EmptyStateComponent/index.tsx
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -89,7 +89,7 @@ function EmptyStateComponent({
contentContainerStyle={[{minHeight: minModalHeight}, styles.flexGrow1, styles.flexShrink0, containerStyles]}
style={styles.flex1}
>
-
+ [reportActions, reports, reportNameValuePairs, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale],
- [reportActions, reports, reportNameValuePairs, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale],
+ () => [reportActions, reports, reportNameValuePairs, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale, transactions],
+ [reportActions, reports, reportNameValuePairs, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale, transactions],
);
const previousOptionMode = usePrevious(optionMode);
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 9c47dd0da547..9cd82fd4fad5 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -10,7 +10,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {getConnectedIntegration, isPolicyAdmin} from '@libs/PolicyUtils';
+import {getConnectedIntegration} from '@libs/PolicyUtils';
import {getOriginalMessage, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
import {
canBeExported,
@@ -27,13 +27,13 @@ import {
isAllowedToSubmitDraftExpenseReport,
isArchivedReportWithID,
isClosedExpenseReportWithNoExpenses,
- isCurrentUserSubmitter,
isInvoiceReport,
navigateBackOnDeleteTransaction,
reportTransactionsSelector,
} from '@libs/ReportUtils';
import {
allHavePendingRTERViolation,
+ checkIfShouldShowMarkAsCashButton,
isDuplicate as isDuplicateTransactionUtils,
isExpensifyCardTransaction,
isOnHold as isOnHoldTransactionUtils,
@@ -150,11 +150,12 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const hasOnlyPendingTransactions = useMemo(() => {
return !!transactions && transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));
}, [transactions]);
- const transactionIDs = transactions?.map((t) => t.transactionID) ?? [];
- const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {
- selector: (allTransactions) =>
- Object.fromEntries(Object.entries(allTransactions ?? {}).filter(([key]) => transactionIDs.includes(key.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, '')))),
- });
+ const transactionIDs = useMemo(() => transactions?.map((t) => t.transactionID) ?? [], [transactions]);
+ const [allViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
+ const violations = useMemo(
+ () => Object.fromEntries(Object.entries(allViolations ?? {}).filter(([key]) => transactionIDs.includes(key.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, '')))),
+ [allViolations, transactionIDs],
+ );
// Check if there is pending rter violation in all transactionViolations with given transactionIDs.
const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDs, violations);
// Check if user should see broken connection violation warning.
@@ -173,8 +174,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
const shouldShowMarkAsCashButton =
- !!transactionThreadReportID &&
- (hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(moneyRequestReport?.reportID))));
+ !!transactionThreadReportID && checkIfShouldShowMarkAsCashButton(hasAllPendingRTERViolations, shouldShowBrokenConnectionViolation, moneyRequestReport, policy);
const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere;
@@ -300,7 +300,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
if (hasOnlyHeldExpenses) {
return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expensesOnHold')};
}
- if (shouldShowBrokenConnectionViolation) {
+ if (!!transaction?.transactionID && shouldShowBrokenConnectionViolation) {
return {
icon: getStatusIcon(Expensicons.Hourglass),
description: (
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index 5dfbbe9fa727..aed7163b2921 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -10,10 +10,9 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionViolations from '@hooks/useTransactionViolations';
import Navigation from '@libs/Navigation/Navigation';
-import {isPolicyAdmin} from '@libs/PolicyUtils';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
-import {isCurrentUserSubmitter} from '@libs/ReportUtils';
import {
+ checkIfShouldShowMarkAsCashButton,
hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils,
hasReceipt,
isDuplicate as isDuplicateTransactionUtils,
@@ -83,8 +82,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
const hasPendingRTERViolation = hasPendingRTERViolationTransactionUtils(transactionViolations);
- const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction, report, policy, transactionViolations);
- const shouldShowMarkAsCashButton = hasPendingRTERViolation || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(parentReport?.reportID)));
+ const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction, parentReport, policy, transactionViolations);
+ const shouldShowMarkAsCashButton = checkIfShouldShowMarkAsCashButton(hasPendingRTERViolation, shouldShowBrokenConnectionViolation, parentReport, policy);
const markAsCash = useCallback(() => {
markAsCashAction(transaction?.transactionID, reportID);
@@ -115,7 +114,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
description: (
),
diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
index 020553d9b1f5..0fb38f36ebd9 100644
--- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
+++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
@@ -258,7 +258,7 @@ function SearchPageHeaderInput({
{showPopupButton && (
+
+
+
+
+
);
}
diff --git a/src/components/ValuePicker/index.tsx b/src/components/ValuePicker/index.tsx
index cb604c855cb4..80974aeb78a6 100644
--- a/src/components/ValuePicker/index.tsx
+++ b/src/components/ValuePicker/index.tsx
@@ -2,15 +2,12 @@ import React, {forwardRef, useState} from 'react';
import type {ForwardedRef} from 'react';
import {View} from 'react-native';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import useStyleUtils from '@hooks/useStyleUtils';
import Navigation from '@libs/Navigation/Navigation';
-import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {ValuePickerItem, ValuePickerProps} from './types';
import ValueSelectorModal from './ValueSelectorModal';
function ValuePicker({value, label, items, placeholder = '', errorText = '', onInputChange, furtherDetails, shouldShowTooltips = true}: ValuePickerProps, forwardedRef: ForwardedRef) {
- const StyleUtils = useStyleUtils();
const [isPickerVisible, setIsPickerVisible] = useState(false);
const showPickerModal = () => {
@@ -28,7 +25,6 @@ function ValuePicker({value, label, items, placeholder = '', errorText = '', onI
hidePickerModal();
};
- const descStyle = !value || value.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null;
const selectedItem = items?.find((item) => item.value === value);
return (
@@ -38,7 +34,6 @@ function ValuePicker({value, label, items, placeholder = '', errorText = '', onI
shouldShowRightIcon
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
title={selectedItem?.label || placeholder || ''}
- descriptionTextStyle={descStyle}
description={label}
onPress={showPickerModal}
furtherDetails={furtherDetails}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 68e1649e1fa0..73da02793813 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -5019,6 +5019,7 @@ const translations = {
cardFeeds: 'Card feeds',
cardFeedName: ({cardFeedBankName, cardFeedLabel}: {cardFeedBankName: string; cardFeedLabel?: string}) =>
`All ${cardFeedBankName}${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`,
+ cardFeedNameCSV: ({cardFeedLabel}: {cardFeedLabel?: string}) => `All CSV Imported Cards${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`,
},
current: 'Current',
past: 'Past',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 9ab166330e07..0d54c36a90cf 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -5068,6 +5068,7 @@ const translations = {
cardFeeds: 'Flujos de tarjetas',
cardFeedName: ({cardFeedBankName, cardFeedLabel}: {cardFeedBankName: string; cardFeedLabel?: string}) =>
`Todo ${cardFeedBankName}${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`,
+ cardFeedNameCSV: ({cardFeedLabel}: {cardFeedLabel?: string}) => `Todas las Tarjetas Importadas desde CSV${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`,
},
amount: {
lessThan: ({amount}: OptionalParam = {}) => `Menos de ${amount ?? ''}`,
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index c1a0341c7166..03b5103a6491 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -1000,6 +1000,7 @@ const READ_COMMANDS = {
GET_ASSIGNED_SUPPORT_DATA: 'GetAssignedSupportData',
OPEN_WORKSPACE_PLAN_PAGE: 'OpenWorkspacePlanPage',
GET_CORPAY_ONBOARDING_FIELDS: 'GetCorpayOnboardingFields',
+ OPEN_SECURITY_SETTINGS_PAGE: 'OpenSecuritySettingsPage',
} as const;
type ReadCommand = ValueOf;
@@ -1069,6 +1070,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.GET_ASSIGNED_SUPPORT_DATA]: Parameters.GetAssignedSupportDataParams;
[READ_COMMANDS.OPEN_WORKSPACE_PLAN_PAGE]: Parameters.OpenWorkspacePlanPageParams;
[READ_COMMANDS.GET_CORPAY_ONBOARDING_FIELDS]: Parameters.GetCorpayOnboardingFieldsParams;
+ [READ_COMMANDS.OPEN_SECURITY_SETTINGS_PAGE]: null;
};
const SIDE_EFFECT_REQUEST_COMMANDS = {
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index 088c8d2f6e5e..3024bd21e394 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import ExpensifyCardImage from '@assets/images/expensify-card.svg';
+import type IllustrationsType from '@styles/theme/illustrations/types';
import * as Illustrations from '@src/components/Icon/Illustrations';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
@@ -261,7 +262,7 @@ function sortCardsByCardholderName(cardsList: OnyxEntry, per
});
}
-function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD.BANK): IconAsset {
+function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD.BANK, illustrations: IllustrationsType): IconAsset {
const feedIcons = {
[CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: Illustrations.VisaCompanyCardDetailLarge,
[CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX]: Illustrations.AmexCardCompanyCardDetailLarge,
@@ -274,6 +275,7 @@ function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD
[CONST.COMPANY_CARD.FEED_BANK_NAME.WELLS_FARGO]: Illustrations.WellsFargoCompanyCardDetailLarge,
[CONST.COMPANY_CARD.FEED_BANK_NAME.BREX]: Illustrations.BrexCompanyCardDetailLarge,
[CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE]: Illustrations.StripeCompanyCardDetailLarge,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CSV]: illustrations.GenericCSVCompanyCardLarge,
[CONST.EXPENSIFY_CARD.BANK]: ExpensifyCardImage,
};
@@ -292,7 +294,11 @@ function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD
return feedIcons[feedKey];
}
- return Illustrations.AmexCompanyCards;
+ if (cardFeed.includes(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV)) {
+ return illustrations.GenericCSVCompanyCardLarge;
+ }
+
+ return illustrations.GenericCompanyCardLarge;
}
/**
@@ -326,11 +332,16 @@ function getBankName(feedType: CompanyCardFeed): string {
[CONST.COMPANY_CARD.FEED_BANK_NAME.CITIBANK]: 'Citibank',
[CONST.COMPANY_CARD.FEED_BANK_NAME.WELLS_FARGO]: 'Wells Fargo',
[CONST.COMPANY_CARD.FEED_BANK_NAME.BREX]: 'Brex',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CSV]: CONST.COMPANY_CARDS.CARD_TYPE.CSV,
};
// In existing OldDot setups other variations of feeds could exist, ex: vcf2, vcf3, oauth.americanexpressfdx.com 2003
const feedKey = (Object.keys(feedNamesMapping) as CompanyCardFeed[]).find((feed) => feedType.startsWith(feed));
+ if (feedType.includes(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV)) {
+ return CONST.COMPANY_CARDS.CARD_TYPE.CSV;
+ }
+
if (!feedKey) {
return '';
}
@@ -338,7 +349,7 @@ function getBankName(feedType: CompanyCardFeed): string {
return feedNamesMapping[feedKey];
}
-const getBankCardDetailsImage = (bank: ValueOf): IconAsset => {
+const getBankCardDetailsImage = (bank: ValueOf, illustrations: IllustrationsType): IconAsset => {
const iconMap: Record, IconAsset> = {
[CONST.COMPANY_CARDS.BANKS.AMEX]: Illustrations.AmexCardCompanyCardDetail,
[CONST.COMPANY_CARDS.BANKS.BANK_OF_AMERICA]: Illustrations.BankOfAmericaCompanyCardDetail,
@@ -348,7 +359,7 @@ const getBankCardDetailsImage = (bank: ValueOf
[CONST.COMPANY_CARDS.BANKS.WELLS_FARGO]: Illustrations.WellsFargoCompanyCardDetail,
[CONST.COMPANY_CARDS.BANKS.BREX]: Illustrations.BrexCompanyCardDetail,
[CONST.COMPANY_CARDS.BANKS.STRIPE]: Illustrations.StripeCompanyCardDetail,
- [CONST.COMPANY_CARDS.BANKS.OTHER]: Illustrations.OtherCompanyCardDetail,
+ [CONST.COMPANY_CARDS.BANKS.OTHER]: illustrations.GenericCompanyCard,
};
return iconMap[bank];
};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 2058a317aa2b..91939c86f07f 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -279,12 +279,12 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/WorkspaceInviteMessagePage').default,
[SCREENS.WORKSPACE.WORKFLOWS_PAYER]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPayerPage').default,
[SCREENS.WORKSPACE.NAME]: () => require('../../../../pages/workspace/WorkspaceNamePage').default,
- [SCREENS.WORKSPACE.DESCRIPTION]: () => require('../../../../pages/workspace/WorkspaceProfileDescriptionPage').default,
- [SCREENS.WORKSPACE.SHARE]: () => require('../../../../pages/workspace/WorkspaceProfileSharePage').default,
- [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../../pages/workspace/WorkspaceProfileCurrencyPage').default,
+ [SCREENS.WORKSPACE.DESCRIPTION]: () => require('../../../../pages/workspace/WorkspaceOverviewDescriptionPage').default,
+ [SCREENS.WORKSPACE.SHARE]: () => require('../../../../pages/workspace/WorkspaceOverviewSharePage').default,
+ [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../../pages/workspace/WorkspaceOverviewCurrencyPage').default,
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../../pages/workspace/categories/CategorySettingsPage').default,
- [SCREENS.WORKSPACE.ADDRESS]: () => require('../../../../pages/workspace/WorkspaceProfileAddressPage').default,
- [SCREENS.WORKSPACE.PLAN]: () => require('../../../../pages/workspace/WorkspaceProfilePlanTypePage').default,
+ [SCREENS.WORKSPACE.ADDRESS]: () => require('../../../../pages/workspace/WorkspaceOverviewAddressPage').default,
+ [SCREENS.WORKSPACE.PLAN]: () => require('../../../../pages/workspace/WorkspaceOverviewPlanTypePage').default,
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default,
[SCREENS.WORKSPACE.CATEGORIES_IMPORT]: () => require('../../../../pages/workspace/categories/ImportCategoriesPage').default,
[SCREENS.WORKSPACE.CATEGORIES_IMPORTED]: () => require('../../../../pages/workspace/categories/ImportedCategoriesPage').default,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx
index c1b0fcc481f7..6ee46975618d 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx
@@ -15,7 +15,7 @@ type Screens = Partial Reac
const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default;
const CENTRAL_PANE_WORKSPACE_SCREENS = {
- [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceProfilePage').default,
+ [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceOverviewPage').default,
[SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default,
[SCREENS.WORKSPACE.INVOICES]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default,
[SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../pages/workspace/WorkspaceMembersPage').default,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index b6c57aa1eaf5..90d14b50890a 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -303,13 +303,13 @@ const config: LinkingOptions['config'] = {
path: ROUTES.SETTINGS_SUBSCRIPTION_REQUEST_EARLY_CANCELLATION,
},
[SCREENS.WORKSPACE.CURRENCY]: {
- path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route,
+ path: ROUTES.WORKSPACE_OVERVIEW_CURRENCY.route,
},
[SCREENS.WORKSPACE.ADDRESS]: {
- path: ROUTES.WORKSPACE_PROFILE_ADDRESS.route,
+ path: ROUTES.WORKSPACE_OVERVIEW_ADDRESS.route,
},
[SCREENS.WORKSPACE.PLAN]: {
- path: ROUTES.WORKSPACE_PROFILE_PLAN.route,
+ path: ROUTES.WORKSPACE_OVERVIEW_PLAN.route,
},
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT.route},
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS.route},
@@ -559,7 +559,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {path: ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.route},
[SCREENS.WORKSPACE.ACCOUNTING.MULTI_CONNECTION_SELECTOR]: {path: ROUTES.WORKSPACE_ACCOUNTING_MULTI_CONNECTION_SELECTOR.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
- path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
+ path: ROUTES.WORKSPACE_OVERVIEW_DESCRIPTION.route,
},
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: {
path: ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY.route,
@@ -568,7 +568,7 @@ const config: LinkingOptions['config'] = {
path: ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET.route,
},
[SCREENS.WORKSPACE.SHARE]: {
- path: ROUTES.WORKSPACE_PROFILE_SHARE.route,
+ path: ROUTES.WORKSPACE_OVERVIEW_SHARE.route,
},
[SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: {
path: ROUTES.WORKSPACE_INVOICES_COMPANY_NAME.route,
@@ -878,7 +878,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.KEYBOARD_SHORTCUTS]: {
path: ROUTES.KEYBOARD_SHORTCUTS,
},
- [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route,
+ [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_OVERVIEW_NAME.route,
[SCREENS.SETTINGS.SHARE_CODE]: {
path: ROUTES.SETTINGS_SHARE_CODE,
},
@@ -1585,7 +1585,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.INITIAL]: {
path: ROUTES.WORKSPACE_INITIAL.route,
},
- [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route,
+ [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_OVERVIEW.route,
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 98ea3606e6c1..fc73a6db8626 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -854,6 +854,7 @@ type SettingsNavigatorParamList = {
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: {
policyID: string;
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index e3d04034abeb..5745dd0b7dc8 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -26,7 +26,17 @@ import {
isPolicyAdmin,
} from '@libs/PolicyUtils';
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
-import {getReportTransactions, isOpenExpenseReport, isProcessingReport, isReportIDApproved, isSettled, isThread} from '@libs/ReportUtils';
+import {
+ getReportTransactions,
+ isCurrentUserSubmitter,
+ isOpenExpenseReport,
+ isProcessingReport,
+ isReportApproved,
+ isReportIDApproved,
+ isReportManuallyReimbursed,
+ isSettled,
+ isThread,
+} from '@libs/ReportUtils';
import type {IOURequestType} from '@userActions/IOU';
import CONST from '@src/CONST';
import type {IOUType} from '@src/CONST';
@@ -820,7 +830,7 @@ function shouldShowBrokenConnectionViolation(
// This should not be possible except in the case of incorrect type assertions. Generally TS should prevent this at compile time.
throw new Error('Invalid argument combination. If a transactionIDList is passed in, then an OnyxCollection of violations is expected');
}
- violations = transactionOrIDList.flatMap((id) => transactionViolations?.[id] ?? []);
+ violations = transactionOrIDList.flatMap((id) => transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? []);
} else {
if (!Array.isArray(transactionViolations)) {
// This should not be possible except in the case of incorrect type assertions. Generally TS should prevent this at compile time.
@@ -830,7 +840,24 @@ function shouldShowBrokenConnectionViolation(
}
const brokenConnectionViolations = violations.filter((violation) => isBrokenConnectionViolation(violation));
- return brokenConnectionViolations.length > 0 && (!isPolicyAdmin(policy) || isOpenExpenseReport(report) || (isProcessingReport(report) && isInstantSubmitEnabled(policy)));
+
+ if (brokenConnectionViolations.length > 0) {
+ if (!isPolicyAdmin(policy) || isCurrentUserSubmitter(report?.reportID)) {
+ return true;
+ }
+ return isOpenExpenseReport(report) || (isProcessingReport(report) && isInstantSubmitEnabled(policy));
+ }
+
+ return false;
+}
+
+function checkIfShouldShowMarkAsCashButton(hasRTERVPendingViolation: boolean, shouldDisplayBrokenConnectionViolation: boolean, report: OnyxEntry, policy: OnyxEntry) {
+ if (hasRTERVPendingViolation) {
+ return true;
+ }
+ return (
+ shouldDisplayBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(report?.reportID)) && !isReportApproved({report}) && !isReportManuallyReimbursed(report)
+ );
}
/**
@@ -1518,6 +1545,7 @@ export {
isPerDiemRequest,
isViolationDismissed,
isBrokenConnectionViolation,
+ checkIfShouldShowMarkAsCashButton,
shouldShowRTERViolationMessage,
};
diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts
index e955b69ec7ab..39ada8cf2a10 100644
--- a/src/libs/actions/Delegate.ts
+++ b/src/libs/actions/Delegate.ts
@@ -3,7 +3,7 @@ import Onyx from 'react-native-onyx';
import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import * as API from '@libs/API';
import type {AddDelegateParams, RemoveDelegateParams, UpdateDelegateRoleParams} from '@libs/API/parameters';
-import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
+import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import Log from '@libs/Log';
import * as NetworkStore from '@libs/Network/NetworkStore';
@@ -673,6 +673,10 @@ function restoreDelegateSession(authenticateResponse: Response) {
});
}
+function openSecuritySettingsPage() {
+ API.read(READ_COMMANDS.OPEN_SECURITY_SETTINGS_PAGE, null);
+}
+
export {
connect,
disconnect,
@@ -687,5 +691,6 @@ export {
clearDelegateRolePendingAction,
updateDelegateRole,
removeDelegate,
+ openSecuritySettingsPage,
KEYS_TO_PRESERVE_DELEGATE_ACCESS,
};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 4e373f614274..4ec5ac4c9dd8 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -122,6 +122,7 @@ import {
isInvoiceReport as isInvoiceReportReportUtils,
isInvoiceRoom,
isMoneyRequestReport as isMoneyRequestReportReportUtils,
+ isOneOnOneChat,
isOpenExpenseReport as isOpenExpenseReportReportUtils,
isOpenInvoiceReport as isOpenInvoiceReportReportUtils,
isOptimisticPersonalDetail,
@@ -344,7 +345,7 @@ type PerDiemExpenseTransactionParams = {
billable?: boolean;
};
-type RequestMoneyPolicyParams = {
+type BasePolicyParams = {
policy?: OnyxEntry;
policyTagList?: OnyxEntry;
policyCategories?: OnyxEntry;
@@ -359,7 +360,7 @@ type RequestMoneyParticipantParams = {
type PerDiemExpenseInformation = {
report: OnyxEntry;
participantParams: RequestMoneyParticipantParams;
- policyParams?: RequestMoneyPolicyParams;
+ policyParams?: BasePolicyParams;
transactionParams: PerDiemExpenseTransactionParams;
};
@@ -367,14 +368,14 @@ type PerDiemExpenseInformationParams = {
parentChatReport: OnyxEntry;
transactionParams: PerDiemExpenseTransactionParams;
participantParams: RequestMoneyParticipantParams;
- policyParams?: RequestMoneyPolicyParams;
+ policyParams?: BasePolicyParams;
moneyRequestReportID?: string;
};
type RequestMoneyInformation = {
report: OnyxEntry;
participantParams: RequestMoneyParticipantParams;
- policyParams?: RequestMoneyPolicyParams;
+ policyParams?: BasePolicyParams;
gpsPoints?: GPSPoint;
action?: IOUAction;
reimbursible?: boolean;
@@ -385,7 +386,7 @@ type MoneyRequestInformationParams = {
parentChatReport: OnyxEntry;
transactionParams: RequestMoneyTransactionParams;
participantParams: RequestMoneyParticipantParams;
- policyParams?: RequestMoneyPolicyParams;
+ policyParams?: BasePolicyParams;
moneyRequestReportID?: string;
existingTransactionID?: string;
existingTransaction?: OnyxEntry;
@@ -422,7 +423,7 @@ type BuildOnyxDataForMoneyRequestParams = {
shouldCreateNewMoneyRequestReport: boolean;
isOneOnOneSplit?: boolean;
existingTransactionThreadReportID?: string;
- policyParams?: RequestMoneyPolicyParams;
+ policyParams?: BasePolicyParams;
optimisticParams: MoneyRequestOptimisticParams;
};
@@ -449,7 +450,7 @@ type CreateDistanceRequestInformation = {
iouType?: ValueOf;
existingTransaction?: OnyxEntry;
transactionParams: DistanceRequestTransactionParams;
- policyParams?: RequestMoneyPolicyParams;
+ policyParams?: BasePolicyParams;
};
type TrackExpenseTransactionParams = {
@@ -477,10 +478,68 @@ type CreateTrackExpenseParams = {
isDraftPolicy: boolean;
action?: IOUAction;
participantParams: RequestMoneyParticipantParams;
- policyParams?: RequestMoneyPolicyParams;
+ policyParams?: BasePolicyParams;
transactionParams: TrackExpenseTransactionParams;
};
+type BuildOnyxDataForInvoiceParams = {
+ chat: {
+ report: OnyxEntry;
+ createdAction: OptimisticCreatedReportAction;
+ reportPreviewAction: ReportAction;
+ isNewReport: boolean;
+ };
+ iou: {
+ createdAction: OptimisticCreatedReportAction;
+ action: OptimisticIOUReportAction;
+ report: OnyxTypes.Report;
+ };
+ transactionParams: {
+ transaction: OnyxTypes.Transaction;
+ threadReport: OptimisticChatReport;
+ threadCreatedReportAction: OptimisticCreatedReportAction | null;
+ };
+ policyParams: BasePolicyParams;
+ optimisticData: {
+ recentlyUsedCurrencies?: string[];
+ policyRecentlyUsedCategories: string[];
+ policyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags;
+ personalDetailListAction: OnyxTypes.PersonalDetailsList;
+ };
+ companyName?: string;
+ companyWebsite?: string;
+};
+
+type GetTrackExpenseInformationTransactionParams = {
+ comment: string;
+ amount: number;
+ currency: string;
+ created: string;
+ merchant: string;
+ receipt: OnyxEntry;
+ category?: string;
+ tag?: string;
+ taxCode?: string;
+ taxAmount?: number;
+ billable?: boolean;
+ linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction;
+};
+
+type GetTrackExpenseInformationParticipantParams = {
+ payeeEmail?: string;
+ payeeAccountID?: number;
+ participant: Participant;
+};
+
+type GetTrackExpenseInformationParams = {
+ parentChatReport: OnyxEntry;
+ moneyRequestReportID?: string;
+ existingTransactionID?: string;
+ participantParams: GetTrackExpenseInformationParticipantParams;
+ policyParams: BasePolicyParams;
+ transactionParams: GetTrackExpenseInformationTransactionParams;
+};
+
let allPersonalDetails: OnyxTypes.PersonalDetailsList = {};
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
@@ -1462,36 +1521,18 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR
}
/** Builds the Onyx data for an invoice */
-function buildOnyxDataForInvoice(
- chatReport: OnyxEntry,
- iouReport: OnyxTypes.Report,
- transaction: OnyxTypes.Transaction,
- chatCreatedAction: OptimisticCreatedReportAction,
- iouCreatedAction: OptimisticCreatedReportAction,
- iouAction: OptimisticIOUReportAction,
- optimisticPersonalDetailListAction: OnyxTypes.PersonalDetailsList,
- reportPreviewAction: ReportAction,
- optimisticPolicyRecentlyUsedCategories: string[],
- optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags,
- isNewChatReport: boolean,
- transactionThreadReport: OptimisticChatReport,
- transactionThreadCreatedReportAction: OptimisticCreatedReportAction | null,
- policy?: OnyxEntry,
- policyTagList?: OnyxEntry,
- policyCategories?: OnyxEntry,
- optimisticRecentlyUsedCurrencies?: string[],
- companyName?: string,
- companyWebsite?: string,
-): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] {
- const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null]));
+function buildOnyxDataForInvoice(invoiceParams: BuildOnyxDataForInvoiceParams): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] {
+ const {chat, iou, transactionParams, policyParams, optimisticData: optimisticDataParams, companyName, companyWebsite} = invoiceParams;
+
+ const clearedPendingFields = Object.fromEntries(Object.keys(transactionParams.transaction.pendingFields ?? {}).map((key) => [key, null]));
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iou.report?.reportID}`,
value: {
- ...iouReport,
- lastMessageText: getReportActionText(iouAction),
- lastMessageHtml: getReportActionHtml(iouAction),
+ ...iou.report,
+ lastMessageText: getReportActionText(iou.action),
+ lastMessageHtml: getReportActionHtml(iou.action),
pendingFields: {
createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
@@ -1499,96 +1540,96 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
- value: transaction,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionParams.transaction.transactionID}`,
+ value: transactionParams.transaction,
},
- isNewChatReport
+ chat.isNewReport
? {
onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chat.report?.reportID}`,
value: {
- [chatCreatedAction.reportActionID]: chatCreatedAction,
- [reportPreviewAction.reportActionID]: reportPreviewAction,
+ [chat.createdAction.reportActionID]: chat.createdAction,
+ [chat.reportPreviewAction.reportActionID]: chat.reportPreviewAction,
},
}
: {
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chat.report?.reportID}`,
value: {
- [reportPreviewAction.reportActionID]: reportPreviewAction,
+ [chat.reportPreviewAction.reportActionID]: chat.reportPreviewAction,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iou.report?.reportID}`,
value: {
- [iouCreatedAction.reportActionID]: iouCreatedAction as OnyxTypes.ReportAction,
- [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction,
+ [iou.createdAction.reportActionID]: iou.createdAction as OnyxTypes.ReportAction,
+ [iou.action.reportActionID]: iou.action as OnyxTypes.ReportAction,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
- value: transactionThreadReport,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionParams.threadReport.reportID}`,
+ value: transactionParams.threadReport,
},
];
- if (transactionThreadCreatedReportAction?.reportActionID) {
+ if (transactionParams.threadCreatedReportAction?.reportActionID) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionParams.threadReport.reportID}`,
value: {
- [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction,
+ [transactionParams.threadCreatedReportAction.reportActionID]: transactionParams.threadCreatedReportAction,
},
});
}
const successData: OnyxUpdate[] = [];
- if (chatReport) {
+ if (chat.report) {
optimisticData.push({
// Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page
- onyxMethod: isNewChatReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`,
+ onyxMethod: chat.isNewReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chat.report.reportID}`,
value: {
- ...chatReport,
+ ...chat.report,
lastReadTime: DateUtils.getDBTime(),
- iouReportID: iouReport.reportID,
- ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}),
+ iouReportID: iou.report?.reportID,
+ ...(chat.isNewReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}),
},
});
}
- if (optimisticPolicyRecentlyUsedCategories.length) {
+ if (optimisticDataParams.policyRecentlyUsedCategories.length) {
optimisticData.push({
onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${iouReport.policyID}`,
- value: optimisticPolicyRecentlyUsedCategories,
+ key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${iou.report?.policyID}`,
+ value: optimisticDataParams.policyRecentlyUsedCategories,
});
}
- if (optimisticRecentlyUsedCurrencies?.length) {
+ if (optimisticDataParams.recentlyUsedCurrencies?.length) {
optimisticData.push({
onyxMethod: Onyx.METHOD.SET,
key: ONYXKEYS.RECENTLY_USED_CURRENCIES,
- value: optimisticRecentlyUsedCurrencies,
+ value: optimisticDataParams.recentlyUsedCurrencies,
});
}
- if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) {
+ if (!isEmptyObject(optimisticDataParams.policyRecentlyUsedTags)) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`,
- value: optimisticPolicyRecentlyUsedTags,
+ key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iou.report?.policyID}`,
+ value: optimisticDataParams.policyRecentlyUsedTags,
});
}
const redundantParticipants: Record = {};
- if (!isEmptyObject(optimisticPersonalDetailListAction)) {
+ if (!isEmptyObject(optimisticDataParams.personalDetailListAction)) {
const successPersonalDetailListAction: Record = {};
// BE will send different participants. We clear the optimistic ones to avoid duplicated entries
- Object.keys(optimisticPersonalDetailListAction).forEach((accountIDKey) => {
+ Object.keys(optimisticDataParams.personalDetailListAction).forEach((accountIDKey) => {
const accountID = Number(accountIDKey);
successPersonalDetailListAction[accountID] = null;
redundantParticipants[accountID] = null;
@@ -1597,7 +1638,7 @@ function buildOnyxDataForInvoice(
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- value: optimisticPersonalDetailListAction,
+ value: optimisticDataParams.personalDetailListAction,
});
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
@@ -1609,7 +1650,7 @@ function buildOnyxDataForInvoice(
successData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iou.report?.reportID}`,
value: {
participants: redundantParticipants,
pendingFields: null,
@@ -1618,14 +1659,14 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${iou.report?.reportID}`,
value: {
isOptimisticReport: false,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionParams.threadReport.reportID}`,
value: {
participants: redundantParticipants,
pendingFields: null,
@@ -1634,14 +1675,14 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionThreadReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionParams.threadReport.reportID}`,
value: {
isOptimisticReport: false,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionParams.transaction.transactionID}`,
value: {
pendingAction: null,
pendingFields: clearedPendingFields,
@@ -1649,30 +1690,30 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chat.report?.reportID}`,
value: {
- ...(isNewChatReport
+ ...(chat.isNewReport
? {
- [chatCreatedAction.reportActionID]: {
+ [chat.createdAction.reportActionID]: {
pendingAction: null,
errors: null,
},
}
: {}),
- [reportPreviewAction.reportActionID]: {
+ [chat.reportPreviewAction.reportActionID]: {
pendingAction: null,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iou.report?.reportID}`,
value: {
- [iouCreatedAction.reportActionID]: {
+ [iou.createdAction.reportActionID]: {
pendingAction: null,
errors: null,
},
- [iouAction.reportActionID]: {
+ [iou.action.reportActionID]: {
pendingAction: null,
errors: null,
},
@@ -1680,12 +1721,12 @@ function buildOnyxDataForInvoice(
},
);
- if (transactionThreadCreatedReportAction?.reportActionID) {
+ if (transactionParams.threadCreatedReportAction?.reportActionID) {
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionParams.threadReport.reportID}`,
value: {
- [transactionThreadCreatedReportAction.reportActionID]: {
+ [transactionParams.threadCreatedReportAction.reportActionID]: {
pendingAction: null,
errors: null,
},
@@ -1693,11 +1734,11 @@ function buildOnyxDataForInvoice(
});
}
- if (isNewChatReport) {
+ if (chat.isNewReport) {
successData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chat.report?.reportID}`,
value: {
participants: redundantParticipants,
pendingFields: null,
@@ -1706,7 +1747,7 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReport?.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${chat.report?.reportID}`,
value: {
isOptimisticReport: false,
},
@@ -1719,13 +1760,13 @@ function buildOnyxDataForInvoice(
const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chat.report?.reportID}`,
value: {
- iouReportID: chatReport?.iouReportID,
- lastReadTime: chatReport?.lastReadTime,
+ iouReportID: chat.report?.iouReportID,
+ lastReadTime: chat.report?.lastReadTime,
pendingFields: null,
- hasOutstandingChildRequest: chatReport?.hasOutstandingChildRequest,
- ...(isNewChatReport
+ hasOutstandingChildRequest: chat.report?.hasOutstandingChildRequest,
+ ...(chat.isNewReport
? {
errorFields: {
createChat: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'),
@@ -1736,7 +1777,7 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iou.report?.reportID}`,
value: {
pendingFields: null,
errorFields: {
@@ -1746,7 +1787,7 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionParams.threadReport.reportID}`,
value: {
errorFields: {
createChat: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'),
@@ -1755,7 +1796,7 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionParams.transaction.transactionID}`,
value: {
errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage'),
pendingFields: clearedPendingFields,
@@ -1763,26 +1804,31 @@ function buildOnyxDataForInvoice(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iou.report?.reportID}`,
value: {
- [iouCreatedAction.reportActionID]: {
- // Disabling this line since transaction.filename can be an empty string
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, false, errorKey),
+ [iou.createdAction.reportActionID]: {
+ // Disabling this line since transactionParams.transaction.filename can be an empty string
+ errors: getReceiptError(
+ transactionParams.transaction.receipt,
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ transactionParams.transaction?.filename || transactionParams.transaction.receipt?.filename,
+ false,
+ errorKey,
+ ),
},
- [iouAction.reportActionID]: {
+ [iou.action.reportActionID]: {
errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage'),
},
},
},
];
- if (transactionThreadCreatedReportAction?.reportActionID) {
+ if (transactionParams.threadCreatedReportAction?.reportActionID) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionParams.threadReport.reportID}`,
value: {
- [transactionThreadCreatedReportAction.reportActionID]: {
+ [transactionParams.threadCreatedReportAction.reportActionID]: {
errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage', errorKey),
},
},
@@ -1792,7 +1838,7 @@ function buildOnyxDataForInvoice(
if (companyName && companyWebsite) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyParams.policy?.id}`,
value: {
invoice: {
companyName,
@@ -1806,7 +1852,7 @@ function buildOnyxDataForInvoice(
});
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyParams.policy?.id}`,
value: {
invoice: {
pendingFields: {
@@ -1818,7 +1864,7 @@ function buildOnyxDataForInvoice(
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyParams.policy?.id}`,
value: {
invoice: {
companyName: null,
@@ -1833,17 +1879,17 @@ function buildOnyxDataForInvoice(
}
// We don't need to compute violations unless we're on a paid policy
- if (!policy || !isPaidGroupPolicy(policy)) {
+ if (!policyParams.policy || !isPaidGroupPolicy(policyParams.policy)) {
return [optimisticData, successData, failureData];
}
const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(
- transaction,
+ transactionParams.transaction,
[],
- policy,
- policyTagList ?? {},
- policyCategories ?? {},
- hasDependentTags(policy, policyTagList ?? {}),
+ policyParams.policy,
+ policyParams.policyTagList ?? {},
+ policyParams.policyCategories ?? {},
+ hasDependentTags(policyParams.policy, policyParams.policyTagList ?? {}),
true,
);
@@ -1851,7 +1897,7 @@ function buildOnyxDataForInvoice(
optimisticData.push(violationsOnyxData);
failureData.push({
onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionParams.transaction.transactionID}`,
value: [],
});
}
@@ -2579,27 +2625,24 @@ function getSendInvoiceInformation(
);
// STEP 6: Build Onyx Data
- const [optimisticData, successData, failureData] = buildOnyxDataForInvoice(
- chatReport,
- optimisticInvoiceReport,
- optimisticTransaction,
- optimisticCreatedActionForChat,
- optimisticCreatedActionForIOUReport,
- iouAction,
- optimisticPersonalDetailListAction,
- reportPreviewAction,
- optimisticPolicyRecentlyUsedCategories,
- optimisticPolicyRecentlyUsedTags,
- isNewChatReport,
- optimisticTransactionThread,
- optimisticCreatedActionForTransactionThread,
- policy,
- policyTagList,
- policyCategories,
- optimisticRecentlyUsedCurrencies,
+ const [optimisticData, successData, failureData] = buildOnyxDataForInvoice({
+ chat: {report: chatReport, createdAction: optimisticCreatedActionForChat, reportPreviewAction, isNewReport: isNewChatReport},
+ iou: {createdAction: optimisticCreatedActionForIOUReport, action: iouAction, report: optimisticInvoiceReport},
+ transactionParams: {
+ transaction: optimisticTransaction,
+ threadReport: optimisticTransactionThread,
+ threadCreatedReportAction: optimisticCreatedActionForTransactionThread,
+ },
+ policyParams: {policy, policyTagList, policyCategories},
+ optimisticData: {
+ personalDetailListAction: optimisticPersonalDetailListAction,
+ recentlyUsedCurrencies: optimisticRecentlyUsedCurrencies,
+ policyRecentlyUsedCategories: optimisticPolicyRecentlyUsedCategories,
+ policyRecentlyUsedTags: optimisticPolicyRecentlyUsedTags,
+ },
companyName,
companyWebsite,
- );
+ });
return {
createdIOUReportActionID: optimisticCreatedActionForIOUReport.reportActionID,
@@ -3078,29 +3121,12 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI
* Gathers all the data needed to make an expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then
* it creates optimistic versions of them and uses those instead
*/
-function getTrackExpenseInformation(
- parentChatReport: OnyxEntry,
- participant: Participant,
- comment: string,
- amount: number,
- currency: string,
- created: string,
- merchant: string,
- receipt: OnyxEntry,
- category: string | undefined,
- tag: string | undefined,
- taxCode: string | undefined,
- taxAmount: number | undefined,
- billable: boolean | undefined,
- policy: OnyxEntry | undefined,
- policyTagList: OnyxEntry | undefined,
- policyCategories: OnyxEntry | undefined,
- payeeEmail = currentUserEmail,
- payeeAccountID = userAccountID,
- moneyRequestReportID = '',
- linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction,
- existingTransactionID?: string,
-): TrackExpenseInformation | null {
+function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): TrackExpenseInformation | null {
+ const {parentChatReport, moneyRequestReportID = '', existingTransactionID, participantParams, policyParams, transactionParams} = params;
+ const {payeeAccountID = userAccountID, payeeEmail = currentUserEmail, participant} = participantParams;
+ const {policy, policyCategories, policyTagList} = policyParams;
+ const {comment, amount, currency, created, merchant, receipt, category, tag, taxCode, taxAmount, billable, linkedTrackedExpenseReportAction} = transactionParams;
+
const optimisticData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];
const failureData: OnyxUpdate[] = [];
@@ -4824,31 +4850,38 @@ function trackExpense(params: CreateTrackExpenseParams) {
actionableWhisperReportActionIDParam,
onyxData,
} =
- getTrackExpenseInformation(
- currentChatReport,
- participant,
- comment,
- amount,
- currency,
- created,
- merchant,
- trackedReceipt,
- category,
- tag,
- taxCode,
- taxAmount,
- billable,
- policy,
- policyTagList,
- policyCategories,
- payeeEmail,
- payeeAccountID,
+ getTrackExpenseInformation({
+ parentChatReport: currentChatReport,
moneyRequestReportID,
- linkedTrackedExpenseReportAction,
- isMovingTransactionFromTrackExpense && linkedTrackedExpenseReportAction && isMoneyRequestAction(linkedTrackedExpenseReportAction)
- ? getOriginalMessage(linkedTrackedExpenseReportAction)?.IOUTransactionID
- : undefined,
- ) ?? {};
+ existingTransactionID:
+ isMovingTransactionFromTrackExpense && linkedTrackedExpenseReportAction && isMoneyRequestAction(linkedTrackedExpenseReportAction)
+ ? getOriginalMessage(linkedTrackedExpenseReportAction)?.IOUTransactionID
+ : undefined,
+ participantParams: {
+ participant,
+ payeeAccountID,
+ payeeEmail,
+ },
+ transactionParams: {
+ comment,
+ amount,
+ currency,
+ created,
+ merchant,
+ receipt: trackedReceipt,
+ category,
+ tag,
+ taxCode,
+ taxAmount,
+ billable,
+ linkedTrackedExpenseReportAction,
+ },
+ policyParams: {
+ policy,
+ policyCategories,
+ policyTagList,
+ },
+ }) ?? {};
const activeReportID = isMoneyRequestReport ? report.reportID : chatReport?.reportID;
const recentServerValidatedWaypoints = getRecentWaypoints().filter((item) => !item.pendingAction);
@@ -5288,7 +5321,7 @@ function createSplitsAndOnyxData(
// or, if the split is being made from the workspace chat, then the oneOnOneChatReport is the same as the splitChatReport
// in this case existingSplitChatReport will belong to the policy expense chat and we won't be
// entering code that creates optimistic personal details
- if ((!hasMultipleParticipants && !existingSplitChatReportID) || isOwnPolicyExpenseChat) {
+ if ((!hasMultipleParticipants && !existingSplitChatReportID) || isOwnPolicyExpenseChat || isOneOnOneChat(splitChatReport)) {
oneOnOneChatReport = splitChatReport;
shouldCreateOptimisticPersonalDetails = !existingSplitChatReport && !personalDetailExists;
} else {
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index d6910ebcb26b..63448c89fc8a 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -3643,15 +3643,14 @@ function prepareOnboardingOnyxData(
userReportedIntegration?: OnboardingAccounting,
wasInvited?: boolean,
) {
- // If the user has the "combinedTrackSubmit" beta enabled we'll show different tasks for track and submit expense.
if (engagementChoice === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND) {
// eslint-disable-next-line no-param-reassign
- data = CONST.COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND];
+ data = CONST.CREATE_EXPENSE_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND];
}
if (engagementChoice === CONST.ONBOARDING_CHOICES.EMPLOYER || engagementChoice === CONST.ONBOARDING_CHOICES.SUBMIT) {
// eslint-disable-next-line no-param-reassign
- data = CONST.COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.SUBMIT];
+ data = CONST.CREATE_EXPENSE_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.SUBMIT];
}
// Guides are assigned and tasks are posted in the #admins room for the MANAGE_TEAM and TRACK_WORKSPACE onboarding actions, except for emails that have a '+'.
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 574225b20f38..d403cc1c3108 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -30,6 +30,7 @@ import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {isOffline} from '@libs/Network/NetworkStore';
import * as SequentialQueue from '@libs/Network/SequentialQueue';
+import NetworkConnection from '@libs/NetworkConnection';
import * as NumberUtils from '@libs/NumberUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import Pusher from '@libs/Pusher';
@@ -915,6 +916,7 @@ function subscribeToPusherPong() {
// When any PONG event comes in, reset this flag so that checkforLatePongReplies will resume looking for missed PONGs
pongHasBeenMissed = false;
+ NetworkConnection.setOfflineStatus(false, 'PONG event was recieved from the server so assuming that means the client is back online');
});
}
@@ -938,6 +940,9 @@ function pingPusher() {
const pingID = NumberUtils.rand64();
const pingTimestamp = Date.now();
+ // Reset this flag so that checkforLatePongReplies will resume looking for missed PONGs (in the case we are coming back online after being offline for a bit)
+ pongHasBeenMissed = false;
+
// In local development, there can end up being multiple intervals running because when JS code is replaced with hot module replacement, the old interval is not cleared
// and keeps running. This little bit of logic will attempt to keep multiple pings from happening.
if (pingTimestamp - lastPingSentTimestamp < PING_INTERVAL_LENGTH_IN_SECONDS * 1000) {
@@ -972,6 +977,7 @@ function checkforLatePongReplies() {
// When going offline, reset the pingpong state so that when the network reconnects, the client will start fresh
lastPingSentTimestamp = Date.now();
pongHasBeenMissed = true;
+ NetworkConnection.setOfflineStatus(true, 'PONG event was not received from the server in time so assuming that means the client is offline');
} else {
Log.info(`[Pusher PINGPONG] Last PONG event was ${timeSinceLastPongReceived} ms ago so not going offline`);
}
@@ -980,6 +986,11 @@ function checkforLatePongReplies() {
let pingPusherIntervalID: ReturnType;
let checkforLatePongRepliesIntervalID: ReturnType;
function initializePusherPingPong() {
+ // Skip doing the ping pong during tests so that the network isn't knocked offline, forcing tests to timeout
+ if (process.env.NODE_ENV === 'test') {
+ return;
+ }
+
// Only run the ping pong from the leader client
if (!ActiveClientManager.isClientTheLeader()) {
Log.info("[Pusher PINGPONG] Not starting PING PONG because this instance isn't the leader client");
diff --git a/src/pages/ReimbursementAccount/NonUSD/Country/subSteps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/Country/subSteps/Confirmation.tsx
index b75cc1045525..67430030a5f8 100644
--- a/src/pages/ReimbursementAccount/NonUSD/Country/subSteps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/NonUSD/Country/subSteps/Confirmation.tsx
@@ -46,7 +46,7 @@ function Confirmation({onNext}: SubStepProps) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID));
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID));
};
const handleSelectingCountry = (country: string) => {
diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx
index a4013e0795a5..6d917e052189 100644
--- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx
+++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx
@@ -11,12 +11,14 @@ import CardListItem from '@components/SelectionList/Search/CardListItem';
import type {AdditionalCardProps} from '@components/SelectionList/Search/CardListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
+import useThemeIllustrations from '@hooks/useThemeIllustrations';
import useThemeStyles from '@hooks/useThemeStyles';
import {openSearchFiltersCardPage, updateAdvancedFilters} from '@libs/actions/Search';
import {getBankName, getCardFeedIcon, isCard, isCardClosed, isCardHiddenFromSearch} from '@libs/CardUtils';
import {getDescriptionForPolicyDomainCard, getPolicy} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
+import type IllustrationsType from '@styles/theme/illustrations/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -43,10 +45,10 @@ function getRepeatingBanks(workspaceCardFeedsKeys: string[], domainFeedsData: Re
return Object.keys(bankFrequency).filter((bank) => bankFrequency[bank] > 1);
}
-function createCardFilterItem(card: Card, personalDetailsList: PersonalDetailsList, selectedCards: string[]): CardFilterItem {
+function createCardFilterItem(card: Card, personalDetailsList: PersonalDetailsList, selectedCards: string[], illustrations: IllustrationsType): CardFilterItem {
const personalDetails = personalDetailsList[card?.accountID ?? CONST.DEFAULT_NUMBER_ID];
const isSelected = selectedCards.includes(card.cardID.toString());
- const icon = getCardFeedIcon(card?.bank as CompanyCardFeed);
+ const icon = getCardFeedIcon(card?.bank as CompanyCardFeed, illustrations);
const cardName = card?.nameValuePairs?.cardTitle;
const text = personalDetails?.displayName ?? cardName;
@@ -71,13 +73,14 @@ function buildCardsData(
userCardList: CardList,
personalDetailsList: PersonalDetailsList,
selectedCards: string[],
+ illustrations: IllustrationsType,
isClosedCards = false,
): ItemsGroupedBySelection {
// Filter condition to build different cards data for closed cards and individual cards based on the isClosedCards flag, we don't want to show closed cards in the individual cards section
const filterCondition = (card: Card) => (isClosedCards ? isCardClosed(card) : !isCardHiddenFromSearch(card) && !isCardClosed(card));
const userAssignedCards: CardFilterItem[] = Object.values(userCardList ?? {})
.filter((card) => filterCondition(card))
- .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards));
+ .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards, illustrations));
// When user is admin of a workspace he sees all the cards of workspace under cards_ Onyx key
const allWorkspaceCards: CardFilterItem[] = Object.values(workspaceCardFeeds)
@@ -85,7 +88,7 @@ function buildCardsData(
.flatMap((cardFeed) => {
return Object.values(cardFeed as Record)
.filter((card) => card && isCard(card) && !userCardList?.[card.cardID] && filterCondition(card))
- .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards));
+ .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards, illustrations));
});
const allCardItems = [...userAssignedCards, ...allWorkspaceCards];
@@ -108,6 +111,7 @@ function createCardFeedItem({
correspondingCardIDs,
selectedCards,
translate,
+ illustrations,
}: {
bank: string;
cardFeedLabel: string | undefined;
@@ -115,12 +119,16 @@ function createCardFeedItem({
correspondingCardIDs: string[];
selectedCards: string[];
translate: LocaleContextProps['translate'];
+ illustrations: IllustrationsType;
}): CardFilterItem {
const cardFeedBankName = bank === CONST.EXPENSIFY_CARD.BANK ? translate('search.filters.card.expensify') : getBankName(bank as CompanyCardFeed);
- const text = translate('search.filters.card.cardFeedName', {cardFeedBankName, cardFeedLabel});
+ const text =
+ cardFeedBankName === CONST.COMPANY_CARDS.CARD_TYPE.CSV
+ ? translate('search.filters.card.cardFeedNameCSV', {cardFeedLabel})
+ : translate('search.filters.card.cardFeedName', {cardFeedBankName, cardFeedLabel});
const isSelected = correspondingCardIDs.every((card) => selectedCards.includes(card));
- const icon = getCardFeedIcon(bank as CompanyCardFeed);
+ const icon = getCardFeedIcon(bank as CompanyCardFeed, illustrations);
return {
text,
keyForList,
@@ -139,6 +147,7 @@ function buildCardFeedsData(
domainFeedsData: Record,
selectedCards: string[],
translate: LocaleContextProps['translate'],
+ illustrations: IllustrationsType,
): ItemsGroupedBySelection {
const repeatingBanks = getRepeatingBanks(Object.keys(workspaceCardFeeds), domainFeedsData);
const selectedFeeds: CardFilterItem[] = [];
@@ -155,6 +164,7 @@ function buildCardFeedsData(
translate,
keyForList: `${domainName}-${bank}`,
selectedCards,
+ illustrations,
});
if (feedItem.isSelected) {
selectedFeeds.push(feedItem);
@@ -187,6 +197,7 @@ function buildCardFeedsData(
translate,
keyForList: cardFeedKey,
selectedCards,
+ illustrations,
});
if (feedItem.isSelected) {
selectedFeeds.push(feedItem);
@@ -201,6 +212,7 @@ function buildCardFeedsData(
function SearchFiltersCardPage() {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const illustrations = useThemeIllustrations();
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST);
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST);
@@ -215,13 +227,13 @@ function SearchFiltersCardPage() {
}, []);
const individualCardsSectionData = useMemo(
- () => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, false),
- [workspaceCardFeeds, userCardList, personalDetails, selectedCards],
+ () => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, illustrations, false),
+ [workspaceCardFeeds, userCardList, personalDetails, selectedCards, illustrations],
);
const closedCardsSectionData = useMemo(
- () => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, true),
- [workspaceCardFeeds, userCardList, personalDetails, selectedCards],
+ () => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, illustrations, true),
+ [workspaceCardFeeds, userCardList, personalDetails, selectedCards, illustrations],
);
const domainFeedsData = useMemo(
@@ -241,8 +253,8 @@ function SearchFiltersCardPage() {
);
const cardFeedsSectionData = useMemo(
- () => buildCardFeedsData(workspaceCardFeeds ?? {}, domainFeedsData, selectedCards, translate),
- [domainFeedsData, workspaceCardFeeds, selectedCards, translate],
+ () => buildCardFeedsData(workspaceCardFeeds ?? {}, domainFeedsData, selectedCards, translate, illustrations),
+ [domainFeedsData, workspaceCardFeeds, selectedCards, translate, illustrations],
);
const shouldShowSearchInput =
diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx
index e4b8ddc8b45c..f01be69fd273 100644
--- a/src/pages/Search/SearchTypeMenu.tsx
+++ b/src/pages/Search/SearchTypeMenu.tsx
@@ -177,6 +177,7 @@ function SearchTypeMenu({queryJSON, shouldGroupByReports}: SearchTypeMenuProps)
{typeMenuItems.map((item, index) => {
diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx
index 73b1a4af8ec1..763ee3b10039 100644
--- a/src/pages/home/HeaderView.tsx
+++ b/src/pages/home/HeaderView.tsx
@@ -1,5 +1,5 @@
import {useRoute} from '@react-navigation/native';
-import React, {memo, useEffect, useMemo} from 'react';
+import React, {memo, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
@@ -111,6 +111,7 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked,
const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL);
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`);
+ const [isDismissedDiscountBanner, setIsDismissedDiscountBanner] = useState(false);
const {translate} = useLocalize();
const theme = useTheme();
@@ -203,21 +204,23 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked,
const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED);
const isChatUsedForOnboarding = isChatUsedForOnboardingReportUtils(report, onboardingPurposeSelected);
+ const shouldShowEarlyDiscountBanner = shouldShowDiscount && isChatUsedForOnboarding;
+ const shouldShowGuideBookingButtonInEarlyDiscountBanner = shouldShowGuideBooking && shouldShowEarlyDiscountBanner && !isDismissedDiscountBanner;
const guideBookingButton = (
)}
-
- {shouldShowGuideBooking && !shouldUseNarrowLayout && guideBookingButton}
+
+ {!shouldShowGuideBookingButtonInEarlyDiscountBanner && shouldShowGuideBooking && !shouldUseNarrowLayout && guideBookingButton}
{!shouldUseNarrowLayout && !shouldShowDiscount && isChatUsedForOnboarding && (
{!isParentReportLoading && !isLoading && canJoin && shouldUseNarrowLayout && {joinButton}}
-
- {!isLoading && shouldShowGuideBooking && shouldUseNarrowLayout && {guideBookingButton}}
+
+ {!shouldShowGuideBookingButtonInEarlyDiscountBanner && !isLoading && shouldShowGuideBooking && shouldUseNarrowLayout && (
+ {guideBookingButton}
+ )}
{!isLoading && !shouldShowDiscount && isChatUsedForOnboarding && shouldUseNarrowLayout && (
- {shouldShowDiscount && isChatUsedForOnboarding && }
+ {shouldShowEarlyDiscountBanner && (
+ setIsDismissedDiscountBanner(true)}
+ />
+ )}
>
);
}
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index fa8ffaaf7abe..f29a4d85fc56 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -35,29 +35,24 @@ import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils';
-import {getDisplayNameOrDefault, isPersonalDetailsEmpty} from '@libs/PersonalDetailsUtils';
+import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import {
getCombinedReportActions,
getOneTransactionThreadReportID,
- isActionOfType,
isCreatedAction,
- isDeletedAction,
isDeletedParentAction,
isMoneyRequestAction,
isWhisperAction,
shouldReportActionBeVisible,
} from '@libs/ReportActionsUtils';
import {
- canAccessReport,
canEditReportAction,
canUserPerformWriteAction,
findLastAccessedReport,
getParticipantsAccountIDsForDisplay,
- getReportIDFromLink,
getReportOfflinePendingActionAndErrors,
isChatThread,
isConciergeChatReport,
- isExpenseReport,
isGroupChat,
isHiddenForCurrentUser,
isInvoiceReport,
@@ -70,7 +65,6 @@ import {
isTrackExpenseReport,
isValidReportIDFromPath,
} from '@libs/ReportUtils';
-import shouldFetchReport from '@libs/shouldFetchReport';
import {isNumeric} from '@libs/ValidationUtils';
import type {ReportsSplitNavigatorParamList} from '@navigation/types';
import {setShouldShowComposeInput} from '@userActions/Composer';
@@ -89,9 +83,7 @@ import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import HeaderView from './HeaderView';
-import ReportActionsListItemRenderer from './report/ReportActionsListItemRenderer';
import ReportActionsView from './report/ReportActionsView';
import ReportFooter from './report/ReportFooter';
import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext';
@@ -154,23 +146,17 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID);
const [accountManagerReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(accountManagerReportID)}`);
const [userLeavingStatus] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportIDFromRoute}`, {initialValue: false});
- const [reportOnyx, reportResult] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true});
+ const [reportOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true});
const [reportNameValuePairsOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportIDFromRoute}`, {allowStaleData: true});
const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`, {initialValue: defaultReportMetadata});
- const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED, {initialValue: false});
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {allowStaleData: true, initialValue: {}});
- const [betas] = useOnyx(ONYXKEYS.BETAS);
const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(reportOnyx?.parentReportID)}`, {
canEvict: false,
selector: (parentReportActions) => getParentReportAction(parentReportActions, reportOnyx?.parentReportActionID),
});
- const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
- const wasLoadingApp = usePrevious(isLoadingApp);
- const finishedLoadingApp = wasLoadingApp && !isLoadingApp;
const deletedParentAction = isDeletedParentAction(parentReportAction);
const prevDeletedParentAction = usePrevious(deletedParentAction);
- const isLoadingReportOnyx = isLoadingOnyxValue(reportResult);
const permissions = useDeepCompareRef(reportOnyx?.permissions);
useEffect(() => {
@@ -194,7 +180,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`);
navigation.setParams({reportID: lastAccessedReportID});
- }, [activeWorkspaceID, canUseDefaultRooms, navigation, route, finishedLoadingApp]);
+ }, [activeWorkspaceID, canUseDefaultRooms, navigation, route]);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const chatWithAccountManagerText = useMemo(() => {
@@ -272,6 +258,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
const [isLinkingToMessage, setIsLinkingToMessage] = useState(!!reportActionIDFromRoute);
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.accountID});
+ const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const {reportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute);
const [isBannerVisible, setIsBannerVisible] = useState(true);
@@ -291,7 +278,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
[reportActions, reportActionIDFromRoute],
);
- const isPendingActionExist = !!reportActions.at(0)?.pendingAction;
const doesCreatedActionExists = useCallback(() => !!reportActions?.findLast((action) => isCreatedAction(action)), [reportActions]);
const isLinkedMessageAvailable = indexOfLinkedMessage > -1;
@@ -300,10 +286,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
// OpenReport will be called each time the user scrolls up the report a bit, clicks on report preview, and then goes back."
const isLinkedMessagePageReady = isLinkedMessageAvailable && (reportActions.length - indexOfLinkedMessage >= CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || doesCreatedActionExists());
- // If there's a non-404 error for the report we should show it instead of blocking the screen
- const hasHelpfulErrors = Object.keys(report?.errorFields ?? {}).some((key) => key !== 'notFound');
- const shouldHideReport = !hasHelpfulErrors && !canAccessReport(report, policies, betas);
-
const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], isOffline);
const [transactionThreadReportActions = {}] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`);
const combinedReportActions = getCombinedReportActions(reportActions, transactionThreadReportID ?? null, Object.values(transactionThreadReportActions));
@@ -321,12 +303,12 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
}, [prevIsFocused, isFocused]);
useEffect(() => {
- if (!report?.reportID || shouldHideReport) {
+ if (!report?.reportID) {
wasReportAccessibleRef.current = false;
return;
}
wasReportAccessibleRef.current = true;
- }, [shouldHideReport, report]);
+ }, [report]);
const onBackButtonPress = useCallback(() => {
if (isInNarrowPaneModal) {
@@ -385,10 +367,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
return reportIDFromRoute !== '' && !!report?.reportID && !isTransitioning;
}, [report, reportIDFromRoute]);
- const isInitialPageReady = isOffline
- ? reportActions.length > 0
- : reportActions.length >= CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || isPendingActionExist || (doesCreatedActionExists() && reportActions.length > 0);
-
const isLinkedActionDeleted = useMemo(
() => !!linkedAction && !shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, canUserPerformWriteAction(report)),
[linkedAction, report],
@@ -419,22 +397,10 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
});
}, [isFocused, deleteTransactionNavigateBackUrl]);
- const isLoading = isLoadingApp ?? (!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || isPersonalDetailsEmpty());
-
- const shouldShowSkeleton =
- (isLinkingToMessage && !isLinkedMessagePageReady) ||
- (!isLinkingToMessage && !isInitialPageReady) ||
- isEmptyObject(reportOnyx) ||
- isLoadingReportOnyx ||
- !isCurrentReportLoadedFromOnyx ||
- (!!deleteTransactionNavigateBackUrl && getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID) ||
- (!reportMetadata.isOptimisticReport && isLoading);
-
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundLinkedAction =
(!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted && isNavigatingToDeletedAction) ||
- (shouldShowSkeleton &&
- !reportMetadata.isLoadingInitialReportActions &&
+ (!reportMetadata?.isLoadingInitialReportActions &&
!!reportActionIDFromRoute &&
!!sortedAllReportActions &&
sortedAllReportActions?.length > 0 &&
@@ -450,41 +416,20 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
return true;
}
- // Wait until we're sure the app is done loading (needs to be a strict equality check since it's undefined initially)
if (isLoadingApp !== false) {
return false;
}
- // If we just finished loading the app, we still need to try fetching the report. Wait until that's done before
- // showing the Not Found page
- if (finishedLoadingApp) {
- return false;
- }
-
// eslint-disable-next-line react-compiler/react-compiler
if (!wasReportAccessibleRef.current && !firstRenderRef.current && !reportID && !isOptimisticDelete && !reportMetadata?.isLoadingInitialReportActions && !userLeavingStatus) {
// eslint-disable-next-line react-compiler/react-compiler
return true;
}
- if (shouldHideReport) {
- return true;
- }
return !!currentReportIDFormRoute && !isValidReportIDFromPath(currentReportIDFormRoute);
},
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- [
- firstRender,
- shouldShowNotFoundLinkedAction,
- isLoadingApp,
- finishedLoadingApp,
- reportID,
- isOptimisticDelete,
- reportMetadata?.isLoadingInitialReportActions,
- userLeavingStatus,
- shouldHideReport,
- currentReportIDFormRoute,
- ],
+ [firstRender, shouldShowNotFoundLinkedAction, reportID, isOptimisticDelete, reportMetadata?.isLoadingInitialReportActions, userLeavingStatus, currentReportIDFormRoute],
);
const fetchReport = useCallback(() => {
@@ -511,38 +456,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
};
}, [reportID]);
- const fetchReportIfNeeded = useCallback(() => {
- // Report ID will be empty when the reports collection is empty.
- // This could happen when we are loading the collection for the first time after logging in.
- if (!isValidReportIDFromPath(reportIDFromRoute)) {
- return;
- }
-
- /**
- * Since OpenReport is a write, the response from OpenReport will get dropped while the app is
- * still loading. This usually happens when signing in and deeplinking to a report. Instead,
- * we'll fetch the report after the app finishes loading.
- *
- * This needs to be a strict equality check since isLoadingApp is initially undefined until the
- * value is loaded from Onyx
- */
- if (isLoadingApp !== false) {
- return;
- }
-
- if (!shouldFetchReport(report, reportMetadata.isOptimisticReport)) {
- return;
- }
- // When creating an optimistic report that already exists, we need to skip openReport
- // when replacing the optimistic report with the real one received from the server.
- if (isSkippingOpenReport.current) {
- isSkippingOpenReport.current = false;
- return;
- }
-
- fetchReport();
- }, [reportIDFromRoute, isLoadingApp, report, fetchReport, reportMetadata.isOptimisticReport]);
-
const dismissBanner = useCallback(() => {
setIsBannerVisible(false);
}, []);
@@ -582,25 +495,12 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
}, []);
useEffect(() => {
- // Call OpenReport only if we are not linking to a message or the report is not available yet
- if (isLoadingReportOnyx || reportActionIDFromRoute) {
- return;
- }
- fetchReportIfNeeded();
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [isLoadingReportOnyx]);
-
- useEffect(() => {
- if (isLoadingReportOnyx || !reportActionIDFromRoute || isLinkedMessagePageReady) {
- return;
- }
-
// This function is triggered when a user clicks on a link to navigate to a report.
// For each link click, we retrieve the report data again, even though it may already be cached.
// There should be only one openReport execution per page start or navigating
fetchReport();
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [route, isLinkedMessagePageReady, isLoadingReportOnyx, reportActionIDFromRoute]);
+ }, [route, isLinkedMessagePageReady, reportActionIDFromRoute]);
// If a user has chosen to leave a thread, and then returns to it (e.g. with the back button), we need to call `openReport` again in order to allow the user to rejoin and to receive real-time updates
useEffect(() => {
@@ -670,14 +570,11 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
return;
}
- fetchReportIfNeeded();
setShouldShowComposeInput(true);
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [
route,
report,
- // errors,
- fetchReportIfNeeded,
prevReport?.reportID,
prevUserLeavingStatus,
userLeavingStatus,
@@ -729,18 +626,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
});
}, [reportMetadata?.isLoadingInitialReportActions]);
- // If we deeplinked to the report after signing in, we need to fetch the report after the app is done loading
- useEffect(() => {
- if (!finishedLoadingApp) {
- return;
- }
-
- fetchReportIfNeeded();
-
- // This should only run once when the app is done loading
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [finishedLoadingApp]);
-
const navigateToEndOfReport = useCallback(() => {
Navigation.setParams({reportActionID: ''});
fetchReport();
@@ -783,18 +668,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
// After creating the task report then navigating to task detail we don't have any report actions and the last read time is empty so We need to update the initial last read time when opening the task report detail.
readNewestAction(report?.reportID);
}, [report]);
- const mostRecentReportAction = reportActions.at(0);
- const isMostRecentReportIOU = mostRecentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU;
- const isSingleIOUReportAction = reportActions.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU).length === 1;
- const isSingleExpenseReport = isExpenseReport(report) && isMostRecentReportIOU && isSingleIOUReportAction;
- const isSingleInvoiceReport = isInvoiceReport(report) && isMostRecentReportIOU && isSingleIOUReportAction;
- const shouldShowMostRecentReportAction =
- !!mostRecentReportAction &&
- shouldReportActionBeVisible(mostRecentReportAction, mostRecentReportAction.reportActionID, canUserPerformWriteAction(report)) &&
- !isSingleExpenseReport &&
- !isSingleInvoiceReport &&
- !isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.CREATED) &&
- !isDeletedAction(mostRecentReportAction);
const lastRoute = usePrevious(route);
@@ -854,46 +727,18 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]}
testID="report-actions-view-wrapper"
>
- {!shouldShowSkeleton && !!report && (
+ {!!report && !isLoadingApp ? (
- )}
-
- {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded.
- If we prevent rendering the report while they are loading then
- we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */}
- {shouldShowSkeleton && (
- <>
-
- {shouldShowMostRecentReportAction && (
-
- )}
- >
+ ) : (
+
)}
{isCurrentReportLoadedFromOnyx ? (
diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx
index 47c7c6e255ab..82f165358cca 100644
--- a/src/pages/home/report/ReportActionsList.tsx
+++ b/src/pages/home/report/ReportActionsList.tsx
@@ -3,7 +3,7 @@ import {useIsFocused, useRoute} from '@react-navigation/native';
// eslint-disable-next-line lodash/import-scope
import type {DebouncedFunc} from 'lodash';
import React, {memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
-import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native';
+import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
import {DeviceEventEmitter, InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
@@ -89,21 +89,6 @@ type ReportActionsListProps = {
/** The ID of the most recent IOU report action connected with the shown report */
mostRecentIOUReportActionID?: string | null;
- /** The report metadata loading states */
- isLoadingInitialReportActions?: boolean;
-
- /** Are we loading more report actions? */
- isLoadingOlderReportActions?: boolean;
-
- /** Was there an error when loading older report actions? */
- hasLoadingOlderReportActionsError?: boolean;
-
- /** Are we loading newer report actions? */
- isLoadingNewerReportActions?: boolean;
-
- /** Was there an error when loading newer report actions? */
- hasLoadingNewerReportActionsError?: boolean;
-
/** Callback executed on list layout */
onLayout: (event: LayoutChangeEvent) => void;
@@ -158,11 +143,6 @@ function ReportActionsList({
report,
transactionThreadReport,
parentReportAction,
- isLoadingInitialReportActions = false,
- isLoadingOlderReportActions = false,
- hasLoadingOlderReportActionsError = false,
- isLoadingNewerReportActions = false,
- hasLoadingNewerReportActionsError = false,
sortedReportActions,
sortedVisibleReportActions,
onScroll,
@@ -207,7 +187,6 @@ function ReportActionsList({
const scrollingVerticalOffset = useRef(0);
const readActionSkipped = useRef(false);
const hasHeaderRendered = useRef(false);
- const hasFooterRendered = useRef(false);
const linkedReportActionID = route?.params?.reportActionID;
const lastAction = sortedVisibleReportActions.at(0);
@@ -482,6 +461,9 @@ function ReportActionsList({
return;
}
if (!hasNewestReportActionRef.current) {
+ if (Navigation.getReportRHPActiveRoute()) {
+ return;
+ }
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID));
return;
}
@@ -691,40 +673,6 @@ function ReportActionsList({
// eslint-disable-next-line react-compiler/react-compiler
const canShowHeader = isOffline || hasHeaderRendered.current;
- const contentContainerStyle: StyleProp = useMemo(
- () => [styles.chatContentScrollView, isLoadingNewerReportActions && canShowHeader ? styles.chatContentScrollViewWithHeaderLoader : {}],
- [isLoadingNewerReportActions, styles.chatContentScrollView, styles.chatContentScrollViewWithHeaderLoader, canShowHeader],
- );
-
- const lastReportAction: OnyxTypes.ReportAction | undefined = useMemo(() => sortedReportActions.at(-1) ?? undefined, [sortedReportActions]);
-
- const retryLoadOlderChatsError = useCallback(() => {
- loadOlderChats(true);
- }, [loadOlderChats]);
-
- // eslint-disable-next-line react-compiler/react-compiler
- const listFooterComponent = useMemo(() => {
- // Skip this hook on the first render (when online), as we are not sure if more actions are going to be loaded,
- // Therefore showing the skeleton on footer might be misleading.
- // When offline, there should be no second render, so we should show the skeleton if the corresponding loading prop is present.
- // In case of an error we want to display the footer no matter what.
- if (!isOffline && !hasFooterRendered.current && !hasLoadingOlderReportActionsError) {
- hasFooterRendered.current = true;
- return null;
- }
-
- return (
-
- );
- }, [isLoadingInitialReportActions, isLoadingOlderReportActions, lastReportAction?.actionName, isOffline, hasLoadingOlderReportActionsError, retryLoadOlderChatsError]);
-
const onLayoutInner = useCallback(
(event: LayoutChangeEvent) => {
onLayout(event);
@@ -745,7 +693,7 @@ function ReportActionsList({
const listHeaderComponent = useMemo(() => {
// In case of an error we want to display the header no matter what.
- if (!canShowHeader && !hasLoadingNewerReportActionsError) {
+ if (!canShowHeader) {
// eslint-disable-next-line react-compiler/react-compiler
hasHeaderRendered.current = true;
return null;
@@ -754,12 +702,10 @@ function ReportActionsList({
return (
);
- }, [isLoadingNewerReportActions, canShowHeader, hasLoadingNewerReportActionsError, retryLoadNewerChatsError]);
+ }, [canShowHeader, retryLoadNewerChatsError]);
const onStartReached = useCallback(() => {
if (!isSearchTopmostFullScreenRoute()) {
@@ -798,14 +744,13 @@ function ReportActionsList({
style={styles.overscrollBehaviorContain}
data={sortedVisibleReportActions}
renderItem={renderItem}
- contentContainerStyle={contentContainerStyle}
+ contentContainerStyle={styles.chatContentScrollView}
keyExtractor={keyExtractor}
initialNumToRender={initialNumToRender}
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
onStartReached={onStartReached}
onStartReachedThreshold={0.75}
- ListFooterComponent={listFooterComponent}
ListHeaderComponent={listHeaderComponent}
keyboardShouldPersistTaps="handled"
onLayout={onLayoutInner}
diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx
index e5e715c16d86..88b4cae332ed 100755
--- a/src/pages/home/report/ReportActionsView.tsx
+++ b/src/pages/home/report/ReportActionsView.tsx
@@ -1,14 +1,19 @@
import {useIsFocused, useRoute} from '@react-navigation/native';
-import lodashIsEqual from 'lodash/isEqual';
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
+import EmptyStateComponent from '@components/EmptyStateComponent';
+import * as Illustrations from '@components/Icon/Illustrations';
+import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
+import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
+import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import {getNewerActions, getOlderActions, openReport, updateLoadingInitialReportAction} from '@libs/actions/Report';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getNewerActions, getOlderActions, updateLoadingInitialReportAction} from '@libs/actions/Report';
import Timing from '@libs/actions/Timing';
import DateUtils from '@libs/DateUtils';
import getIsReportFullyVisible from '@libs/getIsReportFullyVisible';
@@ -27,9 +32,7 @@ import {
isMoneyRequestAction,
shouldReportActionBeVisible,
} from '@libs/ReportActionsUtils';
-import {buildOptimisticCreatedReportAction, buildOptimisticIOUReportAction, canUserPerformWriteAction, isMoneyRequestReport, isUserCreatedPolicyRoom} from '@libs/ReportUtils';
-import {didUserLogInDuringSession} from '@libs/SessionUtils';
-import shouldFetchReport from '@libs/shouldFetchReport';
+import {buildOptimisticCreatedReportAction, buildOptimisticIOUReportAction, canUserPerformWriteAction, isMoneyRequestReport} from '@libs/ReportUtils';
import {ReactionListContext} from '@pages/home/ReportScreenContext';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -53,18 +56,6 @@ type ReportActionsViewProps = {
/** The report metadata loading states */
isLoadingInitialReportActions?: boolean;
- /** The report actions are loading more data */
- isLoadingOlderReportActions?: boolean;
-
- /** There an error when loading older report actions */
- hasLoadingOlderReportActionsError?: boolean;
-
- /** The report actions are loading newer data */
- isLoadingNewerReportActions?: boolean;
-
- /** There an error when loading newer report actions */
- hasLoadingNewerReportActionsError?: boolean;
-
/** The reportID of the transaction thread report associated with this current report, if any */
// eslint-disable-next-line react/no-unused-prop-types
transactionThreadReportID?: string | null;
@@ -81,25 +72,21 @@ let listOldID = Math.round(Math.random() * 100);
function ReportActionsView({
report,
parentReportAction,
- reportActions: allReportActions = [],
- isLoadingInitialReportActions = false,
- isLoadingOlderReportActions = false,
- hasLoadingOlderReportActionsError = false,
- isLoadingNewerReportActions = false,
- hasLoadingNewerReportActionsError = false,
+ reportActions: allReportActions,
+ isLoadingInitialReportActions,
transactionThreadReportID,
hasNewerActions,
hasOlderActions,
}: ReportActionsViewProps) {
useCopySelectionHelper();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
const reactionListRef = useContext(ReactionListContext);
const route = useRoute>();
- const [session] = useOnyx(ONYXKEYS.SESSION);
const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`, {
selector: (reportActions: OnyxEntry) => getSortedReportActionsForDisplay(reportActions, canUserPerformWriteAction(report), true),
});
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`);
- const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`);
const prevTransactionThreadReport = usePrevious(transactionThreadReport);
const reportActionID = route?.params?.reportActionID;
const prevReportActionID = usePrevious(reportActionID);
@@ -108,22 +95,13 @@ function ReportActionsView({
const didLoadNewerChats = useRef(false);
const {isOffline} = useNetwork();
- const network = useNetwork();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const contentListHeight = useRef(0);
const isFocused = useIsFocused();
- const prevAuthTokenType = usePrevious(session?.authTokenType);
const [isNavigatingToLinkedMessage, setNavigatingToLinkedMessage] = useState(!!reportActionID);
const prevShouldUseNarrowLayoutRef = useRef(shouldUseNarrowLayout);
const reportID = report.reportID;
const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]);
- const openReportIfNecessary = () => {
- if (!shouldFetchReport(report, reportMetadata?.isOptimisticReport)) {
- return;
- }
-
- openReport(reportID, reportActionID);
- };
useEffect(() => {
// When we linked to message - we do not need to wait for initial actions - they already exists
@@ -153,7 +131,7 @@ function ReportActionsView({
// and we also generate an expense action if the number of expenses in allReportActions is less than the total number of expenses
// to display at least one expense action to match the total data.
const reportActionsToDisplay = useMemo(() => {
- if (!isMoneyRequestReport(report) || !allReportActions.length) {
+ if (!isMoneyRequestReport(report) || !allReportActions?.length) {
return allReportActions;
}
@@ -210,7 +188,7 @@ function ReportActionsView({
// Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one)
// so that we display transaction-level and report-level report actions in order in the one-transaction view
const reportActions = useMemo(
- () => getCombinedReportActions(reportActionsToDisplay, transactionThreadReportID ?? null, transactionThreadReportActions ?? []),
+ () => (reportActionsToDisplay ? getCombinedReportActions(reportActionsToDisplay, transactionThreadReportID ?? null, transactionThreadReportActions ?? []) : []),
[reportActionsToDisplay, transactionThreadReportActions, transactionThreadReportID],
);
@@ -218,7 +196,7 @@ function ReportActionsView({
() =>
isEmptyObject(transactionThreadReportActions)
? undefined
- : (allReportActions.find((action) => action.reportActionID === transactionThreadReport?.parentReportActionID) as OnyxEntry),
+ : (allReportActions?.find((action) => action.reportActionID === transactionThreadReport?.parentReportActionID) as OnyxEntry),
[allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID],
);
@@ -234,10 +212,10 @@ function ReportActionsView({
);
const reportActionIDMap = useMemo(() => {
- const reportActionIDs = allReportActions.map((action) => action.reportActionID);
+ const reportActionIDs = allReportActions?.map((action) => action.reportActionID);
return reportActions.map((action) => ({
reportActionID: action.reportActionID,
- reportID: reportActionIDs.includes(action.reportActionID) ? reportID : transactionThreadReport?.reportID,
+ reportID: reportActionIDs?.includes(action.reportActionID) ? reportID : transactionThreadReport?.reportID,
}));
}, [allReportActions, reportID, transactionThreadReport, reportActions]);
@@ -250,22 +228,6 @@ function ReportActionsView({
const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]);
useEffect(() => {
- const wasLoginChangedDetected = prevAuthTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS && !session?.authTokenType;
- if (wasLoginChangedDetected && didUserLogInDuringSession() && isUserCreatedPolicyRoom(report)) {
- openReportIfNecessary();
- }
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [session, report]);
-
- useEffect(() => {
- const prevShouldUseNarrowLayout = prevShouldUseNarrowLayoutRef.current;
- // If the view is expanded from mobile to desktop layout
- // we update the new marker position, mark the report as read, and fetch new report actions
- const didScreenSizeIncrease = prevShouldUseNarrowLayout && !shouldUseNarrowLayout;
- const didReportBecomeVisible = isReportFullyVisible && didScreenSizeIncrease;
- if (didReportBecomeVisible) {
- openReportIfNecessary();
- }
// update ref with current state
prevShouldUseNarrowLayoutRef.current = shouldUseNarrowLayout;
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
@@ -282,14 +244,7 @@ function ReportActionsView({
const loadOlderChats = useCallback(
(force = false) => {
// Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline.
- if (
- !force &&
- (!!network.isOffline ||
- isLoadingOlderReportActions ||
- // If there was an error only try again once on initial mount.
- (didLoadOlderChats.current && hasLoadingOlderReportActionsError) ||
- isLoadingInitialReportActions)
- ) {
+ if (!force && isOffline) {
return;
}
@@ -313,17 +268,7 @@ function ReportActionsView({
getOlderActions(reportID, oldestReportAction.reportActionID);
}
},
- [
- network.isOffline,
- isLoadingOlderReportActions,
- isLoadingInitialReportActions,
- oldestReportAction,
- reportID,
- reportActionIDMap,
- transactionThreadReport,
- hasLoadingOlderReportActionsError,
- hasOlderActions,
- ],
+ [isOffline, oldestReportAction, reportID, reportActionIDMap, transactionThreadReport, hasOlderActions],
);
const loadNewerChats = useCallback(
@@ -333,13 +278,11 @@ function ReportActionsView({
(!reportActionID ||
!isFocused ||
!newestReportAction ||
- isLoadingInitialReportActions ||
- isLoadingNewerReportActions ||
!hasNewerActions ||
isOffline ||
// If there was an error only try again once on initial mount. We should also still load
// more in case we have cached messages.
- (didLoadNewerChats.current && hasLoadingNewerReportActionsError) ||
+ didLoadNewerChats.current ||
newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
) {
return;
@@ -360,19 +303,7 @@ function ReportActionsView({
getNewerActions(reportID, newestReportAction.reportActionID);
}
},
- [
- reportActionID,
- isFocused,
- newestReportAction,
- isLoadingInitialReportActions,
- isLoadingNewerReportActions,
- hasNewerActions,
- isOffline,
- hasLoadingNewerReportActionsError,
- transactionThreadReport,
- reportActionIDMap,
- reportID,
- ],
+ [reportActionID, isFocused, newestReportAction, hasNewerActions, isOffline, transactionThreadReport, reportActionIDMap, reportID],
);
/**
@@ -420,9 +351,21 @@ function ReportActionsView({
};
}, [isTheFirstReportActionIsLinked]);
- // Comments have not loaded at all yet do nothing
- if (!reportActions.length) {
- return null;
+ if (isLoadingInitialReportActions && visibleReportActions.length === 0 && !isOffline) {
+ return ;
+ }
+
+ if (visibleReportActions.length === 0) {
+ return (
+
+ );
}
// AutoScroll is disabled when we do linking to a specific reportAction
@@ -440,11 +383,6 @@ function ReportActionsView({
mostRecentIOUReportActionID={mostRecentIOUReportActionID}
loadOlderChats={loadOlderChats}
loadNewerChats={loadNewerChats}
- isLoadingInitialReportActions={isLoadingInitialReportActions}
- isLoadingOlderReportActions={isLoadingOlderReportActions}
- hasLoadingOlderReportActionsError={hasLoadingOlderReportActionsError}
- isLoadingNewerReportActions={isLoadingNewerReportActions}
- hasLoadingNewerReportActionsError={hasLoadingNewerReportActionsError}
listID={listID}
onContentSizeChange={onContentSizeChange}
shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll}
@@ -457,46 +395,4 @@ function ReportActionsView({
ReportActionsView.displayName = 'ReportActionsView';
-function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActionsViewProps): boolean {
- if (!lodashIsEqual(oldProps.reportActions, newProps.reportActions)) {
- return false;
- }
-
- if (!lodashIsEqual(oldProps.parentReportAction, newProps.parentReportAction)) {
- return false;
- }
-
- if (oldProps.isLoadingInitialReportActions !== newProps.isLoadingInitialReportActions) {
- return false;
- }
-
- if (oldProps.isLoadingOlderReportActions !== newProps.isLoadingOlderReportActions) {
- return false;
- }
-
- if (oldProps.isLoadingNewerReportActions !== newProps.isLoadingNewerReportActions) {
- return false;
- }
-
- if (oldProps.hasLoadingOlderReportActionsError !== newProps.hasLoadingOlderReportActionsError) {
- return false;
- }
-
- if (oldProps.hasLoadingNewerReportActionsError !== newProps.hasLoadingNewerReportActionsError) {
- return false;
- }
-
- if (oldProps.hasNewerActions !== newProps.hasNewerActions) {
- return false;
- }
-
- if (oldProps.hasOlderActions !== newProps.hasOlderActions) {
- return false;
- }
-
- return lodashIsEqual(oldProps.report, newProps.report);
-}
-
-const MemoizedReportActionsView = React.memo(ReportActionsView, arePropsEqual);
-
-export default Performance.withRenderTrace({id: ' rendering'})(MemoizedReportActionsView);
+export default Performance.withRenderTrace({id: ' rendering'})(ReportActionsView);
diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx
index 0b92bc6f00f1..fd7696e21c58 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.tsx
+++ b/src/pages/settings/Security/SecuritySettingsPage.tsx
@@ -1,5 +1,5 @@
import debounce from 'lodash/debounce';
-import React, {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import type {RefObject} from 'react';
import {Dimensions, View} from 'react-native';
import type {GestureResponderEvent} from 'react-native';
@@ -27,7 +27,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import {clearDelegateErrorsByField, removeDelegate} from '@libs/actions/Delegate';
+import {clearDelegateErrorsByField, openSecuritySettingsPage, removeDelegate} from '@libs/actions/Delegate';
import {getLatestError} from '@libs/ErrorUtils';
import getClickedTargetLocation from '@libs/getClickedTargetLocation';
import {formatPhoneNumber} from '@libs/LocalePhoneNumber';
@@ -239,6 +239,10 @@ function SecuritySettingsPage() {
},
];
+ useEffect(() => {
+ openSecuritySettingsPage();
+ }, []);
+
return (
void;
};
-function EarlyDiscountBanner({isSubscriptionPage}: EarlyDiscountBannerProps) {
+function EarlyDiscountBanner({isSubscriptionPage, GuideBookingButton, onDismissedDiscountBanner}: EarlyDiscountBannerProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -42,26 +53,56 @@ function EarlyDiscountBanner({isSubscriptionPage}: EarlyDiscountBannerProps) {
return () => clearInterval(intervalID);
}, [firstDayFreeTrial]);
+ const dismissButton = useMemo(
+ () =>
+ discountInfo?.discountType === 25 && (
+
+ {
+ setIsDismissed(true);
+ onDismissedDiscountBanner?.();
+ }}
+ role={CONST.ROLE.BUTTON}
+ accessibilityLabel={translate('common.close')}
+ >
+
+
+
+ ),
+ [theme.icon, translate, onDismissedDiscountBanner, discountInfo?.discountType],
+ );
+
const rightComponent = useMemo(() => {
const smallScreenStyle = shouldUseNarrowLayout ? [styles.flex0, styles.flexBasis100, styles.justifyContentCenter] : [];
return (
-
+
+ {GuideBookingButton}
);
- }, [shouldUseNarrowLayout, styles.flex0, styles.flexBasis100, styles.justifyContentCenter, styles.flexRow, styles.gap2, styles.flex1, translate, discountInfo?.discountType]);
+ }, [
+ shouldUseNarrowLayout,
+ styles.flex0,
+ styles.mr2,
+ styles.flexBasis100,
+ styles.alignItemsCenter,
+ styles.justifyContentCenter,
+ styles.flexRow,
+ styles.gap2,
+ styles.flex1,
+ translate,
+ GuideBookingButton,
+ dismissButton,
+ ]);
if (!firstDayFreeTrial || !lastDayFreeTrial || !discountInfo) {
return null;
@@ -71,16 +112,21 @@ function EarlyDiscountBanner({isSubscriptionPage}: EarlyDiscountBannerProps) {
return null;
}
- const title = isSubscriptionPage ? (
-
- {translate('subscription.billingBanner.earlyDiscount.subscriptionPageTitle.phrase1', {discountType: discountInfo?.discountType})}
- {translate('subscription.billingBanner.earlyDiscount.subscriptionPageTitle.phrase2')}
-
- ) : (
-
- {translate('subscription.billingBanner.earlyDiscount.onboardingChatTitle.phrase1')}
- {translate('subscription.billingBanner.earlyDiscount.onboardingChatTitle.phrase2', {discountType: discountInfo?.discountType})}
-
+ const title = (
+
+ {isSubscriptionPage ? (
+
+ {translate('subscription.billingBanner.earlyDiscount.subscriptionPageTitle.phrase1', {discountType: discountInfo?.discountType})}
+ {translate('subscription.billingBanner.earlyDiscount.subscriptionPageTitle.phrase2')}
+
+ ) : (
+
+ {translate('subscription.billingBanner.earlyDiscount.onboardingChatTitle.phrase1')}
+ {translate('subscription.billingBanner.earlyDiscount.onboardingChatTitle.phrase2', {discountType: discountInfo?.discountType})}
+
+ )}
+ {shouldUseNarrowLayout && dismissButton}
+
);
return (
diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx
index cbcbb4280bcd..7aeb1d277c07 100644
--- a/src/pages/settings/Wallet/PaymentMethodList.tsx
+++ b/src/pages/settings/Wallet/PaymentMethodList.tsx
@@ -17,6 +17,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import type {FormattedSelectedPaymentMethodIcon} from '@hooks/usePaymentMethodState/types';
import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeIllustrations from '@hooks/useThemeIllustrations';
import useThemeStyles from '@hooks/useThemeStyles';
import {clearAddPaymentMethodError, clearDeletePaymentMethodError} from '@libs/actions/PaymentMethods';
import {getCardFeedIcon, isExpensifyCard, maskCardNumber} from '@libs/CardUtils';
@@ -195,6 +196,7 @@ function PaymentMethodList({
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
+ const illustrations = useThemeIllustrations();
const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated});
const [bankAccountList = {}, bankAccountListResult] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
@@ -216,7 +218,7 @@ function PaymentMethodList({
const assignedCardsGrouped: PaymentMethodItem[] = [];
assignedCardsSorted.forEach((card) => {
- const icon = getCardFeedIcon(card.bank as CompanyCardFeed);
+ const icon = getCardFeedIcon(card.bank as CompanyCardFeed, illustrations);
if (!isExpensifyCard(card.cardID)) {
const pressHandler = onPress as CardPressHandler;
@@ -356,6 +358,7 @@ function PaymentMethodList({
onPress,
isLoadingBankAccountList,
isLoadingCardList,
+ illustrations,
]);
/**
diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
index 378d75732d76..e60d4a62c5ed 100644
--- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
@@ -95,7 +95,7 @@ function PageNotFoundFallback({policyID, fullPageNotFoundViewProps, isFeatureEna
Navigation.goBack(ROUTES.SETTINGS_WORKSPACES);
return;
}
- Navigation.goBack(policyID && !isMoneyRequest ? ROUTES.WORKSPACE_PROFILE.getRoute(policyID) : undefined);
+ Navigation.goBack(policyID && !isMoneyRequest ? ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID) : undefined);
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...fullPageNotFoundViewProps}
diff --git a/src/pages/workspace/WorkspaceConfirmationPage.tsx b/src/pages/workspace/WorkspaceConfirmationPage.tsx
index ea3638b0c84d..66d4b4944fad 100644
--- a/src/pages/workspace/WorkspaceConfirmationPage.tsx
+++ b/src/pages/workspace/WorkspaceConfirmationPage.tsx
@@ -9,14 +9,14 @@ import getCurrentUrl from '@libs/Navigation/currentUrl';
import ROUTES from '@src/ROUTES';
function WorkspaceConfirmationPage() {
- // It is necessary to use here isSmallScreenWidth because on a wide layout we should always navigate to ROUTES.WORKSPACE_PROFILE.
+ // It is necessary to use here isSmallScreenWidth because on a wide layout we should always navigate to ROUTES.WORKSPACE_OVERVIEW.
// shouldUseNarrowLayout cannot be used to determine that as this screen is displayed in RHP and shouldUseNarrowLayout always returns true.
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const onSubmit = (params: WorkspaceConfirmationSubmitFunctionParams) => {
const policyID = params.policyID || generatePolicyID();
- const routeToNavigate = isSmallScreenWidth ? ROUTES.WORKSPACE_INITIAL.getRoute(policyID) : ROUTES.WORKSPACE_PROFILE.getRoute(policyID);
+ const routeToNavigate = isSmallScreenWidth ? ROUTES.WORKSPACE_INITIAL.getRoute(policyID) : ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID);
createWorkspaceWithPolicyDraftAndNavigateToIt('', params.name, false, false, '', policyID, params.currency, params.avatarFile as File, routeToNavigate);
};
const currentUrl = getCurrentUrl();
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 36713c5dce8f..8048e7707db9 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -315,7 +315,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
{
translationKey: 'workspace.common.profile',
icon: Building,
- action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)))),
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID)))),
brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
screenName: SCREENS.WORKSPACE.PROFILE,
},
diff --git a/src/pages/workspace/WorkspaceProfileAddressPage.tsx b/src/pages/workspace/WorkspaceOverviewAddressPage.tsx
similarity index 82%
rename from src/pages/workspace/WorkspaceProfileAddressPage.tsx
rename to src/pages/workspace/WorkspaceOverviewAddressPage.tsx
index 57f55e468b1f..d32f8c64b746 100644
--- a/src/pages/workspace/WorkspaceProfileAddressPage.tsx
+++ b/src/pages/workspace/WorkspaceOverviewAddressPage.tsx
@@ -12,11 +12,11 @@ import type {Address} from '@src/types/onyx/PrivatePersonalDetails';
import type {WithPolicyProps} from './withPolicy';
import withPolicy from './withPolicy';
-type WorkspaceProfileAddressPagePolicyProps = WithPolicyProps;
+type WorkspaceOverviewAddressPagePolicyProps = WithPolicyProps;
-type WorkspaceProfileAddressPageProps = PlatformStackScreenProps & WorkspaceProfileAddressPagePolicyProps;
+type WorkspaceOverviewAddressPageProps = PlatformStackScreenProps & WorkspaceOverviewAddressPagePolicyProps;
-function WorkspaceProfileAddressPage({policy, route}: WorkspaceProfileAddressPageProps) {
+function WorkspaceOverviewAddressPage({policy, route}: WorkspaceOverviewAddressPageProps) {
const {translate} = useLocalize();
const backTo = route.params.backTo;
const address: Address = useMemo(() => {
@@ -58,6 +58,6 @@ function WorkspaceProfileAddressPage({policy, route}: WorkspaceProfileAddressPag
);
}
-WorkspaceProfileAddressPage.displayName = 'WorkspaceProfileAddressPage';
+WorkspaceOverviewAddressPage.displayName = 'WorkspaceOverviewAddressPage';
-export default withPolicy(WorkspaceProfileAddressPage);
+export default withPolicy(WorkspaceOverviewAddressPage);
diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceOverviewCurrencyPage.tsx
similarity index 87%
rename from src/pages/workspace/WorkspaceProfileCurrencyPage.tsx
rename to src/pages/workspace/WorkspaceOverviewCurrencyPage.tsx
index 5c0279714cf0..4726997a5d54 100644
--- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx
+++ b/src/pages/workspace/WorkspaceOverviewCurrencyPage.tsx
@@ -18,11 +18,11 @@ import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper';
import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
-type WorkspaceProfileCurrencyPageProps = WithPolicyAndFullscreenLoadingProps;
+type WorkspaceOverviewCurrencyPageProps = WithPolicyAndFullscreenLoadingProps;
const {COUNTRY} = INPUT_IDS.ADDITIONAL_DATA;
-function WorkspaceProfileCurrencyPage({policy}: WorkspaceProfileCurrencyPageProps) {
+function WorkspaceOverviewCurrencyPage({policy}: WorkspaceOverviewCurrencyPageProps) {
const {translate} = useLocalize();
const onSelectCurrency = (item: CurrencyListItem) => {
@@ -44,7 +44,7 @@ function WorkspaceProfileCurrencyPage({policy}: WorkspaceProfileCurrencyPageProp
>
;
+type WorkspaceOverviewPageProps = WithPolicyProps & PlatformStackScreenProps;
-function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: WorkspaceProfilePageProps) {
+function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: WorkspaceOverviewPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -79,37 +79,37 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac
if (!policy?.id) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_CURRENCY.getRoute(policy.id));
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_CURRENCY.getRoute(policy.id));
}, [policy?.id]);
const onPressAddress = useCallback(() => {
if (!policy?.id) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy.id));
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_ADDRESS.getRoute(policy.id));
}, [policy?.id]);
const onPressName = useCallback(() => {
if (!policy?.id) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy.id));
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_NAME.getRoute(policy.id));
}, [policy?.id]);
const onPressDescription = useCallback(() => {
if (!policy?.id) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy.id));
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_DESCRIPTION.getRoute(policy.id));
}, [policy?.id]);
const onPressShare = useCallback(() => {
if (!policy?.id) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_SHARE.getRoute(policy.id));
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_SHARE.getRoute(policy.id));
}, [policy?.id]);
const onPressPlanType = useCallback(() => {
if (!policy?.id) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_PLAN.getRoute(policy.id));
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_PLAN.getRoute(policy.id));
}, [policy?.id]);
const policyName = policy?.name ?? '';
const policyDescription =
@@ -176,7 +176,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac
;
isSelected: boolean;
};
-function WorkspaceProfilePlanTypePage({policy}: WithPolicyProps) {
+function WorkspaceOverviewPlanTypePage({policy}: WithPolicyProps) {
const [currentPlan, setCurrentPlan] = useState(policy?.type);
const policyID = policy?.id;
const {translate} = useLocalize();
@@ -102,7 +102,7 @@ function WorkspaceProfilePlanTypePage({policy}: WithPolicyProps) {
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
>
@@ -157,6 +157,6 @@ function WorkspaceProfilePlanTypePage({policy}: WithPolicyProps) {
);
}
-WorkspaceProfilePlanTypePage.displayName = 'WorkspaceProfilePlanTypePage';
+WorkspaceOverviewPlanTypePage.displayName = 'WorkspaceOverviewPlanTypePage';
-export default withPolicy(WorkspaceProfilePlanTypePage);
+export default withPolicy(WorkspaceOverviewPlanTypePage);
diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceOverviewSharePage.tsx
similarity index 96%
rename from src/pages/workspace/WorkspaceProfileSharePage.tsx
rename to src/pages/workspace/WorkspaceOverviewSharePage.tsx
index 7d6e73009a9c..2eab58a65e78 100644
--- a/src/pages/workspace/WorkspaceProfileSharePage.tsx
+++ b/src/pages/workspace/WorkspaceOverviewSharePage.tsx
@@ -29,7 +29,7 @@ import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper';
import withPolicy from './withPolicy';
import type {WithPolicyProps} from './withPolicy';
-function WorkspaceProfileSharePage({policy}: WithPolicyProps) {
+function WorkspaceOverviewSharePage({policy}: WithPolicyProps) {
const themeStyles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
@@ -68,7 +68,7 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) {
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
>
bankAccountList?.[paymentBankAccountID.toString()], [paymentBankAccountID, bankAccountList]);
+ const selectedBankAccount = useMemo(() => bankAccountList?.[paymentBankAccountID?.toString() ?? ''], [paymentBankAccountID, bankAccountList]);
const bankAccountNumber = useMemo(() => selectedBankAccount?.accountData?.accountNumber ?? '', [selectedBankAccount]);
const settlementAccountEnding = getLastFourDigits(bankAccountNumber);
@@ -44,7 +44,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting
if (!bankAccountList || isEmptyObject(bankAccountList)) {
return [];
}
- const eligibleBankAccounts = CardUtils.getEligibleBankAccountsForCard(bankAccountList);
+ const eligibleBankAccounts = getEligibleBankAccountsForCard(bankAccountList);
const data = eligibleBankAccounts.map((bankAccount) => ({
text: bankAccount.title,
@@ -56,7 +56,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting
}, [bankAccountList, paymentBankAccountID]);
const selectBankAccount = (newBankAccountID?: number) => {
- Card.updateSettlementAccount(workspaceAccountID, policyID, newBankAccountID, paymentBankAccountID);
+ updateSettlementAccount(workspaceAccountID, policyID, newBankAccountID, paymentBankAccountID);
Navigation.goBack(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, connection));
};
@@ -75,7 +75,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting
{translate('workspace.accounting.chooseReconciliationAccount.chooseBankAccount')}
{translate('workspace.accounting.chooseReconciliationAccount.accountMatches')}
- Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT.getRoute(policyID))}>
+ Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT.getRoute(policyID, Navigation.getActiveRoute()))}>
{translate('workspace.accounting.chooseReconciliationAccount.settlementAccount')}
{translate('workspace.accounting.chooseReconciliationAccount.reconciliationWorks', {lastFourPAN: settlementAccountEnding})}
@@ -85,7 +85,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting
sections={sections}
onSelectRow={({value}) => selectBankAccount(value)}
ListItem={RadioListItem}
- initiallyFocusedOptionKey={paymentBankAccountID.toString()}
+ initiallyFocusedOptionKey={paymentBankAccountID?.toString()}
/>
);
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
index 20eac995a72b..18928d56b0bf 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
@@ -16,6 +16,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePolicy from '@hooks/usePolicy';
import useTheme from '@hooks/useTheme';
+import useThemeIllustrations from '@hooks/useThemeIllustrations';
import useThemeStyles from '@hooks/useThemeStyles';
import {getCardFeedIcon, getDefaultCardName, maskCardNumber} from '@libs/CardUtils';
import {getLatestErrorField} from '@libs/ErrorUtils';
@@ -49,6 +50,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
+ const illustrations = useThemeIllustrations();
const {isOffline} = useNetwork();
const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME);
const connectedIntegration = getConnectedIntegration(policy, accountingIntegrations) ?? connectionSyncProgress?.connectionName;
@@ -97,7 +99,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
>();
const styles = useThemeStyles();
+ const illustrations = useThemeIllustrations();
+
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
const [bankSelected, setBankSelected] = useState>();
const [hasError, setHasError] = useState(false);
@@ -69,7 +72,7 @@ function SelectBankStep() {
isSelected: bankSelected === bank,
leftElement: (
Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT.getRoute(policyID))}
+ onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT.getRoute(policyID, Navigation.getActiveRoute()))}
/>
diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx
index 6ba68ba25fa7..3290ee8b3ce6 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx
@@ -11,15 +11,15 @@ import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as AccountingUtils from '@libs/AccountingUtils';
+import {getRouteParamForConnection} from '@libs/AccountingUtils';
import {getLastFourDigits} from '@libs/BankAccountUtils';
-import * as CardUtils from '@libs/CardUtils';
+import {getEligibleBankAccountsForCard} from '@libs/CardUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
-import * as PolicyUtils from '@libs/PolicyUtils';
+import {getWorkspaceAccountID} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
-import * as Card from '@userActions/Card';
+import {updateSettlementAccount as updateSettlementAccountCard} from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -32,8 +32,8 @@ type WorkspaceSettlementAccountPageProps = PlatformStackScreenProps {
const options = eligibleBankAccounts.map((bankAccount) => {
@@ -78,7 +78,7 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP
}, [eligibleBankAccounts, paymentBankAccountID, styles, translate]);
const updateSettlementAccount = (value: number) => {
- Card.updateSettlementAccount(workspaceAccountID, policyID, value, paymentBankAccountID);
+ updateSettlementAccountCard(workspaceAccountID, policyID, value, paymentBankAccountID);
Navigation.goBack();
};
@@ -95,14 +95,20 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP
>
Navigation.goBack(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS.getRoute(policyID))}
+ onBackButtonPress={() => {
+ if (route.params?.backTo) {
+ Navigation.goBack(route.params.backTo);
+ return;
+ }
+ Navigation.goBack(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS.getRoute(policyID));
+ }}
/>
updateSettlementAccount(value ?? 0)}
shouldSingleExecuteRowSelect
- initiallyFocusedOptionKey={paymentBankAccountID.toString()}
+ initiallyFocusedOptionKey={paymentBankAccountID?.toString()}
listHeaderContent={
<>
{translate('workspace.expensifyCard.settlementAccountDescription')}
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
index a9c6bd14f0c2..f0a4b10621c8 100644
--- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
@@ -19,6 +19,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeIllustrations from '@hooks/useThemeIllustrations';
import useThemeStyles from '@hooks/useThemeStyles';
import {setPolicyPreventSelfApproval} from '@libs/actions/Policy/Policy';
import {removeApprovalWorkflow as removeApprovalWorkflowAction, updateApprovalWorkflow} from '@libs/actions/Workflow';
@@ -63,6 +64,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
const styles = useThemeStyles();
const {formatPhoneNumber, translate} = useLocalize();
const StyleUtils = useStyleUtils();
+ const illustrations = useThemeIllustrations();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [cardList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`);
@@ -371,7 +373,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
badgeText={
memberCard.bank === CONST.EXPENSIFY_CARD.BANK ? convertToDisplayString(memberCard.nameValuePairs?.unapprovedExpenseLimit) : ''
}
- icon={getCardFeedIcon(memberCard.bank as CompanyCardFeed)}
+ icon={getCardFeedIcon(memberCard.bank as CompanyCardFeed, illustrations)}
displayInDefaultIconColor
iconStyles={styles.cardIcon}
iconWidth={variables.cardIconWidth}
diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx
index 968c22283c7b..b96977a359f0 100644
--- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx
+++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx
@@ -9,6 +9,7 @@ import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import type {ListItem} from '@components/SelectionList/types';
import useLocalize from '@hooks/useLocalize';
+import useThemeIllustrations from '@hooks/useThemeIllustrations';
import useThemeStyles from '@hooks/useThemeStyles';
import {
getCardFeedIcon,
@@ -51,6 +52,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const illustrations = useThemeIllustrations();
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [selectedFeed, setSelectedFeed] = useState('');
const [shouldShowError, setShouldShowError] = useState(false);
@@ -124,7 +126,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew
isSelected: selectedFeed === key,
leftElement: (
position: 'absolute',
opacity: 0,
left: -9999,
+ top: -9999,
},
containerWithSpaceBetween: {
@@ -3755,6 +3756,7 @@ const styles = (theme: ThemeColors) =>
narrowSearchHeaderStyle: {
paddingTop: 1,
backgroundColor: theme.appBG,
+ flex: 1,
},
narrowSearchRouterInactiveStyle: {
diff --git a/src/styles/theme/illustrations/themes/dark.ts b/src/styles/theme/illustrations/themes/dark.ts
index 64d121448863..300440960862 100644
--- a/src/styles/theme/illustrations/themes/dark.ts
+++ b/src/styles/theme/illustrations/themes/dark.ts
@@ -1,3 +1,6 @@
+import GenericCompanyCard from '@assets/images/companyCards/generic-dark.svg';
+import GenericCSVCompanyCardLarge from '@assets/images/companyCards/large/generic-csv-dark-large.svg';
+import GenericCompanyCardLarge from '@assets/images/companyCards/large/generic-dark-large.svg';
import ExpensifyApprovedLogo from '@assets/images/subscription-details__approvedlogo.svg';
import EmptyStateBackgroundImage from '@assets/images/themeDependent/empty-state_background-fade-dark.png';
import ExampleCheckEN from '@assets/images/themeDependent/example-check-image-dark-en.png';
@@ -11,6 +14,9 @@ const illustrations = {
ExampleCheckES,
WorkspaceProfile,
ExpensifyApprovedLogo,
+ GenericCompanyCard,
+ GenericCompanyCardLarge,
+ GenericCSVCompanyCardLarge,
} satisfies IllustrationsType;
export default illustrations;
diff --git a/src/styles/theme/illustrations/themes/light.ts b/src/styles/theme/illustrations/themes/light.ts
index a4e5b45ef99c..a4fa86d2f816 100644
--- a/src/styles/theme/illustrations/themes/light.ts
+++ b/src/styles/theme/illustrations/themes/light.ts
@@ -1,3 +1,6 @@
+import GenericCompanyCard from '@assets/images/companyCards/generic-light.svg';
+import GenericCSVCompanyCardLarge from '@assets/images/companyCards/large/generic-csv-light-large.svg';
+import GenericCompanyCardLarge from '@assets/images/companyCards/large/generic-light-large.svg';
import ExpensifyApprovedLogo from '@assets/images/subscription-details__approvedlogo--light.svg';
import EmptyStateBackgroundImage from '@assets/images/themeDependent/empty-state_background-fade-light.png';
import ExampleCheckEN from '@assets/images/themeDependent/example-check-image-light-en.png';
@@ -11,6 +14,9 @@ const illustrations = {
ExampleCheckES,
WorkspaceProfile,
ExpensifyApprovedLogo,
+ GenericCompanyCard,
+ GenericCompanyCardLarge,
+ GenericCSVCompanyCardLarge,
} satisfies IllustrationsType;
export default illustrations;
diff --git a/src/styles/theme/illustrations/types.ts b/src/styles/theme/illustrations/types.ts
index 04faeb43e9ed..cd897fa4d1cc 100644
--- a/src/styles/theme/illustrations/types.ts
+++ b/src/styles/theme/illustrations/types.ts
@@ -1,6 +1,7 @@
import type {FC} from 'react';
import type {ImageSourcePropType} from 'react-native';
import type {SvgProps} from 'react-native-svg';
+import type IconAsset from '@src/types/utils/IconAsset';
type IllustrationsType = {
EmptyStateBackgroundImage: ImageSourcePropType;
@@ -8,6 +9,9 @@ type IllustrationsType = {
ExampleCheckEN: ImageSourcePropType;
WorkspaceProfile: ImageSourcePropType;
ExpensifyApprovedLogo: FC;
+ GenericCompanyCard: IconAsset;
+ GenericCompanyCardLarge: IconAsset;
+ GenericCSVCompanyCardLarge: IconAsset;
};
export default IllustrationsType;
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index 7f1ff92705cc..11accbdae72b 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -1800,6 +1800,67 @@ describe('actions/IOU', () => {
});
});
});
+
+ it('should update split chat report lastVisibleActionCreated to the latest IOU action when split bill in a DM', async () => {
+ // Given a DM chat with no expenses
+ const reportID = '1';
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
+ reportID,
+ type: CONST.REPORT.TYPE.CHAT,
+ participants: {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT},
+ });
+
+ // When the user split bill twice on the DM
+ splitBill({
+ participants: [{accountID: CARLOS_ACCOUNT_ID, login: CARLOS_EMAIL}],
+ currentUserLogin: RORY_EMAIL,
+ currentUserAccountID: RORY_ACCOUNT_ID,
+ comment: '',
+ amount: 100,
+ currency: CONST.CURRENCY.USD,
+ merchant: 'test',
+ created: '',
+ existingSplitChatReportID: reportID,
+ });
+
+ await waitForBatchedUpdates();
+
+ splitBill({
+ participants: [{accountID: CARLOS_ACCOUNT_ID, login: CARLOS_EMAIL}],
+ currentUserLogin: RORY_EMAIL,
+ currentUserAccountID: RORY_ACCOUNT_ID,
+ comment: '',
+ amount: 200,
+ currency: CONST.CURRENCY.USD,
+ merchant: 'test',
+ created: '',
+ existingSplitChatReportID: reportID,
+ });
+
+ await waitForBatchedUpdates();
+
+ // Then the DM lastVisibleActionCreated should be updated to the second IOU action created
+ const iouAction = await new Promise>((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ callback: (reportActions) => {
+ Onyx.disconnect(connection);
+ resolve(Object.values(reportActions ?? {}).find((action) => isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(action)?.amount === 200));
+ },
+ });
+ });
+
+ const report = await new Promise>((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ callback: (reportVal) => {
+ Onyx.disconnect(connection);
+ resolve(reportVal);
+ },
+ });
+ });
+ expect(report?.lastVisibleActionCreated).toBe(iouAction?.created);
+ });
});
describe('payMoneyRequestElsewhere', () => {
diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts
index 9aa8f3db7d17..9ff5e614604d 100644
--- a/tests/actions/PolicyTest.ts
+++ b/tests/actions/PolicyTest.ts
@@ -138,8 +138,9 @@ describe('actions/Policy', () => {
expect(reportAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
expect(reportAction.actorAccountID).toBe(ESH_ACCOUNT_ID);
});
- // Created report action and and default MANAGE_TEAM intent tasks (7) assigned to user by guide, signingoff messages (1)
- expect(adminReportActions.length).toBe(9);
+ // Created Report Action, MANAGE_TEAM tasks (minus tasks that requires integrations to be enabled) and signoff message
+ const manageTeamDefaultTaskCount = CONST.ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.MANAGE_TEAM].tasks.length - 2;
+ expect(adminReportActions.length).toBe(2 + manageTeamDefaultTaskCount);
let createdTaskReportActions = 0;
let signingOffMessage = 0;
let taskReportActions = 0;
@@ -160,7 +161,7 @@ describe('actions/Policy', () => {
});
expect(createdTaskReportActions).toBe(1);
expect(signingOffMessage).toBe(1);
- expect(taskReportActions).toBe(7);
+ expect(taskReportActions).toBe(manageTeamDefaultTaskCount);
// Check for success data
(fetch as MockFetch)?.resume?.();
diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx
index 03f32f8ca846..73df63153b1d 100644
--- a/tests/ui/PaginationTest.tsx
+++ b/tests/ui/PaginationTest.tsx
@@ -4,10 +4,10 @@ import {act, fireEvent, render, screen, within} from '@testing-library/react-nat
import {addSeconds, format, subMinutes} from 'date-fns';
import React from 'react';
import Onyx from 'react-native-onyx';
-import * as Localize from '@libs/Localize';
-import * as SequentialQueue from '@libs/Network/SequentialQueue';
-import * as AppActions from '@userActions/App';
-import * as User from '@userActions/User';
+import {translateLocal} from '@libs/Localize';
+import {waitForIdle} from '@libs/Network/SequentialQueue';
+import {setSidebarLoaded} from '@userActions/App';
+import {subscribeToUserEvents} from '@userActions/User';
import App from '@src/App';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -52,7 +52,7 @@ function getReportScreen(reportID = REPORT_ID) {
}
function scrollToOffset(offset: number) {
- const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages');
+ const hintText = translateLocal('sidebarScreen.listOfChatMessages');
fireEvent.scroll(within(getReportScreen()).getByLabelText(hintText), {
nativeEvent: {
contentOffset: {
@@ -82,9 +82,9 @@ function triggerListLayout(reportID?: string) {
function getReportActions(reportID?: string) {
const report = getReportScreen(reportID);
return [
- ...within(report).queryAllByLabelText(Localize.translateLocal('accessibilityHints.chatMessage')),
+ ...within(report).queryAllByLabelText(translateLocal('accessibilityHints.chatMessage')),
// Created action has a different accessibility label.
- ...within(report).queryAllByLabelText(Localize.translateLocal('accessibilityHints.chatWelcomeMessage')),
+ ...within(report).queryAllByLabelText(translateLocal('accessibilityHints.chatWelcomeMessage')),
];
}
@@ -192,7 +192,7 @@ async function signInAndGetApp(): Promise {
// Render the App and sign in as a test user.
render();
await waitForBatchedUpdatesWithAct();
- const hintText = Localize.translateLocal('loginForm.loginForm');
+ const hintText = translateLocal('loginForm.loginForm');
const loginForm = await screen.findAllByLabelText(hintText);
expect(loginForm).toHaveLength(1);
@@ -202,7 +202,7 @@ async function signInAndGetApp(): Promise {
await waitForBatchedUpdatesWithAct();
- User.subscribeToUserEvents();
+ subscribeToUserEvents();
await waitForBatchedUpdates();
@@ -253,7 +253,7 @@ async function signInAndGetApp(): Promise {
});
// We manually setting the sidebar as loaded since the onLayout event does not fire in tests
- AppActions.setSidebarLoaded();
+ setSidebarLoaded();
});
await waitForBatchedUpdatesWithAct();
@@ -261,7 +261,7 @@ async function signInAndGetApp(): Promise {
describe('Pagination', () => {
afterEach(async () => {
- await SequentialQueue.waitForIdle();
+ await waitForIdle();
await act(async () => {
await Onyx.clear();
@@ -346,7 +346,7 @@ describe('Pagination', () => {
expect(getReportActions()).toHaveLength(10);
// There is 1 extra call here because of the comment linking report.
- TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 2);
+ TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 3);
TestHelper.expectAPICommandToHaveBeenCalledWith('OpenReport', 1, {reportID: REPORT_ID, reportActionID: '5'});
TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0);
TestHelper.expectAPICommandToHaveBeenCalledWith('GetNewerActions', 0, {reportID: REPORT_ID, reportActionID: '5'});
@@ -357,13 +357,12 @@ describe('Pagination', () => {
scrollToOffset(0);
await waitForBatchedUpdatesWithAct();
- TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 2);
+ TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 3);
TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0);
- TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 2);
- TestHelper.expectAPICommandToHaveBeenCalledWith('GetNewerActions', 1, {reportID: REPORT_ID, reportActionID: '10'});
+ TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 1);
- // We now have 15 messages. 5 from the initial OpenReport and 10 from the 2 GetNewerActions calls.
- expect(getReportActions()).toHaveLength(15);
+ // We now have 10 messages. 5 from the initial OpenReport and 5 from the GetNewerActions call.
+ expect(getReportActions()).toHaveLength(10);
// Simulate the backend returning no new messages to simulate reaching the start of the chat.
mockGetNewerActions(0);
@@ -373,12 +372,11 @@ describe('Pagination', () => {
scrollToOffset(0);
await waitForBatchedUpdatesWithAct();
- TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 2);
+ TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 3);
TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0);
- TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 3);
- TestHelper.expectAPICommandToHaveBeenCalledWith('GetNewerActions', 2, {reportID: REPORT_ID, reportActionID: '15'});
+ TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 1);
- // We still have 15 messages. 5 from the initial OpenReport and 10 from the 2 GetNewerActions calls.
- expect(getReportActions()).toHaveLength(15);
+ // We still have 15 messages. 5 from the initial OpenReport and 5 from the GetNewerActions call.
+ expect(getReportActions()).toHaveLength(10);
});
});
diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts
index e14b886c35d6..26b931140d83 100644
--- a/tests/unit/CardUtilsTest.ts
+++ b/tests/unit/CardUtilsTest.ts
@@ -1,10 +1,14 @@
import type {OnyxCollection} from 'react-native-onyx';
+import type IllustrationsType from '@styles/theme/illustrations/types';
+import type * as Illustrations from '@src/components/Icon/Illustrations';
import CONST from '@src/CONST';
import {
checkIfFeedConnectionIsBroken,
flatAllCardsList,
formatCardExpiration,
+ getBankCardDetailsImage,
getBankName,
+ getCardFeedIcon,
getCompanyFeeds,
getCustomOrFormattedFeedName,
getFeedType,
@@ -238,7 +242,19 @@ const allCardsList = {
},
},
} as OnyxCollection;
-/* eslint-enable @typescript-eslint/naming-convention */
+
+const mockIllustrations = {
+ EmptyStateBackgroundImage: 'EmptyStateBackgroundImage',
+ ExampleCheckES: 'ExampleCheckES',
+ ExampleCheckEN: 'ExampleCheckEN',
+ WorkspaceProfile: 'WorkspaceProfile',
+ ExpensifyApprovedLogo: 'ExpensifyApprovedLogo',
+ GenericCompanyCard: 'GenericCompanyCard',
+ GenericCSVCompanyCardLarge: 'GenericCSVCompanyCardLarge',
+ GenericCompanyCardLarge: 'GenericCompanyCardLarge',
+};
+
+jest.mock('@src/components/Icon/Illustrations', () => require('../../__mocks__/Illustrations') as typeof Illustrations);
describe('CardUtils', () => {
describe('Expiration date formatting', () => {
@@ -454,6 +470,12 @@ describe('CardUtils', () => {
expect(feedName).toBe('American Express');
});
+ it('Should return a valid name if a CSV imported feed variation was provided', () => {
+ const feed = 'cards_2267989_ccupload666' as CompanyCardFeed;
+ const feedName = getBankName(feed);
+ expect(feedName).toBe('CSV');
+ });
+
it('Should return empty string if invalid feed was provided', () => {
const feed = 'vvcf' as CompanyCardFeed;
const feedName = getBankName(feed);
@@ -461,6 +483,46 @@ describe('CardUtils', () => {
});
});
+ describe('getCardFeedIcon', () => {
+ it('Should return a valid illustration if a valid feed was provided', () => {
+ const feed = 'vcf';
+ const illustration = getCardFeedIcon(feed, mockIllustrations as unknown as IllustrationsType);
+ expect(illustration).toBe('VisaCompanyCardDetailLarge');
+ });
+
+ it('Should return a valid illustration if an OldDot feed variation was provided', () => {
+ const feed = 'oauth.americanexpressfdx.com 2003' as CompanyCardFeed;
+ const illustration = getCardFeedIcon(feed, mockIllustrations as unknown as IllustrationsType);
+ expect(illustration).toBe('AmexCardCompanyCardDetailLarge');
+ });
+
+ it('Should return a valid illustration if a CSV imported feed variation was provided', () => {
+ const feed = 'cards_2267989_ccupload666' as CompanyCardFeed;
+ const illustration = getCardFeedIcon(feed, mockIllustrations as unknown as IllustrationsType);
+ expect(illustration).toBe('GenericCSVCompanyCardLarge');
+ });
+
+ it('Should return valid illustration if a non-matching feed was provided', () => {
+ const feed = '666' as CompanyCardFeed;
+ const illustration = getCardFeedIcon(feed, mockIllustrations as unknown as IllustrationsType);
+ expect(illustration).toBe('GenericCompanyCardLarge');
+ });
+ });
+
+ describe('getBankCardDetailsImage', () => {
+ it('Should return a valid illustration if a valid bank name was provided', () => {
+ const bank = 'American Express';
+ const illustration = getBankCardDetailsImage(bank, mockIllustrations as unknown as IllustrationsType);
+ expect(illustration).toBe('AmexCardCompanyCardDetail');
+ });
+
+ it('Should return a valid illustration if Other bank name was provided', () => {
+ const bank = 'Other';
+ const illustration = getBankCardDetailsImage(bank, mockIllustrations as unknown as IllustrationsType);
+ expect(illustration).toBe('GenericCompanyCard');
+ });
+ });
+
describe('getFilteredCardList', () => {
it('Should return filtered custom feed cards list', () => {
const cardsList = getFilteredCardList(customFeedCardsList, undefined);
diff --git a/tests/unit/Search/buildCardFilterDataTest.ts b/tests/unit/Search/buildCardFilterDataTest.ts
index 23d911c5901d..c8dd1bbe5405 100644
--- a/tests/unit/Search/buildCardFilterDataTest.ts
+++ b/tests/unit/Search/buildCardFilterDataTest.ts
@@ -5,6 +5,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider';
// eslint-disable-next-line no-restricted-syntax
import * as PolicyUtils from '@libs/PolicyUtils';
import {buildCardFeedsData, buildCardsData} from '@pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage';
+import type IllustrationsType from '@styles/theme/illustrations/types';
import type {CardList, Policy, WorkspaceCardsList} from '@src/types/onyx';
// Use jest.spyOn to mock the implementation
@@ -276,9 +277,26 @@ const domainFeedDataMock = {testDomain: {domainName: 'testDomain', bank: 'Expens
const translateMock = jest.fn();
-describe('buildCardsData individual cards', () => {
+const illustrationsMock = {
+ EmptyStateBackgroundImage: jest.fn(),
+ ExampleCheckES: jest.fn(),
+ ExampleCheckEN: jest.fn(),
+ WorkspaceProfile: jest.fn(),
+ ExpensifyApprovedLogo: jest.fn(),
+ GenericCompanyCard: jest.fn(),
+ GenericCompanyCardLarge: jest.fn(),
+ GenericCSVCompanyCardLarge: jest.fn(),
+};
+
+describe('buildIndividualCardsData', () => {
it("Builds all individual cards and doesn't generate duplicates", () => {
- const result = buildCardsData(workspaceCardFeeds as unknown as Record, cardList as unknown as CardList, {}, ['21588678']);
+ const result = buildCardsData(
+ workspaceCardFeeds as unknown as Record,
+ cardList as unknown as CardList,
+ {},
+ ['21588678'],
+ illustrationsMock as IllustrationsType,
+ );
expect(result.unselected.length + result.selected.length).toEqual(11);
@@ -297,14 +315,27 @@ describe('buildCardsData individual cards', () => {
});
});
it("Doesn't include physical cards that haven't been issued or haven't been activated", () => {
- const result = buildCardsData(workspaceCardFeedsHiddenOnSearch as unknown as Record, cardListHiddenOnSearch as unknown as CardList, {}, []);
+ const result = buildCardsData(
+ workspaceCardFeedsHiddenOnSearch as unknown as Record,
+ cardListHiddenOnSearch as unknown as CardList,
+ {},
+ [],
+ illustrationsMock as IllustrationsType,
+ );
expect(result.unselected.length + result.selected.length).toEqual(0);
});
});
describe('buildCardsData closed cards', () => {
it("Builds all closed cards and doesn't generate duplicates", () => {
- const result = buildCardsData(workspaceCardFeedsClosed as unknown as Record, cardListClosed as unknown as CardList, {}, ['21539012'], true);
+ const result = buildCardsData(
+ workspaceCardFeedsClosed as unknown as Record,
+ cardListClosed as unknown as CardList,
+ {},
+ ['21539012'],
+ illustrationsMock as IllustrationsType,
+ true,
+ );
expect(result.unselected.length + result.selected.length).toEqual(4);
// Check if Expensify card was built correctly
@@ -325,7 +356,7 @@ describe('buildCardsData closed cards', () => {
describe('buildCardsData with empty argument objects', () => {
it('Returns empty array when cardList and workspaceCardFeeds are empty', () => {
- const result = buildCardsData({}, {}, {}, []);
+ const result = buildCardsData({}, {}, {}, [], illustrationsMock as IllustrationsType);
expect(result).toEqual({selected: [], unselected: []});
});
});
@@ -336,6 +367,7 @@ describe('buildCardFeedsData', () => {
domainFeedDataMock,
[],
translateMock as LocaleContextProps['translate'],
+ illustrationsMock as IllustrationsType,
);
it('Buids domain card feed properly', () => {
@@ -369,7 +401,7 @@ describe('buildCardFeedsData', () => {
describe('buildIndividualCardsData with empty argument objects', () => {
it('Return empty array when domainCardFeeds and workspaceCardFeeds are empty', () => {
- const result = buildCardFeedsData({}, {}, [], translateMock as LocaleContextProps['translate']);
+ const result = buildCardFeedsData({}, {}, [], translateMock as LocaleContextProps['translate'], illustrationsMock as IllustrationsType);
expect(result).toEqual({selected: [], unselected: []});
});
});