diff --git a/android/app/build.gradle b/android/app/build.gradle
index d87c8d487391..8664db4c5b1e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -13,7 +13,7 @@ apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.grad
/* Fullstory settings */
fullstory {
org 'o-1WN56P-na1'
- enabledVariants 'all'
+ enabledVariants 'production'
logcatLevel 'debug'
recordOnStart false
}
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009004701
- versionName "9.0.47-1"
+ versionCode 1009004704
+ versionName "9.0.47-4"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md
index 158a55b93e0f..6a43141f1ab9 100644
--- a/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md
+++ b/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md
@@ -18,7 +18,7 @@ This error occurs when the account applied as a category to the expense in Expen
5. Click on the pencil icon on the right to check if you have "In multiple accounts" selected:
6. If "In multiple accounts" is selected, go to Chart of Accounts and click Edit for the account in question.
7. Check the billable option and select an income account within your chart of accounts
-8. Sync your QuickBooks Online connection in Settings > Workspaces > [click workspace] > Connections.
+8. Sync your QuickBooks Online connection in Settings > Workspaces > [workspace name] > Connections.
9. Open the report and click the Export to button and then the QuickBooks Online option.
# ExpensiError QBO046: Feature Not Included in Subscription
@@ -40,11 +40,11 @@ _Please note: Self Employed is not supported:_
**Why does this happen?**
-QuickBooks Online requires all expenses exported from Expensify to use a category matching an account in your chart of accounts. If a category from another source is used, QuickBooks Online will reject the expense. This errors occurs when an expense on the report has a category applied that is not valid in QuickBooks Online.
+QuickBooks Online requires all expenses exported from Expensify to use a category matching an account in your chart of accounts. If a category from another source is used, QuickBooks Online will reject the expense. This error occurs when an expense on the report has a category applied that is not valid in QuickBooks Online.
## How to fix it
-1. Sync your QuickBooks Online connection in Expensify from Settings > Workspaces > [click workspace] > Connections, and click the **Sync Now** button.
+1. Sync your QuickBooks Online connection in Expensify from Settings > Workspaces > [workspace name] > Connections, and click the **Sync Now** button.
2. Review the expenses on the report. If any appear with a red _Category no longer valid_ violation, recategorize the expense until all expenses are violation-free.
3. Click the **Export t**o button and then the **QuickBooks Online** option.
- If you receive the same error, continue.
@@ -56,7 +56,7 @@ QuickBooks Online requires all expenses exported from Expensify to use a categor
**Why does this happen?**
-This error occurs when you have an Employee Record set up with the employee's name, which prevents the Expensify integration from automatically creating the Vendor Record with the same name, since QuickBooks Online won't allow you to have an employee and vendor with the same name.
+This error occurs when you have an Employee Record set up with the employee's name. This prevents the Expensify integration from automatically creating the Vendor Record with the same name since QuickBooks Online won't allow you to have an employee and vendor with the same name.
## How to fix it
@@ -67,7 +67,7 @@ There are two different ways you can resolve this error.
1. Log into QuickBooks Online.
2. Access the Employee Records for your submitters.
3. Edit the name to differentiate them from the name they have on their account in Expensify.
-4. Sync your QuickBooks Online connection in Settings > Workspaces > [click workspace] > Connections.
+4. Sync your QuickBooks Online connection in Settings > Workspaces > [workspace name] > Connections.
5. Open the report and click the Export to button and then the QuickBooks Online option.
**Option 2**:
@@ -85,7 +85,7 @@ This error occurs when you are exporting reimbursable expenses as Journal Entrie
There are three different ways you can resolve this error.
- Select a different type of export for reimbursable expenses under Settings > Workspaces > [worksapce name] > Connections > Configure > Export tab.
-- Enable _Automatically Create Entities_ under Settings > Workspaces > [worksapce name] > Connections > Configure > Advanced to create vendor records automatically.
+- Enable _Automatically Create Entities_ under Settings > Workspaces > [workspace name] > Connections > Configure > Advanced to create vendor records automatically.
- Manually create vendor records in QuickBooks Online for each employee.
# ExpensiError QBO099: Items marked as billable must have sales information checked
@@ -97,7 +97,7 @@ This error occurs when an Item category on an expense does not have sales inform
## How to fix it
1. Log into QuickBooks Online.
-2. Navigate to to your items list.
+2. Navigate to your items list.
3. Click **Edit** to the right of the item used on the report with the error. Here you will see an option to check either "Sales" or "Purchasing".
4. Check the option for **Sales**.
5. Select an income account.
@@ -146,12 +146,12 @@ This error occurs because the currency on the Vendor record in QuickBooks Online
1. Log into QuickBooks Online.
2. Open the vendor record.
-3. Update the record to use with the correct A/P account, currency and an email matching their Expensify email.
-You can find the correct Vendor record by exporting your QuickBooks Online [vendor list](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fqbo.intuit.com%2Fapp%2Fvendors) to a spreadsheet (click the export icon on the right-hand side of the page), and search for the email address of the person who submitted the report.
+3. Update the record to use with the correct A/P account, currency, and email matching their Expensify email.
+You can find the correct Vendor record by exporting your QuickBooks Online vendor list to a spreadsheet (click the export icon on the right-hand side of the page), and search for the email address of the person who submitted the report.
If you have multiple Vendors with different currencies with the same email, Expensify is likely trying to export to the wrong one.
-1. Try removing the email address from the vendor in QuickBooks Online you aren't trying to export to.
+1. Try removing the email address from the vendor in QuickBooks Online that you aren't trying to export to.
2. Sync your QuickBooks Online connection in Settings > Workspaces > [workspace name] > Connections.
3. Open the report and click the **Export to** button and then the **QuickBooks Online** option.
@@ -160,13 +160,13 @@ If this still fails, you'll need to confirm that the A/P account selected in Exp
1. Navigate to Settings > Workspaces > [workspace name] > Connections.
2. Under the Exports tab check that both A/P accounts are the correct currency.
-# Why are company card expenses exporting to the wrong account in QuickBooks Online?
+# Why are company card expenses exported to the wrong account in QuickBooks Online?
Multiple factors could be causing your company card transactions to export to the wrong place in your accounting system, but the best place to start is always the same.
1. First, confirm that the company cards have been mapped to the correct accounts in Settings > Domains > Company Cards > click the **Edit Export button** for the card to view the account.
-2. Next, confirm the expenses in question have been imported from the company card?
- - Only expenses that have the Card+Lock icon next to them will export according to the mapping settings that you configure in the domain settings.
+2. Next, confirm the expenses in question have been imported from the company card.
+ - Only expenses with the Card+Lock icon next to them will export according to the mapping settings that you configure in the domain settings.
It’s important to note that expenses imported from a card linked at the individual account level, expenses created from a SmartScanned receipt, and manually created cash expenses will export to the default bank account selected in your connection's configuration settings.
@@ -174,7 +174,7 @@ It’s important to note that expenses imported from a card linked at the indivi
The user exporting the report must be a domain admin. You can check the history and comment section at the bottom of the report to see who exported the report.
-If your reports are being exported automatically by Concierge, the user listed as the Preferred Exporter under Settings > Workspaces > [workspaces name] > Connections > click **Configure** must be a domain admin as well.
+If your reports are being exported automatically by Concierge, the user listed as the Preferred Exporter under Settings > Workspaces > [workspace name] > Connections > click **Configure** must also be a domain admin.
If the report exporter is not a domain admin, all company card expenses will export to the bank account set in Settings > Workspaces > [workspace name] > Connections > click **Configure** for non-reimbursable expenses.
diff --git a/docs/articles/new-expensify/connections/sage-intacct/Connect-to-Sage-Intacct.md b/docs/articles/new-expensify/connections/sage-intacct/Connect-to-Sage-Intacct.md
index 8e69a03fb666..c577c17e8463 100644
--- a/docs/articles/new-expensify/connections/sage-intacct/Connect-to-Sage-Intacct.md
+++ b/docs/articles/new-expensify/connections/sage-intacct/Connect-to-Sage-Intacct.md
@@ -4,37 +4,37 @@ description: Integrate Sage Intacct with Expensify
order: 1
---
-# Connect to Sage Intacct
+The Sage Intacct integration allows for automated syncing and reduces manual entries. The integration allows you to import your standard dimensions (like department, class, location, customer, and project/job) as well as user-defined dimensions for selection in Expensify.
-Enjoy automated syncing and reduce manual entries with the Expensify and Sage Intacct integration. Gain in-depth, real-time financial insights with user-defined dimensions, as well as expense coding by department, class, location, customer, and project (job).
+The features available for the Expensify connection with Sage Intacct vary based on your Sage Intacct subscription. The features may still be visible in Expensify even if you don't have access, but you will receive an error if the feature isn't available with your subscription.
{% include info.html %}
The Sage Intacct integration is only available on the Control plan.
{% include end-info.html %}
-## Overview
+# Overview
-Expensify’s integration with Sage Intacct allows you to connect using either role-based permissions or user-based permissions and exporting either expense reports or vendor bills.
+Expensify’s integration with Sage Intacct allows you to connect using either role-based permissions or user-based permissions and to export either expense reports or vendor bills.
-Checklist of items to complete:
+**Checklist of items to complete:**
1. Create a web services user and configure permissions
-1. Enable the T&E module (only required if exporting out-of-pocket expenses as Expense Reports)
-1. Set up Employees in Sage Intacct (only required if exporting expenses as Expense Reports)
-1. Set up Expense Types (only required if exporting expenses as Expense Reports)
-1. Enable Customization Services
-1. Download the Expensify Package
-1. Upload the Expensify Package in Sage Intacct
-1. Add web services authorization
-1. Enter credentials and connect Expensify and Sage Intacct
-1. Configure integration sync options
-
-## Step 1a: Create a web services user (Connecting with User-based permissions)
-Note: If the steps in this section look different in your Sage Intacct instance, you likely use role-based permissions. If that’s the case, follow the steps [here].
+2. Enable the T&E module (only required if exporting out-of-pocket expenses as Expense Reports)
+3. Set up Employees in Sage Intacct (only required if exporting expenses as Expense Reports)
+4. Set up Expense Types (only required if exporting expenses as Expense Reports)
+5. Enable Customization Services
+6. Download the Expensify Package
+7. Upload the Expensify Package in Sage Intacct
+8. Add web services authorization
+9. Enter credentials and connect Expensify and Sage Intacct
+10. Configure integration sync options
+
+# Step 1a: Create a web services user (Connecting with User-based permissions)
+Note: If the steps in this section look different from your Sage Intacct instance, you likely use role-based permissions. If that’s the case, start with [Step 1b](#step-1b-create-a-web-services-user-connecting-with-role-based-permissions).
To connect to Sage Intacct, you’ll need to create a special web services user (please note that Sage Intacct does not charge extra for web services users).
-1. Go to **Company > Web Services Users > New**.
+1. Go to **Company > Web Services Users > New**
2. Configure the user as outlined below:
- **User ID**: “xmlgateway_expensify”
- **Last Name and First Name:** “Expensify”
@@ -59,15 +59,15 @@ These are the permissions required for this integration when exporting out-of-po
- **Projects (Read-only)** - Only required if using Projects or Customers
- **Accounts Payable (All)** - Only required if exporting any expenses expenses as vendor bills
-## Step 1b: Create a web services user (Connecting with Role-based permissions)
-Note: If the steps in this section look different in your Sage Intacct instance, you likely use role-based permissions. If that’s the case, follow the steps [here].
+# Step 1b: Create a web services user (Connecting with Role-based permissions)
+Note: If the steps in this section look different in your Sage Intacct instance, you likely use role-based permissions. If that’s the case, start with [Step 1a](#step-1a-create-a-web-services-user-connecting-with-user-based-permissions).
**First, you'll need to create the new role:**
1. In Sage Intacct, click **Company**, then click on the **+ button** next to Roles
-1. Name the role "Expensify", then click **Save**
-1. Go to **Roles > Subscriptions** and find the “Expensify” role you just created
-1. Configure correct permissions for this role by clicking the checkbox and then clicking on the Permissions hyperlink. These are the permissions required for this integration when exporting out-of-pocket expenses as vendor bills:
+2. Name the role "Expensify", then click **Save**
+3. Go to **Roles > Subscriptions** and find the “Expensify” role you just created
+4. Configure correct permissions for this role by clicking the checkbox and then clicking on the Permissions hyperlink. These are the permissions required for this integration when exporting out-of-pocket expenses as vendor bills:
- **Administration (All)**
- **Company (Read-only)**
- **Cash Management (All)**
@@ -89,7 +89,7 @@ Note: If the steps in this section look different in your Sage Intacct instance,
3. Assign the role to that user: click the **+ button**, then select the “Expensify” role and click **Save**
-## Step 2: Enable and configure the Time & Expenses Module
+# Step 2: Enable and configure the Time & Expenses Module
**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.**
Enabling the T&E module is a paid subscription through Sage Intacct and the T&E module is often included in your Sage Intacct instance. For information on the costs of enabling this module, please contact your Sage Intacct account manager.
@@ -118,7 +118,7 @@ In Sage Intacct, go to **Company menu > Subscriptions > Time & Expenses** and to
6. Click **Save** to confirm your configuration
-## Step 3: Set up Employees in Sage Intacct
+# Step 3: Set up Employees in Sage Intacct
**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.**
To set up employees in Sage Intacct:
@@ -135,7 +135,7 @@ To set up employees in Sage Intacct:
1. Fill in their Primary Email Address along with any other required information
-## Step 4: Set up Expense Types in Sage Intacct
+# Step 4: Set up Expense Types in Sage Intacct
**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.**
Expense Types provide a user-friendly way to display the names of your expense accounts to your employees. To set up expense types in Sage Intacct:
@@ -150,20 +150,20 @@ Expense Types provide a user-friendly way to display the names of your expense a
- **Description**
- **Account Number** (from your General Ledger)
-## Step 5: Enable Customization Services
+# Step 5: Enable Customization Services
**Note:** If you already have Platform Services enabled, you can skip this step.
To enable Customization Services, go to **Company > Subscriptions > Customization Services**.
-## Step 6: Download the Expensify Package
+# Step 6: Download the Expensify Package
1. In Expensify, go to Settings > Workspaces
1. Click into the workspace where you'd like to connect to Sage Intacct
- If you already use Expensify, you can optionally create a test workspace by clicking **New Workspace** at the top-right of the Workspaces page. A test workspace allows you to have a sandbox environment for testing before implementing the integration live.
1. Go to **Connections > Sage Intacct > Connect to Sage Intacct**
1. Select **Download Package** (You only need to download the file; we’ll upload it from your Downloads folder later)
-## Step 7: Upload Package in Sage Intacct
+# Step 7: Upload Package in Sage Intacct
If you use Customization Services:
1. Go to **Customization Services > Custom Packages > New Package**
@@ -177,20 +177,20 @@ If you use Platform Services:
1. Click **Import**
-## Step 8: Add Web Services Authorization
+# Step 8: Add Web Services Authorization
1. Go to **Company > Company Info > Security** in Sage Intacct and click **Edit**
2. Scroll down to **Web Services Authorizations** and add “expensify” (all lower case) as a Sender ID
-## Step 9: Enter Credentials and Connect Expensify and Sage Intacct
+# Step 9: Enter Credentials and Connect Expensify and Sage Intacct
1. In Expensify, go to **Settings > Workspaces > [Workspace Name] > Accounting**
1. Click **Set up** next to Sage Intacct
1. Enter the credentials you set for your web services user in Step 1
1. Click **Confirm**
-## FAQ
+# FAQ
-### Why wasn't my report automatically exported to Sage Intacct?
+## Why wasn't my report automatically exported to Sage Intacct?
There are a number of factors that can cause auto-export to fail. If this happens, you will find the specific export error in the report comments for the report that failed to export. Once you’ve resolved any errors, you can manually export the report to Sage Intacct.
-### Can I export negative expenses to Sage Intacct?
+## Can I export negative expenses to Sage Intacct?
Yes, you can export negative expenses to Sage Intacct. If you are exporting out-of-pocket expenses as expense reports, then the total of each exported report cannot be negative.
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index cc66a06c4d86..11b8f6741051 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.47.1
+ 9.0.47.4
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 8bbff99eccaa..5867b2e8fe07 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.47.1
+ 9.0.47.4
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index cf89ff25aaa7..f28c3129d375 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.47
CFBundleVersion
- 9.0.47.1
+ 9.0.47.4
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 30d2bf8760ff..2647a63e85ce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.47-1",
+ "version": "9.0.47-4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.47-1",
+ "version": "9.0.47-4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -94,7 +94,7 @@
"react-native-launch-arguments": "^4.0.2",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "2.0.71",
+ "react-native-onyx": "2.0.73",
"react-native-pager-view": "6.4.1",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
@@ -35411,9 +35411,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "2.0.71",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.71.tgz",
- "integrity": "sha512-LE3CYMdyRrXFrd+PbPpYFqQAQ5CE7EzibdM2ljhHrnTp3pDjtOjhXBjjVNV1rujgkvX56QXfX63ag/DRfqPMNw==",
+ "version": "2.0.73",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.73.tgz",
+ "integrity": "sha512-ZgzTS9TV3wIh6cYfBM5sXrYz5A37x47a61n07e24p22gr7DosBX6J8ixaVCkC25G58A+2A+jRfzdtwRC5yW34A==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index 99f2ed65942f..0e406f17b454 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.47-1",
+ "version": "9.0.47-4",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -149,7 +149,7 @@
"react-native-launch-arguments": "^4.0.2",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "2.0.71",
+ "react-native-onyx": "2.0.73",
"react-native-pager-view": "6.4.1",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index 7152fe01d006..d07a76cd8091 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -489,7 +489,6 @@ const CONST = {
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
SPOTNANA_TRAVEL: 'spotnanaTravel',
REPORT_FIELDS_FEATURE: 'reportFieldsFeature',
- WORKSPACE_FEEDS: 'workspaceFeeds',
COMPANY_CARD_FEEDS: 'companyCardFeeds',
DIRECT_FEEDS: 'directFeeds',
NETSUITE_USA_TAX: 'netsuiteUsaTax',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 645530afc5f2..5d31b2428a4b 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -662,6 +662,22 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/export',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/export` as const,
},
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_SETUP_MODAL: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/setup-modal',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/setup-modal` as const,
+ },
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/setup-required-device',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/setup-required-device` as const,
+ },
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_TRIGGER_FIRST_SYNC: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/trigger-first-sync',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/trigger-first-sync` as const,
+ },
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/import',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/import` as const,
+ },
WORKSPACE_PROFILE_NAME: {
route: 'settings/workspaces/:policyID/profile/name',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/name` as const,
@@ -1534,18 +1550,6 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced/payment-account',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced/payment-account` as const,
},
- POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_SETUP_MODAL: {
- route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/setup-modal',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/setup-modal` as const,
- },
- POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL: {
- route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/setup-required-device',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/setup-required-device` as const,
- },
- POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_TRIGGER_FIRST_SYNC: {
- route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/trigger-first-sync',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/trigger-first-sync` as const,
- },
DEBUG_REPORT: {
route: 'debug/report/:reportID',
getRoute: (reportID: string) => `debug/report/${reportID}` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 53632eaeb4e1..0e29da05bdfe 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -316,6 +316,10 @@ const SCREENS = {
QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Account_Selector',
QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Invoice_Account_Selector',
QUICKBOOKS_DESKTOP_EXPORT: 'Workspace_Accounting_Quickbooks_Desktop_Export',
+ QUICKBOOKS_DESKTOP_SETUP_MODAL: 'Policy_Accouting_Quickbooks_Desktop_Setup_Modal',
+ QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL: 'Policy_Accouting_Quickbooks_Desktop_Setup_Required_Device_Modal',
+ QUICKBOOKS_DESKTOP_TRIGGER_FIRST_SYNC: 'Policy_Accouting_Quickbooks_Desktop_Trigger_First_Sync',
+ QUICKBOOKS_DESKTOP_IMPORT: 'Policy_Accounting_Quickbooks_Desktop_Import',
XERO_IMPORT: 'Policy_Accounting_Xero_Import',
XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers',
XERO_CHART_OF_ACCOUNTS: 'Policy_Accounting_Xero_Import_Chart_Of_Accounts',
@@ -385,9 +389,6 @@ const SCREENS = {
SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account',
SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced',
SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account',
- QUICKBOOKS_DESKTOP_SETUP_MODAL: 'Policy_Accouting_Quickbooks_Desktop_Setup_Modal',
- QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL: 'Policy_Accouting_Quickbooks_Desktop_Setup_Required_Device_Modal',
- QUICKBOOKS_DESKTOP_TRIGGER_FIRST_SYNC: 'Policy_Accouting_Quickbooks_Desktop_Trigger_First_Sync',
CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation',
RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings',
},
diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx
index ffceccc84c8d..e542ed56bdd3 100644
--- a/src/components/Composer/index.native.tsx
+++ b/src/components/Composer/index.native.tsx
@@ -1,13 +1,14 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import mimeDb from 'mime-db';
import type {ForwardedRef} from 'react';
-import React, {useCallback, useEffect, useMemo, useRef} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native';
import {StyleSheet} from 'react-native';
import type {FileObject} from '@components/AttachmentModal';
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useKeyboardState from '@hooks/useKeyboardState';
import useMarkdownStyle from '@hooks/useMarkdownStyle';
import useResetComposerFocus from '@hooks/useResetComposerFocus';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -38,6 +39,7 @@ function Composer(
selection,
value,
isGroupPolicyReport = false,
+ showSoftInputOnFocus = true,
...props
}: ComposerProps,
ref: ForwardedRef,
@@ -50,7 +52,11 @@ function Composer(
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const [contextMenuHidden, setContextMenuHidden] = useState(true);
+
const {inputCallbackRef, inputRef: autoFocusInputRef} = useAutoFocusInput();
+ const keyboardState = useKeyboardState();
+ const isKeyboardShown = keyboardState?.isKeyboardShown ?? false;
useEffect(() => {
if (autoFocus === !!autoFocusInputRef.current) {
@@ -110,6 +116,13 @@ function Composer(
const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]);
const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]);
+ useEffect(() => {
+ if (!showSoftInputOnFocus || !isKeyboardShown) {
+ return;
+ }
+ setContextMenuHidden(false);
+ }, [showSoftInputOnFocus, isKeyboardShown]);
+
return (
);
}
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index 15087193a593..1f63d8a15217 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -73,6 +73,7 @@ function Composer(
isComposerFullSize = false,
shouldContainScroll = true,
isGroupPolicyReport = false,
+ showSoftInputOnFocus = true,
...props
}: ComposerProps,
ref: ForwardedRef,
@@ -389,6 +390,7 @@ function Composer(
value={value}
defaultValue={defaultValue}
autoFocus={autoFocus}
+ inputMode={showSoftInputOnFocus ? 'text' : 'none'}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
onSelectionChange={addCursorPositionToSelectionChange}
diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts
index ef497dd52e47..7f54c7486e8d 100644
--- a/src/components/Composer/types.ts
+++ b/src/components/Composer/types.ts
@@ -74,6 +74,9 @@ type ComposerProps = Omit & {
/** Indicates whether the composer is in a group policy report. Used for disabling report mentioning style in markdown input */
isGroupPolicyReport?: boolean;
+
+ /** Whether the soft keyboard is open */
+ showSoftInputOnFocus?: boolean;
};
export type {TextSelection, ComposerProps, CustomSelectionChangeEvent};
diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx
index 0b314ac8b2a3..3c38c9f4c4a3 100644
--- a/src/components/ReportWelcomeText.tsx
+++ b/src/components/ReportWelcomeText.tsx
@@ -40,7 +40,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) {
const isInvoiceRoom = ReportUtils.isInvoiceRoom(report);
const isSystemChat = ReportUtils.isSystemChat(report);
const isDefault = !(isChatRoom || isPolicyExpenseChat || isSelfDM || isInvoiceRoom || isSystemChat);
- const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, undefined, undefined, true);
+ const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, undefined, true, true);
const isMultipleParticipant = participantAccountIDs.length > 1;
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant);
const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy);
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 3c8d6260698c..7cd555ea9e4e 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -91,8 +91,8 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
const {isSmallScreenWidth, isLargeScreenWidth} = useResponsiveLayout();
const navigation = useNavigation>();
const lastSearchResultsRef = useRef>();
- const {selectionMode} = useMobileSelectionMode(false);
const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions, setShouldShowStatusBarLoading} = useSearchContext();
+ const {selectionMode} = useMobileSelectionMode();
const [offset, setOffset] = useState(0);
const {type, status, sortBy, sortOrder, hash} = queryJSON;
@@ -379,11 +379,6 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
onTurnOnSelectionMode={(item) => item && toggleTransaction(item)}
onCheckboxPress={toggleTransaction}
onSelectAll={toggleAllTransactions}
- isSelected={(item) =>
- status !== CONST.SEARCH.STATUS.EXPENSE.ALL && SearchUtils.isReportListItemType(item)
- ? item.transactions.some((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)
- : !!item.isSelected
- }
customListHeader={
!isLargeScreenWidth ? null : (
)
}
- shouldAutoTurnOff={false}
onScroll={onSearchListScroll}
canSelectMultiple={type !== CONST.SEARCH.DATA_TYPES.CHAT && canSelectMultiple}
customListHeaderHeight={searchHeaderHeight}
diff --git a/src/components/SelectionListWithModal/index.tsx b/src/components/SelectionListWithModal/index.tsx
index 068e70099318..2d218bc815fe 100644
--- a/src/components/SelectionListWithModal/index.tsx
+++ b/src/components/SelectionListWithModal/index.tsx
@@ -1,5 +1,5 @@
import type {ForwardedRef} from 'react';
-import React, {forwardRef, useEffect, useRef, useState} from 'react';
+import React, {forwardRef, useEffect, useState} from 'react';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
@@ -14,12 +14,10 @@ import CONST from '@src/CONST';
type SelectionListWithModalProps = BaseSelectionListProps & {
turnOnSelectionModeOnLongPress?: boolean;
onTurnOnSelectionMode?: (item: TItem | null) => void;
- shouldAutoTurnOff?: boolean;
- isSelected?: (item: TItem) => boolean;
};
function SelectionListWithModal(
- {turnOnSelectionModeOnLongPress, onTurnOnSelectionMode, onLongPressRow, sections, shouldAutoTurnOff, isSelected, ...rest}: SelectionListWithModalProps,
+ {turnOnSelectionModeOnLongPress, onTurnOnSelectionMode, onLongPressRow, sections, ...rest}: SelectionListWithModalProps,
ref: ForwardedRef,
) {
const [isModalVisible, setIsModalVisible] = useState(false);
@@ -28,47 +26,21 @@ function SelectionListWithModal(
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here because there is a race condition that causes shouldUseNarrowLayout to change indefinitely in this component
// See https://github.com/Expensify/App/issues/48675 for more details
const {isSmallScreenWidth} = useResponsiveLayout();
- const {selectionMode} = useMobileSelectionMode(shouldAutoTurnOff);
- // Check if selection should be on when the modal is opened
- const wasSelectionOnRef = useRef(false);
- // Keep track of the number of selected items to determine if we should turn off selection mode
- const selectionRef = useRef(0);
+ const {selectionMode} = useMobileSelectionMode(true);
useEffect(() => {
// We can access 0 index safely as we are not displaying multiple sections in table view
- const selectedItems = sections[0].data.filter((item) => {
- if (isSelected) {
- return isSelected(item);
- }
- return !!item.isSelected;
- });
- selectionRef.current = selectedItems.length;
-
+ const selectedItems = sections[0].data.filter((item) => item.isSelected);
if (!isSmallScreenWidth) {
if (selectedItems.length === 0) {
turnOffMobileSelectionMode();
}
return;
}
- if (!wasSelectionOnRef.current && selectedItems.length > 0) {
- wasSelectionOnRef.current = true;
- }
if (selectedItems.length > 0 && !selectionMode?.isEnabled) {
turnOnMobileSelectionMode();
- } else if (selectedItems.length === 0 && selectionMode?.isEnabled && !wasSelectionOnRef.current) {
- turnOffMobileSelectionMode();
}
- }, [sections, selectionMode, isSmallScreenWidth, isSelected]);
-
- useEffect(
- () => () => {
- if (selectionRef.current !== 0) {
- return;
- }
- turnOffMobileSelectionMode();
- },
- [],
- );
+ }, [sections, selectionMode, isSmallScreenWidth]);
const handleLongPressRow = (item: TItem) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 648c1dad36c3..5de528d741a2 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,6 +1,7 @@
import React, {useCallback, useEffect} from 'react';
import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -14,6 +15,7 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & {
function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
const buttonScale = useSharedValue(1);
const buttonOpacity = useSharedValue(1);
const paymentCompleteTextScale = useSharedValue(0);
@@ -77,7 +79,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
{isPaidAnimationRunning && (
- Payment complete
+ {translate('iou.paymentComplete')}
)}
diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx
index b7d84cb25196..7c35f2661336 100644
--- a/src/hooks/useReportIDs.tsx
+++ b/src/hooks/useReportIDs.tsx
@@ -8,6 +8,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import type {Message} from '@src/types/onyx/ReportAction';
+import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems';
import useActiveWorkspace from './useActiveWorkspace';
import useCurrentReportID from './useCurrentReportID';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
@@ -42,7 +43,8 @@ const reportActionsSelector = (reportActions: OnyxEntry
Object.values(reportActions)
.filter(Boolean)
.map((reportAction) => {
- const {reportActionID, actionName, errors = [], originalMessage} = reportAction;
+ const {reportActionID, actionName, errors = []} = reportAction;
+ const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
const message = ReportActionsUtils.getReportActionMessage(reportAction);
const decision = message?.moderationDecision?.decision;
@@ -81,8 +83,8 @@ function ReportIDsContextProvider({
}: ReportIDsContextProviderProps) {
const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE, {initialValue: CONST.PRIORITY_MODE.DEFAULT});
const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
- const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policySelector});
- const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: reportActionsSelector});
+ const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (c) => mapOnyxCollectionItems(c, policySelector)});
+ const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: (c) => mapOnyxCollectionItems(c, reportActionsSelector)});
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [reportsDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
const [betas] = useOnyx(ONYXKEYS.BETAS);
diff --git a/src/languages/en.ts b/src/languages/en.ts
index ae3c33d1b1d0..fa2d39cc29dd 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1026,6 +1026,7 @@ const translations = {
bookingPendingDescription: "This booking is pending because it hasn't been paid yet.",
bookingArchived: 'This booking is archived',
bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.',
+ paymentComplete: 'Payment complete',
justTrackIt: 'Just track it (don’t submit it)',
},
notificationPreferencesPage: {
@@ -2316,6 +2317,7 @@ const translations = {
}),
settlementFrequency: 'Settlement frequency',
deleteConfirmation: 'Are you sure you want to delete this workspace?',
+ deleteWithCardsConfirmation: 'Are you sure you want to delete this workspace? This will remove all card feeds and assigned cards.',
unavailable: 'Unavailable workspace',
memberNotFound: 'Member not found. To invite a new member to the workspace, please use the invite button above.',
notAuthorized: `You don't have access to this page. If you're trying to join this workspace, just ask the workspace owner to add you as a member. Something else? Reach out to ${CONST.EMAIL.CONCIERGE}.`,
@@ -2380,6 +2382,10 @@ const translations = {
title: 'Open this link to connect',
body: 'To complete setup, open the following link on the computer where QuickBooks Desktop is running.',
},
+ importDescription: 'Choose which coding configurations to import from QuickBooks Desktop to Expensify.',
+ classes: 'Classes',
+ items: 'Items',
+ customers: 'Customers/projects',
},
qbo: {
importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index dea758a226c9..2a7a9864c17f 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -634,7 +634,7 @@ const translations = {
editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`,
deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`,
deleteConfirmation: ({action}: DeleteConfirmationParams) =>
- `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gasto' : 'comentario'}`,
+ `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gasto' : 'comentario'}?`,
onlyVisible: 'Visible sólo para',
replyInThread: 'Responder en el hilo',
joinThread: 'Unirse al hilo',
@@ -1021,6 +1021,7 @@ const translations = {
bookingPendingDescription: 'Esta reserva está pendiente porque aún no se ha pagado.',
bookingArchived: 'Esta reserva está archivada',
bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.',
+ paymentComplete: 'Pago completo',
justTrackIt: 'Solo guardarlo (no enviarlo)',
},
notificationPreferencesPage: {
@@ -2337,6 +2338,7 @@ const translations = {
}),
settlementFrequency: 'Frecuencia de liquidación',
deleteConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo?',
+ deleteWithCardsConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo? Se eliminarán todos los datos de las tarjetas y las tarjetas asignadas.',
unavailable: 'Espacio de trabajo no disponible',
memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botón invitar que está arriba.',
notAuthorized: `No tienes acceso a esta página. Si estás intentando unirte a este espacio de trabajo, pide al dueño del espacio de trabajo que te añada como miembro. ¿Necesitas algo más? Comunícate con ${CONST.EMAIL.CONCIERGE}`,
@@ -2402,6 +2404,10 @@ const translations = {
title: 'Abre este enlace para conectar',
body: 'Para completar la configuración, abre el siguiente enlace en la computadora donde se está ejecutando QuickBooks Desktop.',
},
+ importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Desktop a Expensify.',
+ classes: 'Clases',
+ items: 'Artículos',
+ customers: 'Clientes/proyectos',
},
qbo: {
importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.',
diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts
index 7974c35562ca..f560b286a79c 100644
--- a/src/libs/Fullstory/index.native.ts
+++ b/src/libs/Fullstory/index.native.ts
@@ -17,6 +17,8 @@ const FS = {
init: () => {
Environment.getEnvironment().then((envName: string) => {
// We only want to start fullstory if the app is running in production
+ // Since we don't use it in other environments, it is also disabled in build.gradle to speed up Android build times
+ // See https://github.com/Expensify/App/pull/50206 for more information
if (envName !== CONST.ENVIRONMENT.PRODUCTION) {
return;
}
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 5fbf6a8cc072..21bb35c70006 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -1,11 +1,10 @@
import React, {memo, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import Onyx, {useOnyx} from 'react-native-onyx';
+import Onyx, {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener';
import ComposeProviders from '@components/ComposeProviders';
-import {useSession} from '@components/OnyxProvider';
import OptionsListContextProvider from '@components/OptionListContextProvider';
import {SearchContextProvider} from '@components/Search/SearchContext';
import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal';
@@ -22,7 +21,6 @@ import Log from '@libs/Log';
import getCurrentUrl from '@libs/Navigation/currentUrl';
import getOnboardingModalScreenOptions from '@libs/Navigation/getOnboardingModalScreenOptions';
import Navigation from '@libs/Navigation/Navigation';
-import shouldOpenOnAdminRoom from '@libs/Navigation/shouldOpenOnAdminRoom';
import type {AuthScreensParamList, CentralPaneName, CentralPaneScreensParamList} from '@libs/Navigation/types';
import NetworkConnection from '@libs/NetworkConnection';
import onyxSubscribe from '@libs/onyxSubscribe';
@@ -68,6 +66,17 @@ import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator';
import RightModalNavigator from './Navigators/RightModalNavigator';
import WelcomeVideoModalNavigator from './Navigators/WelcomeVideoModalNavigator';
+type AuthScreensProps = {
+ /** Session of currently logged in user */
+ session: OnyxEntry;
+
+ /** The report ID of the last opened public room as anonymous user */
+ lastOpenedPublicRoomID: OnyxEntry;
+
+ /** The last Onyx update ID was applied to the client */
+ initialLastUpdateIDAppliedToClient: OnyxEntry;
+};
+
const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default;
const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default;
const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default;
@@ -80,6 +89,11 @@ const loadReportAvatar = () => require('../../../pages/Rep
const loadReceiptView = () => require('../../../pages/TransactionReceiptPage').default;
const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default;
+function shouldOpenOnAdminRoom() {
+ const url = getCurrentUrl();
+ return url ? new URL(url).searchParams.get('openOnAdminRoom') === 'true' : false;
+}
+
function getCentralPaneScreenInitialParams(screenName: CentralPaneName, initialReportID?: string): Partial> {
if (screenName === SCREENS.SEARCH.CENTRAL_PANE) {
// Generate default query string with buildSearchQueryString without argument.
@@ -211,7 +225,7 @@ const modalScreenListenersWithCancelSearch = {
},
};
-function AuthScreens() {
+function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDAppliedToClient}: AuthScreensProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {shouldUseNarrowLayout, onboardingIsMediumOrLargerScreenWidth, isSmallScreenWidth} = useResponsiveLayout();
@@ -228,10 +242,6 @@ function AuthScreens() {
const {isOnboardingCompleted} = useOnboardingFlowRouter();
let initialReportID: string | undefined;
const isInitialRender = useRef(true);
- const session = useSession();
- const [lastOpenedPublicRoomID] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID);
- const [initialLastUpdateIDAppliedToClient] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT);
-
if (isInitialRender.current) {
Timing.start(CONST.TIMING.HOMEPAGE_INITIAL_RENDER);
@@ -579,4 +589,14 @@ AuthScreens.displayName = 'AuthScreens';
const AuthScreensMemoized = memo(AuthScreens, () => true);
-export default AuthScreensMemoized;
+export default withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ lastOpenedPublicRoomID: {
+ key: ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID,
+ },
+ initialLastUpdateIDAppliedToClient: {
+ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
+ },
+})(AuthScreensMemoized);
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 70d59dcea58b..eed6686d96e7 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -318,6 +318,7 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/qbd/RequireQuickBooksDesktopPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_TRIGGER_FIRST_SYNC]: () =>
require('../../../../pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_IMPORT]: () => require('../../../../pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage').default,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../../pages/ReimbursementAccount/ReimbursementAccountPage').default,
[SCREENS.GET_ASSISTANCE]: () => require('../../../../pages/GetAssistancePage').default,
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default,
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index b5034e0c359c..b6422494241e 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -49,6 +49,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_SETUP_MODAL,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_TRIGGER_FIRST_SYNC,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_IMPORT,
SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT,
SCREENS.WORKSPACE.ACCOUNTING.XERO_CHART_OF_ACCOUNTS,
SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index c15f104effac..0f3a168825b4 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -391,6 +391,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_TRIGGER_FIRST_SYNC]: {
path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_TRIGGER_FIRST_SYNC.route,
},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_CHART_OF_ACCOUNTS]: {path: ROUTES.POLICY_ACCOUNTING_XERO_CHART_OF_ACCOUNTS.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.route},
diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index c84213918f70..218661632896 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -117,11 +117,7 @@ function getMatchingRootRouteForRHPRoute(route: NavigationPartialRoute): Navigat
// If there is rhpNavigator in the state generated for backTo url, we want to get root route matching to this rhp screen.
const rhpNavigator = stateForBackTo.routes.find((rt) => rt.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
if (rhpNavigator && rhpNavigator.state) {
- const isRHPinState = stateForBackTo.routes.at(0)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
-
- if (isRHPinState) {
- return getMatchingRootRouteForRHPRoute(findFocusedRoute(stateForBackTo) as NavigationPartialRoute);
- }
+ return getMatchingRootRouteForRHPRoute(findFocusedRoute(stateForBackTo) as NavigationPartialRoute);
}
// If we know that backTo targets the root route (full screen) we want to use it.
diff --git a/src/libs/Navigation/shouldOpenOnAdminRoom.ts b/src/libs/Navigation/shouldOpenOnAdminRoom.ts
deleted file mode 100644
index a593e8c22768..000000000000
--- a/src/libs/Navigation/shouldOpenOnAdminRoom.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import getCurrentUrl from './currentUrl';
-
-export default function shouldOpenOnAdminRoom() {
- const url = getCurrentUrl();
- return url ? new URL(url).searchParams.get('openOnAdminRoom') === 'true' : false;
-}
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 847e5e1e4255..ff0f203907f3 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -445,6 +445,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_IMPORT]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: {
policyID: string;
};
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index a004974b88e4..b5674c19dd00 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -49,6 +49,7 @@ import * as Localize from './Localize';
import * as LoginUtils from './LoginUtils';
import ModifiedExpenseMessage from './ModifiedExpenseMessage';
import Navigation from './Navigation/Navigation';
+import Parser from './Parser';
import Performance from './Performance';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PhoneNumber from './PhoneNumber';
@@ -571,33 +572,34 @@ function getAlternateText(option: ReportUtils.OptionData, {showChatPreviewLine =
const isAnnounceRoom = ReportUtils.isAnnounceRoom(report);
const isGroupChat = ReportUtils.isGroupChat(report);
const isExpenseThread = ReportUtils.isMoneyRequest(report);
+ const formattedLastMessageText = ReportUtils.formatReportLastMessageText(Parser.htmlToText(option.lastMessageText ?? ''));
if (isExpenseThread || option.isMoneyRequestReport) {
- return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'iou.expense');
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : Localize.translate(preferredLocale, 'iou.expense');
}
if (option.isThread) {
- return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'threads.thread');
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : Localize.translate(preferredLocale, 'threads.thread');
}
if (option.isChatRoom && !isAdminRoom && !isAnnounceRoom) {
- return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : option.subtitle;
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : option.subtitle;
}
if ((option.isPolicyExpenseChat ?? false) || isAdminRoom || isAnnounceRoom) {
- return showChatPreviewLine && !forcePolicyNamePreview && option.lastMessageText ? option.lastMessageText : option.subtitle;
+ return showChatPreviewLine && !forcePolicyNamePreview && formattedLastMessageText ? formattedLastMessageText : option.subtitle;
}
if (option.isTaskReport) {
- return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'task.task');
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : Localize.translate(preferredLocale, 'task.task');
}
if (isGroupChat) {
- return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'common.group');
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : Localize.translate(preferredLocale, 'common.group');
}
- return showChatPreviewLine && option.lastMessageText
- ? option.lastMessageText
+ return showChatPreviewLine && formattedLastMessageText
+ ? formattedLastMessageText
: LocalePhoneNumber.formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList.at(0)?.login ?? '' : '');
}
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index 9fd94dcb86b8..de3afbabadc2 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -25,10 +25,6 @@ function canUseSpotnanaTravel(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.SPOTNANA_TRAVEL) || canUseAllBetas(betas);
}
-function canUseWorkspaceFeeds(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.WORKSPACE_FEEDS) || canUseAllBetas(betas);
-}
-
function canUseCompanyCardFeeds(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.COMPANY_CARD_FEEDS) || canUseAllBetas(betas);
}
@@ -86,7 +82,6 @@ export default {
canUseDupeDetection,
canUseP2PDistanceRequests,
canUseSpotnanaTravel,
- canUseWorkspaceFeeds,
canUseCompanyCardFeeds,
canUseDirectFeeds,
canUseNetSuiteUSATax,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index e9108956cb1d..b4c275e03469 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -65,6 +65,7 @@ import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import {hasValidDraftComment} from './DraftCommentUtils';
import getAttachmentDetails from './fileDownload/getAttachmentDetails';
+import getIsSmallScreenWidth from './getIsSmallScreenWidth';
import isReportMessageAttachment from './isReportMessageAttachment';
import localeCompare from './LocaleCompare';
import * as LocalePhoneNumber from './LocalePhoneNumber';
@@ -1359,6 +1360,12 @@ function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = fa
});
}
+ // if the user hasn't completed the onboarding flow, whether the user should be in the concierge chat or system chat
+ // should be consistent with what chat the user will land after onboarding flow
+ if (!getIsSmallScreenWidth() && !Array.isArray(onboarding) && !onboarding?.hasCompletedGuidedSetupFlow) {
+ return reportsValues.find(isChatUsedForOnboarding);
+ }
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const shouldFilter = excludeReportID || ignoreDomainRooms;
if (shouldFilter) {
@@ -3351,7 +3358,7 @@ function getTransactionReportName(reportAction: OnyxEntry,
}
return Localize.translateLocal(translationKey, {
formattedAmount,
- comment: TransactionUtils.getDescription(transaction) ?? '',
+ comment: TransactionUtils.getMerchantOrDescription(transaction),
});
}
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 0db771eaa96b..11516af54b28 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -416,6 +416,10 @@ function getMerchant(transaction: OnyxInputOrEntry): string {
return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? '';
}
+function getMerchantOrDescription(transaction: OnyxEntry) {
+ return !isMerchantMissing(transaction) ? getMerchant(transaction) : getDescription(transaction);
+}
+
/**
* Return the reimbursable value. Defaults to true to match BE logic.
*/
@@ -1096,6 +1100,7 @@ export {
getOriginalCurrency,
getOriginalAmount,
getMerchant,
+ getMerchantOrDescription,
getMCCGroup,
getCreated,
getFormattedCreated,
diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts
index ed8d4886659c..9338527eaccc 100644
--- a/src/libs/actions/ReportActions.ts
+++ b/src/libs/actions/ReportActions.ts
@@ -36,6 +36,7 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction, k
const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportAction.reportActionID, originalReportID || '-1');
if (linkedTransactionID) {
Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null);
+ Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportAction.childReportID}`, null);
}
// Delete the failed task report too
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index 2f9ec060c1e8..bb96c98100a2 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -1087,6 +1087,7 @@ function deleteTask(report: OnyxEntry) {
Report.notifyNewAction(report.reportID, currentUserAccountID);
if (shouldDeleteTaskReport) {
+ Navigation.goBack();
if (parentReport?.reportID) {
return ROUTES.REPORT_WITH_ID.getRoute(parentReport.reportID);
}
diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
index 91d80c865db8..eb6d61770be0 100644
--- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
+++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
@@ -10,17 +10,13 @@ import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
-import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import shouldOpenOnAdminRoom from '@libs/Navigation/shouldOpenOnAdminRoom';
-import * as ReportUtils from '@libs/ReportUtils';
import * as ValidationUtils from '@libs/ValidationUtils';
import * as PersonalDetails from '@userActions/PersonalDetails';
import * as Report from '@userActions/Report';
@@ -28,22 +24,19 @@ import * as Welcome from '@userActions/Welcome';
import * as OnboardingFlow from '@userActions/Welcome/OnboardingFlow';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/DisplayNameForm';
import type {BaseOnboardingPersonalDetailsProps} from './types';
-function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNativeStyles}: BaseOnboardingPersonalDetailsProps) {
+function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNativeStyles, route}: BaseOnboardingPersonalDetailsProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED);
const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID);
const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID);
- const {isSmallScreenWidth, onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout();
+ const {shouldUseNarrowLayout, isSmallScreenWidth, onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout();
const {inputCallbackRef} = useAutoFocusInput();
const [shouldValidateOnChange, setShouldValidateOnChange] = useState(false);
const {isOffline} = useNetwork();
- const {canUseDefaultRooms} = usePermissions();
- const {activeWorkspaceID} = useActiveWorkspace();
useEffect(() => {
Welcome.setOnboardingErrorMessage('');
@@ -74,20 +67,13 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
Navigation.dismissModal();
- // When hasCompletedGuidedSetupFlow is true, OnboardingModalNavigator in AuthScreen is removed from the navigation stack.
- // On small screens, this removal redirects navigation to HOME. Dismissing the modal doesn't work properly,
- // so we need to specifically navigate to the last accessed report.
- if (!isSmallScreenWidth) {
- return;
- }
- const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom(), activeWorkspaceID)?.reportID;
- if (!lastAccessedReportID) {
- return;
+ // Only navigate to concierge chat when central pane is visible
+ // Otherwise stay on the chats screen.
+ if (!shouldUseNarrowLayout && !route.params?.backTo) {
+ Report.navigateToConciergeChat();
}
- const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '-1');
- Navigation.navigate(lastAccessedReportRoute);
},
- [onboardingPurposeSelected, onboardingAdminsChatReportID, onboardingPolicyID, activeWorkspaceID, canUseDefaultRooms, isSmallScreenWidth],
+ [onboardingPurposeSelected, onboardingAdminsChatReportID, onboardingPolicyID, shouldUseNarrowLayout, route.params?.backTo],
);
const validate = (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => {
diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
index fd160814f1b4..2dad1e6afab0 100644
--- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
+++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
@@ -23,11 +23,22 @@ import variables from '@styles/variables';
import * as Policy from '@userActions/Policy/Policy';
import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
+import type {OnboardingPurposeType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import type {BaseOnboardingPurposeProps} from './types';
+const selectableOnboardingChoices = Object.values(CONST.SELECTABLE_ONBOARDING_CHOICES);
+
+function getOnboardingChoices(customChoices: OnboardingPurposeType[]) {
+ if (customChoices.length === 0) {
+ return selectableOnboardingChoices;
+ }
+
+ return selectableOnboardingChoices.filter((choice) => customChoices.includes(choice));
+}
+
const menuIcons = {
[CONST.ONBOARDING_CHOICES.EMPLOYER]: Illustrations.ReceiptUpload,
[CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: Illustrations.Abacus,
@@ -53,8 +64,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro
const [customChoices = []] = useOnyx(ONYXKEYS.ONBOARDING_CUSTOM_CHOICES);
- const onboardingChoices =
- customChoices.length > 0 ? Object.values(CONST.SELECTABLE_ONBOARDING_CHOICES).filter((choice) => customChoices.includes(choice)) : Object.values(CONST.SELECTABLE_ONBOARDING_CHOICES);
+ const onboardingChoices = getOnboardingChoices(customChoices);
const menuItems: MenuItemProps[] = onboardingChoices.map((choice) => {
const translationKey = `onboarding.purpose.${choice}` as const;
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 0d47e3fd8f35..f54a9c6ec601 100755
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -41,6 +41,7 @@ import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {PersonalDetails, Report} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems';
type ProfilePageProps = StackScreenProps;
@@ -76,7 +77,7 @@ const chatReportSelector = (report: OnyxEntry): OnyxEntry =>
};
function ProfilePage({route}: ProfilePageProps) {
- const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: chatReportSelector});
+ const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (c) => mapOnyxCollectionItems(c, chatReportSelector)});
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [personalDetailsMetadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_METADATA);
const [session] = useOnyx(ONYXKEYS.SESSION);
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index b45b3bcea4a4..8afeb0cf2307 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -238,7 +238,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report);
const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
- const isEmptyChat = useMemo(() => ReportUtils.isEmptyReport(report), [report]);
const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
const indexOfLinkedMessage = useMemo(
(): number => reportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)),
@@ -810,7 +809,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
policy={policy}
pendingAction={reportPendingAction}
isComposerFullSize={!!isComposerFullSize}
- isEmptyChat={isEmptyChat}
lastReportAction={lastReportAction}
workspaceTooltip={workspaceTooltip}
/>
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
index ded25ee1f215..fd48971ea5af 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
@@ -78,7 +78,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {});
const onEmojiPickerToggle = useRef void)>();
const onCancelDeleteModal = useRef(() => {});
- const onComfirmDeleteModal = useRef(() => {});
+ const onConfirmDeleteModal = useRef(() => {});
const onPopoverHideActionCallback = useRef(() => {});
const callbackWhenDeleteModalHide = useRef(() => {});
@@ -225,7 +225,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {
- setInstanceID(Math.random().toString(36).substr(2, 5));
+ setInstanceID(Math.random().toString(36).slice(2, 7));
onPopoverShow.current();
// After we have called the action, reset it.
@@ -264,7 +264,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {
- callbackWhenDeleteModalHide.current = () => (onComfirmDeleteModal.current = runAndResetCallback(onComfirmDeleteModal.current));
+ callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current);
const reportAction = reportActionRef.current;
if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
@@ -294,8 +294,8 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {}, onCancel = () => {}) => {
onCancelDeleteModal.current = onCancel;
- onComfirmDeleteModal.current = onConfirm;
+ onConfirmDeleteModal.current = onConfirm;
reportIDRef.current = reportID;
reportActionRef.current = reportAction ?? null;
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index e63bd952b4ab..12b145a78e87 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -1,6 +1,6 @@
import {useIsFocused, useNavigation} from '@react-navigation/native';
import lodashDebounce from 'lodash/debounce';
-import type {ForwardedRef, MutableRefObject, RefAttributes, RefObject} from 'react';
+import type {ForwardedRef, MutableRefObject, RefObject} from 'react';
import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {
LayoutChangeEvent,
@@ -14,7 +14,7 @@ import type {
import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, View} from 'react-native';
import {useFocusedInputHandler} from 'react-native-keyboard-controller';
import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import {useAnimatedRef, useSharedValue} from 'react-native-reanimated';
import type {Emoji} from '@assets/emojis/types';
import type {FileObject} from '@components/AttachmentModal';
@@ -29,7 +29,6 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Browser from '@libs/Browser';
-import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
import {forceClearInput} from '@libs/ComponentUtils';
import * as ComposerUtils from '@libs/ComposerUtils';
import convertToLTRForComposer from '@libs/convertToLTRForComposer';
@@ -40,7 +39,6 @@ import getPlatform from '@libs/getPlatform';
import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener';
import Parser from '@libs/Parser';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
@@ -65,113 +63,85 @@ type SyncSelection = {
type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string};
-type ComposerWithSuggestionsOnyxProps = {
- /** The parent report actions for the report */
- parentReportActions: OnyxEntry;
+type ComposerWithSuggestionsProps = Partial & {
+ /** Report ID */
+ reportID: string;
- /** The modal state */
- modal: OnyxEntry;
+ /** Callback to focus composer */
+ onFocus: () => void;
- /** The preferred skin tone of the user */
- preferredSkinTone: number;
+ /** Callback to blur composer */
+ onBlur: (event: NativeSyntheticEvent) => void;
- /** Whether the input is focused */
- editFocused: OnyxEntry;
-};
-
-type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps &
- Partial & {
- /** Report ID */
- reportID: string;
-
- /** Callback to focus composer */
- onFocus: () => void;
-
- /** Callback to blur composer */
- onBlur: (event: NativeSyntheticEvent) => void;
-
- /** Callback when layout of composer changes */
- onLayout?: (event: LayoutChangeEvent) => void;
-
- /** Callback to update the value of the composer */
- onValueChange: (value: string) => void;
+ /** Callback when layout of composer changes */
+ onLayout?: (event: LayoutChangeEvent) => void;
- /** Callback when the composer got cleared on the UI thread */
- onCleared?: (text: string) => void;
+ /** Callback to update the value of the composer */
+ onValueChange: (value: string) => void;
- /** Whether the composer is full size */
- isComposerFullSize: boolean;
+ /** Callback when the composer got cleared on the UI thread */
+ onCleared?: (text: string) => void;
- /** Whether the menu is visible */
- isMenuVisible: boolean;
+ /** Whether the composer is full size */
+ isComposerFullSize: boolean;
- /** The placeholder for the input */
- inputPlaceholder: string;
+ /** Whether the menu is visible */
+ isMenuVisible: boolean;
- /** Function to display a file in a modal */
- displayFileInModal: (file: FileObject) => void;
+ /** The placeholder for the input */
+ inputPlaceholder: string;
- /** Whether the user is blocked from concierge */
- isBlockedFromConcierge: boolean;
+ /** Function to display a file in a modal */
+ displayFileInModal: (file: FileObject) => void;
- /** Whether the input is disabled */
- disabled: boolean;
+ /** Whether the user is blocked from concierge */
+ isBlockedFromConcierge: boolean;
- /** Whether the full composer is available */
- isFullComposerAvailable: boolean;
+ /** Whether the input is disabled */
+ disabled: boolean;
- /** Function to set whether the full composer is available */
- setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void;
+ /** Whether the full composer is available */
+ isFullComposerAvailable: boolean;
- /** Function to set whether the comment is empty */
- setIsCommentEmpty: (isCommentEmpty: boolean) => void;
+ /** Function to set whether the full composer is available */
+ setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void;
- /** Function to handle sending a message */
- handleSendMessage: () => void;
+ /** Function to set whether the comment is empty */
+ setIsCommentEmpty: (isCommentEmpty: boolean) => void;
- /** Whether the compose input should show */
- shouldShowComposeInput: OnyxEntry;
+ /** Function to handle sending a message */
+ handleSendMessage: () => void;
- /** Function to measure the parent container */
- measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void;
+ /** Whether the compose input should show */
+ shouldShowComposeInput: OnyxEntry;
- /** Whether the scroll is likely to trigger a layout */
- isScrollLikelyLayoutTriggered: RefObject;
+ /** Function to measure the parent container */
+ measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void;
- /** Function to raise the scroll is likely layout triggered */
- raiseIsScrollLikelyLayoutTriggered: () => void;
+ /** Whether the scroll is likely to trigger a layout */
+ isScrollLikelyLayoutTriggered: RefObject;
- /** The ref to the suggestions */
- suggestionsRef: React.RefObject;
+ /** Function to raise the scroll is likely layout triggered */
+ raiseIsScrollLikelyLayoutTriggered: () => void;
- /** The ref to the next modal will open */
- isNextModalWillOpenRef: MutableRefObject;
+ /** The ref to the suggestions */
+ suggestionsRef: React.RefObject;
- /** Whether the edit is focused */
- editFocused: boolean;
+ /** The ref to the next modal will open */
+ isNextModalWillOpenRef: MutableRefObject;
- /** Wheater chat is empty */
- isEmptyChat?: boolean;
+ /** The last report action */
+ lastReportAction?: OnyxEntry;
- /** The last report action */
- lastReportAction?: OnyxEntry;
+ /** Whether to include chronos */
+ includeChronos?: boolean;
- /** Whether to include chronos */
- includeChronos?: boolean;
+ /** Whether report is from group policy */
+ isGroupPolicyReport: boolean;
- /** The parent report action ID */
- parentReportActionID?: string;
-
- /** The parent report ID */
- // eslint-disable-next-line react/no-unused-prop-types -- its used in the withOnyx HOC
- parentReportID: string | undefined;
-
- /** Whether report is from group policy */
- isGroupPolicyReport: boolean;
-
- /** policy ID of the report */
- policyID: string;
- };
+ /** policy ID of the report */
+ policyID: string;
+};
type SwitchToCurrentReportProps = {
preexistingReportID: string;
@@ -211,10 +181,6 @@ const debouncedBroadcastUserIsTyping = lodashDebounce(
const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc();
-// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will
-// prevent auto focus on existing chat for mobile device
-const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();
-
/**
* This component holds the value and selection state.
* If a component really needs access to these state values it should be put here.
@@ -223,17 +189,10 @@ const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();
*/
function ComposerWithSuggestions(
{
- // Onyx
- modal,
- preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE,
- parentReportActions,
-
// Props: Report
reportID,
includeChronos,
- isEmptyChat,
lastReportAction,
- parentReportActionID,
isGroupPolicyReport,
policyID,
@@ -263,7 +222,6 @@ function ComposerWithSuggestions(
// Refs
suggestionsRef,
isNextModalWillOpenRef,
- editFocused,
// For testing
children,
@@ -288,6 +246,15 @@ function ComposerWithSuggestions(
}
return draftComment;
});
+
+ const [modal] = useOnyx(ONYXKEYS.MODAL);
+ const [preferredSkinTone] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {
+ selector: EmojiUtils.getPreferredSkinToneIndex,
+ initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE,
+ });
+
+ const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED);
+
const commentRef = useRef(value);
const lastTextRef = useRef(value);
@@ -298,13 +265,7 @@ function ComposerWithSuggestions(
const {shouldUseNarrowLayout} = useResponsiveLayout();
const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES;
- const parentReportAction = parentReportActions?.[parentReportActionID ?? '-1'];
- const shouldAutoFocus =
- !modal?.isVisible &&
- Modal.areAllModalsHidden() &&
- isFocused &&
- (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) &&
- shouldShowComposeInput;
+ const shouldAutoFocus = !modal?.isVisible && shouldShowComposeInput && Modal.areAllModalsHidden() && isFocused;
const valueRef = useRef(value);
valueRef.current = value;
@@ -313,6 +274,8 @@ function ComposerWithSuggestions(
const [composerHeight, setComposerHeight] = useState(0);
+ const [showSoftInputOnFocus, setShowSoftInputOnFocus] = useState(false);
+
const textInputRef = useRef(null);
const syncSelectionWithOnChangeTextRef = useRef(null);
@@ -800,6 +763,19 @@ function ComposerWithSuggestions(
onScroll={hideSuggestionMenu}
shouldContainScroll={Browser.isMobileSafari()}
isGroupPolicyReport={isGroupPolicyReport}
+ showSoftInputOnFocus={showSoftInputOnFocus}
+ onTouchStart={() => {
+ if (showSoftInputOnFocus) {
+ return;
+ }
+ if (Browser.isMobileSafari()) {
+ setTimeout(() => {
+ setShowSoftInputOnFocus(true);
+ }, CONST.ANIMATED_TRANSITION);
+ return;
+ }
+ setShowSoftInputOnFocus(true);
+ }}
/>
@@ -837,22 +813,6 @@ ComposerWithSuggestions.displayName = 'ComposerWithSuggestions';
const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions);
-export default withOnyx, ComposerWithSuggestionsOnyxProps>({
- modal: {
- key: ONYXKEYS.MODAL,
- },
- preferredSkinTone: {
- key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
- selector: EmojiUtils.getPreferredSkinToneIndex,
- },
- editFocused: {
- key: ONYXKEYS.INPUT_FOCUSED,
- },
- parentReportActions: {
- key: ({parentReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`,
- canEvict: false,
- initWithStoredValues: false,
- },
-})(memo(ComposerWithSuggestionsWithRef));
+export default memo(ComposerWithSuggestionsWithRef);
export type {ComposerWithSuggestionsProps, ComposerRef};
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index 762021163833..1f2c7b7798b5 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -28,7 +28,6 @@ import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import DomUtils from '@libs/DomUtils';
import {getDraftComment} from '@libs/DraftCommentUtils';
@@ -64,7 +63,7 @@ type SuggestionsRef = {
getIsSuggestionsMenuVisible: () => boolean;
};
-type ReportActionComposeProps = Pick & {
+type ReportActionComposeProps = Pick & {
/** A method to call when the form is submitted */
onSubmit: (newComment: string) => void;
@@ -90,10 +89,6 @@ type ReportActionComposeProps = Pick {
const initialModalState = getModalState();
- return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
+ return shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
});
const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize);
const [shouldHideEducationalTooltip, setShouldHideEducationalTooltip] = useState(false);
@@ -466,11 +460,8 @@ function ReportActionCompose({
raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered}
reportID={reportID}
policyID={report?.policyID ?? '-1'}
- parentReportID={report?.parentReportID}
- parentReportActionID={report?.parentReportActionID}
includeChronos={ReportUtils.chatIncludesChronos(report)}
isGroupPolicyReport={isGroupPolicyReport}
- isEmptyChat={isEmptyChat}
lastReportAction={lastReportAction}
isMenuVisible={isMenuVisible}
inputPlaceholder={inputPlaceholder}
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 0ed4ae665781..88955766c991 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -150,40 +150,24 @@ function ReportActionItemSingle({
} else {
secondaryAvatar = {name: '', source: '', type: 'avatar'};
}
- const icon = useMemo(
- () => ({
- source: avatarSource ?? FallbackAvatar,
- type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR,
- name: primaryDisplayName ?? '',
- id: avatarId,
- }),
- [avatarSource, isWorkspaceActor, primaryDisplayName, avatarId],
- );
+ const icon = {
+ source: avatarSource ?? FallbackAvatar,
+ type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR,
+ name: primaryDisplayName ?? '',
+ id: avatarId,
+ };
// Since the display name for a report action message is delivered with the report history as an array of fragments
// we'll need to take the displayName from personal details and have it be in the same format for now. Eventually,
// we should stop referring to the report history items entirely for this information.
- const personArray = useMemo(() => {
- const baseArray = displayName
- ? [
- {
- type: 'TEXT',
- text: displayName,
- },
- ]
- : [action?.person?.at(0)] ?? [];
-
- if (displayAllActors && secondaryAvatar?.name) {
- return [
- ...baseArray,
- {
- type: 'TEXT',
- text: secondaryAvatar?.name ?? '',
- },
- ];
- }
- return baseArray;
- }, [displayName, action?.person, displayAllActors, secondaryAvatar?.name]);
+ const personArray = displayName
+ ? [
+ {
+ type: 'TEXT',
+ text: displayName,
+ },
+ ]
+ : action?.person;
const reportID = report?.reportID;
const iouReportID = iouReport?.reportID;
@@ -247,74 +231,6 @@ function ReportActionItemSingle({
);
};
-
- const getHeading = useCallback(() => {
- return () => {
- if (displayAllActors && personArray.length === 2 && isReportPreviewAction) {
- return (
-
-
-
- {` & `}
-
-
-
- );
- }
- return (
-
- {personArray.map((fragment, index) => (
-
- ))}
-
- );
- };
- }, [
- displayAllActors,
- secondaryAvatar,
- isReportPreviewAction,
- personArray,
- styles.flexRow,
- styles.flex1,
- styles.chatItemMessageHeaderSender,
- styles.pre,
- action,
- actorAccountID,
- displayName,
- icon,
- ]);
-
const hasEmojiStatus = !displayAllActors && status?.emojiCode;
const formattedDate = DateUtils.getStatusUntilDate(status?.clearAfter ?? '');
const statusText = status?.text ?? '';
@@ -345,7 +261,18 @@ function ReportActionItemSingle({
accessibilityLabel={actorHint}
role={CONST.ROLE.BUTTON}
>
- {getHeading()}
+ {personArray?.map((fragment, index) => (
+
+ ))}
{!!hasEmojiStatus && (
diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx
index 7c4ec786b633..90746efa3b68 100644
--- a/src/pages/home/report/ReportFooter.tsx
+++ b/src/pages/home/report/ReportFooter.tsx
@@ -48,9 +48,6 @@ type ReportFooterProps = {
/** Whether to show educational tooltip in workspace chat for first-time user */
workspaceTooltip: OnyxEntry;
- /** Whether the chat is empty */
- isEmptyChat?: boolean;
-
/** The pending action when we are adding a chat */
pendingAction?: PendingAction;
@@ -73,7 +70,6 @@ function ReportFooter({
report = {reportID: '-1'},
reportMetadata,
policy,
- isEmptyChat = true,
isReportReadyForDisplay = true,
isComposerFullSize = false,
workspaceTooltip,
@@ -224,7 +220,6 @@ function ReportFooter({
onComposerBlur={onComposerBlur}
reportID={report.reportID}
report={report}
- isEmptyChat={isEmptyChat}
lastReportAction={lastReportAction}
pendingAction={pendingAction}
isComposerFullSize={isComposerFullSize}
@@ -246,7 +241,6 @@ export default memo(
lodashIsEqual(prevProps.report, nextProps.report) &&
prevProps.pendingAction === nextProps.pendingAction &&
prevProps.isComposerFullSize === nextProps.isComposerFullSize &&
- prevProps.isEmptyChat === nextProps.isEmptyChat &&
prevProps.lastReportAction === nextProps.lastReportAction &&
prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay &&
prevProps.workspaceTooltip?.shouldShow === nextProps.workspaceTooltip?.shouldShow &&
diff --git a/src/pages/signin/SignInModal.tsx b/src/pages/signin/SignInModal.tsx
index 0c8373babae5..bad93b29f8af 100644
--- a/src/pages/signin/SignInModal.tsx
+++ b/src/pages/signin/SignInModal.tsx
@@ -1,35 +1,36 @@
import React, {useEffect, useRef} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import {useSession} from '@components/OnyxProvider';
import ScreenWrapper from '@components/ScreenWrapper';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import Navigation from '@libs/Navigation/Navigation';
-import {waitForIdle} from '@libs/Network/SequentialQueue';
import * as App from '@userActions/App';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
+import type {Session} from '@src/types/onyx';
import SignInPage from './SignInPage';
import type {SignInPageRef} from './SignInPage';
-function SignInModal() {
+type SignInModalOnyxProps = {
+ session: OnyxEntry;
+};
+
+type SignInModalProps = SignInModalOnyxProps;
+
+function SignInModal({session}: SignInModalProps) {
const theme = useTheme();
const StyleUtils = useStyleUtils();
const siginPageRef = useRef(null);
- const session = useSession();
useEffect(() => {
const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS;
if (!isAnonymousUser) {
// Signing in RHP is only for anonymous users
Navigation.isNavigationReady().then(() => Navigation.dismissModal());
-
- // To prevent deadlock when OpenReport and OpenApp overlap, wait for the queue to be idle before calling openApp.
- // This ensures that any communication gaps between the client and server during OpenReport processing do not cause the queue to pause,
- // which would prevent us from processing or clearing the queue.
- waitForIdle().then(() => {
- App.openApp();
- });
+ App.openApp();
}
}, [session?.authTokenType]);
@@ -60,4 +61,6 @@ function SignInModal() {
SignInModal.displayName = 'SignInModal';
-export default SignInModal;
+export default withOnyx({
+ session: {key: ONYXKEYS.SESSION},
+})(SignInModal);
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index a743140278f7..54d8401a99f8 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -62,7 +62,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {translate} = useLocalize();
- const {canUseWorkspaceFeeds, canUseWorkspaceRules, canUseCompanyCardFeeds} = usePermissions();
+ const {canUseWorkspaceRules, canUseCompanyCardFeeds} = usePermissions();
const hasAccountingConnection = !isEmptyObject(policy?.connections);
const isAccountingEnabled = !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections);
const isSyncTaxEnabled =
@@ -100,11 +100,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
DistanceRate.enablePolicyDistanceRates(policyID, isEnabled);
},
},
- ];
-
- // TODO remove this when feature will be fully done, and move spend item inside spendItems array
- if (canUseWorkspaceFeeds) {
- spendItems.push({
+ {
icon: Illustrations.HandCard,
titleTranslationKey: 'workspace.moreFeatures.expensifyCard.title',
subtitleTranslationKey: 'workspace.moreFeatures.expensifyCard.subtitle',
@@ -120,8 +116,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
disabledAction: () => {
setIsDisableExpensifyCardWarningModalOpen(true);
},
- });
- }
+ },
+ ];
if (canUseCompanyCardFeeds) {
spendItems.push({
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index cbd43fd17529..1413ad968069 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -3,8 +3,7 @@ import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useState} from 'react';
import type {ImageStyle, StyleProp} from 'react-native';
import {Image, StyleSheet, View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {useOnyx, withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import Avatar from '@components/Avatar';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import Button from '@components/Button';
@@ -34,20 +33,14 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
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 type {WithPolicyProps} from './withPolicy';
import withPolicy from './withPolicy';
import WorkspacePageWithSections from './WorkspacePageWithSections';
-type WorkspaceProfilePageOnyxProps = {
- /** Constant, list of available currencies */
- currencyList: OnyxEntry;
-};
+type WorkspaceProfilePageProps = WithPolicyProps & StackScreenProps;
-type WorkspaceProfilePageProps = WithPolicyProps & WorkspaceProfilePageOnyxProps & StackScreenProps;
-
-function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {}, route}: WorkspaceProfilePageProps) {
+function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: WorkspaceProfilePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -55,6 +48,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {
const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace();
const {canUseSpotnanaTravel} = usePermissions();
+ const [currencyList = {}] = useOnyx(ONYXKEYS.CURRENCY_LIST);
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID});
// When we create a new workspace, the policy prop will be empty on the first render. Therefore, we have to use policyDraft until policy has been set in Onyx.
@@ -63,6 +57,12 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {
const currencySymbol = currencyList?.[outputCurrency]?.symbol ?? '';
const formattedCurrency = !isEmptyObject(policy) && !isEmptyObject(currencyList) ? `${outputCurrency} - ${currencySymbol}` : '';
+ // We need this to update translation for deleting a workspace when it has third party card feeds or expensify card assigned.
+ const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id ?? '-1');
+ const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
+ const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`);
+ const hasCardFeedOrExpensifyCard = !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList);
+
const [street1, street2] = (policy?.address?.addressStreet ?? '').split('\n');
const formattedAddress =
!isEmptyObject(policy) && !isEmptyObject(policy.address)
@@ -285,11 +285,11 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {
)}
setIsDeleteModalOpen(false)}
- prompt={translate('workspace.common.deleteConfirmation')}
+ prompt={hasCardFeedOrExpensifyCard ? translate('workspace.common.deleteWithCardsConfirmation') : translate('workspace.common.deleteConfirmation')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
@@ -302,8 +302,4 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {
WorkspaceProfilePage.displayName = 'WorkspaceProfilePage';
-export default withPolicy(
- withOnyx({
- currencyList: {key: ONYXKEYS.CURRENCY_LIST},
- })(WorkspaceProfilePage),
-);
+export default withPolicy(WorkspaceProfilePage);
diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx
index ea17d945aed5..82503134b09e 100755
--- a/src/pages/workspace/WorkspacesListPage.tsx
+++ b/src/pages/workspace/WorkspacesListPage.tsx
@@ -120,6 +120,12 @@ function WorkspacesListPage() {
const [policyNameToDelete, setPolicyNameToDelete] = useState();
const isLessThanMediumScreen = isMediumScreenWidth || shouldUseNarrowLayout;
+ // We need this to update translation for deleting a workspace when it has third party card feeds or expensify card assigned.
+ const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyIDToDelete ?? '-1');
+ const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
+ const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`);
+ const hasCardFeedOrExpensifyCard = !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList);
+
const confirmDeleteAndHideModal = () => {
if (!policyIDToDelete || !policyNameToDelete) {
return;
@@ -431,7 +437,7 @@ function WorkspacesListPage() {
isVisible={isDeleteModalOpen}
onConfirm={confirmDeleteAndHideModal}
onCancel={() => setIsDeleteModalOpen(false)}
- prompt={translate('workspace.common.deleteConfirmation')}
+ prompt={hasCardFeedOrExpensifyCard ? translate('workspace.common.deleteWithCardsConfirmation') : translate('workspace.common.deleteConfirmation')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index e0ba62f2abe7..0f02e350d91a 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -73,7 +73,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
const [datetimeToRelative, setDateTimeToRelative] = useState('');
const threeDotsMenuContainerRef = useRef(null);
- const {canUseWorkspaceFeeds, canUseNewDotQBD} = usePermissions();
+ const {canUseNewDotQBD} = usePermissions();
const {startIntegrationFlow, popoverAnchorRefs} = useAccountingContext();
const route = useRoute();
@@ -315,7 +315,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
},
];
- if (!canUseWorkspaceFeeds || !policy?.areExpensifyCardsEnabled) {
+ if (!policy?.areExpensifyCardsEnabled) {
configurationOptions.splice(2, 1);
}
@@ -370,7 +370,6 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
styles.pb0,
styles.mt5,
styles.popoverMenuIcon,
- canUseWorkspaceFeeds,
styles.justifyContentCenter,
connectionSyncProgress?.stageInProgress,
datetimeToRelative,
diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx
new file mode 100644
index 000000000000..f20975a25648
--- /dev/null
+++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import ConnectionLayout from '@components/ConnectionLayout';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import Navigation from '@navigation/Navigation';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+type QBDSectionType = {
+ description: string;
+ action: () => void;
+ title?: string;
+ subscribedSettings: [string];
+};
+
+function QuickbooksDesktopImportPage({policy}: WithPolicyProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const policyID = policy?.id ?? '-1';
+ const {mappings, pendingFields, errorFields} = policy?.connections?.quickbooksDesktop?.config ?? {};
+ const {canUseNewDotQBD} = usePermissions();
+
+ const sections: QBDSectionType[] = [
+ {
+ description: translate('workspace.accounting.accounts'),
+ action: () => {}, // TODO: [QBD] will be implemented in https://github.com/Expensify/App/issues/49703
+ title: translate('workspace.accounting.importAsCategory'),
+ subscribedSettings: [CONST.QUICKBOOKS_CONFIG.ENABLE_NEW_CATEGORIES],
+ },
+ {
+ description: translate('workspace.qbd.classes'),
+ action: () => {}, // TODO: [QBD] will be implemented in https://github.com/Expensify/App/issues/49704
+ title: translate(`workspace.accounting.importTypes.${mappings?.classes ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE}`),
+ subscribedSettings: [CONST.QUICKBOOKS_CONFIG.SYNC_CLASSES],
+ },
+ {
+ description: translate('workspace.qbd.customers'),
+ action: () => {}, // TODO: [QBD] will be implemented in https://github.com/Expensify/App/issues/49705
+ title: translate(`workspace.accounting.importTypes.${mappings?.customers ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE}`),
+ subscribedSettings: [CONST.QUICKBOOKS_CONFIG.SYNC_CUSTOMERS],
+ },
+ {
+ description: translate('workspace.qbd.items'),
+ action: () => {}, // TODO: [QBD] will be implemented in https://github.com/Expensify/App/issues/49706
+ subscribedSettings: [CONST.QUICKBOOKS_CONFIG.SYNC_LOCATIONS],
+ },
+ ];
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID))}
+ >
+ {sections.map((section) => (
+
+
+
+ ))}
+
+ );
+}
+
+QuickbooksDesktopImportPage.displayName = 'PolicyQuickbooksDesktopImportPage';
+
+export default withPolicyConnections(QuickbooksDesktopImportPage);
diff --git a/src/pages/workspace/accounting/utils.tsx b/src/pages/workspace/accounting/utils.tsx
index c2dd2f63b8ba..bb8c4d3a7a82 100644
--- a/src/pages/workspace/accounting/utils.tsx
+++ b/src/pages/workspace/accounting/utils.tsx
@@ -256,7 +256,7 @@ function getAccountingIntegrationData(
key={key}
/>
),
- onImportPagePress: () => {},
+ onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT.getRoute(policyID)),
onExportPagePress: () => {},
onCardReconciliationPagePress: () => {},
onAdvancedPagePress: () => {},
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 705bc799c762..956fcd131d7b 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -1243,6 +1243,18 @@ type QBDConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Whether changes made in QuickBooks Online should be reflected into the app automatically */
enabled: boolean;
};
+
+ /** Configuration of import settings from QuickBooks Desktop to the app */
+ mappings: {
+ /** How QuickBooks Desktop classes displayed as */
+ classes: IntegrationEntityMap;
+
+ /** How QuickBooks Desktop customers displayed as */
+ customers: IntegrationEntityMap;
+ };
+
+ /** Collections of form field errors */
+ errorFields?: OnyxCommon.ErrorFields;
}>;
/** State of integration connection */
diff --git a/src/utils/mapOnyxCollectionItems.ts b/src/utils/mapOnyxCollectionItems.ts
new file mode 100644
index 000000000000..fa41f575234c
--- /dev/null
+++ b/src/utils/mapOnyxCollectionItems.ts
@@ -0,0 +1,12 @@
+import type {KeyValueMapping, OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollectionKey} from '@src/ONYXKEYS';
+
+export default function mapOnyxCollectionItems(
+ collection: OnyxCollection,
+ mapper: (entry: OnyxEntry) => TReturn,
+): NonNullable> {
+ return Object.entries(collection ?? {}).reduce((acc: NonNullable>, [key, entry]) => {
+ acc[key] = mapper(entry);
+ return acc;
+ }, {});
+}
diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts
index bdb24bee0bc5..4d4f1711a628 100644
--- a/tests/e2e/config.ts
+++ b/tests/e2e/config.ts
@@ -52,7 +52,7 @@ export default {
LOG_FILE: `${OUTPUT_DIR}/debug.log`,
// The time in milliseconds after which an operation fails due to timeout
- INTERACTION_TIMEOUT: 300000,
+ INTERACTION_TIMEOUT: 150 * 1000,
// Period we wait between each test runs, to let the device cool down
BOOT_COOL_DOWN: 90 * 1000,