diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js
index 93ea47bed2ae..c8360931845a 100644
--- a/.github/actions/javascript/bumpVersion/index.js
+++ b/.github/actions/javascript/bumpVersion/index.js
@@ -1928,7 +1928,7 @@ class SemVer {
do {
const a = this.build[i]
const b = other.build[i]
- debug('prerelease compare', i, a, b)
+ debug('build compare', i, a, b)
if (a === undefined && b === undefined) {
return 0
} else if (b === undefined) {
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 624c00de6831..5030ea1c2f2b 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -28,6 +28,24 @@ jobs:
- name: 🚀 Push tags to trigger staging deploy 🚀
run: git push --tags
+
+ - name: Warn deployers if staging deploy failed
+ if: ${{ failure() }}
+ uses: 8398a7/action-slack@v3
+ with:
+ status: custom
+ custom_payload: |
+ {
+ channel: '#deployer',
+ attachments: [{
+ color: "#DB4545",
+ pretext: ``,
+ text: `đź’Ą NewDot staging deploy failed. đź’Ą`,
+ }]
+ }
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
deployProduction:
runs-on: ubuntu-latest
@@ -65,6 +83,24 @@ jobs:
PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }}
- name: 🚀 Create release to trigger production deploy 🚀
- run: gh release create ${{ env.PRODUCTION_VERSION }} --generate-notes
+ run: gh release create ${{ env.PRODUCTION_VERSION }} --notes '${{ steps.getReleaseBody.outputs.RELEASE_BODY }}'
env:
GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }}
+
+ - name: Warn deployers if production deploy failed
+ if: ${{ failure() }}
+ uses: 8398a7/action-slack@v3
+ with:
+ status: custom
+ custom_payload: |
+ {
+ channel: '#deployer',
+ attachments: [{
+ color: "#DB4545",
+ pretext: ``,
+ text: `đź’Ą NewDot production deploy failed. đź’Ą`,
+ }]
+ }
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 4f1a95ed2786..c53ad2cd3cc7 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -107,8 +107,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009000505
- versionName "9.0.5-5"
+ versionCode 1009000512
+ versionName "9.0.5-12"
// 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/assets/images/product-illustrations/folder-with-papers.svg b/assets/images/product-illustrations/folder-with-papers.svg
new file mode 100644
index 000000000000..3d00fb147ccd
--- /dev/null
+++ b/assets/images/product-illustrations/folder-with-papers.svg
@@ -0,0 +1,33 @@
+
diff --git a/assets/images/simple-illustrations/simple-illustration__virtualcard.svg b/assets/images/simple-illustrations/simple-illustration__virtualcard.svg
new file mode 100644
index 000000000000..2c1f538102a2
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__virtualcard.svg
@@ -0,0 +1,48 @@
+
+
+
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
index 4232c565e715..cc45e83efa59 100644
--- a/docs/_layouts/default.html
+++ b/docs/_layouts/default.html
@@ -14,7 +14,7 @@
-
+
diff --git a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md
index ebad41aa2267..38686462a1c2 100644
--- a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md
+++ b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md
@@ -5,7 +5,6 @@ description: Expensify Card Settings for Employees
# Using Your Expensify Visa® Commercial Card
-## Getting Started
### Activate Your Card
You can start using your card immediately upon receipt by logging into your Expensify account, heading to your Home tab, and following the prompts on the _**Activate your Expensify Card**_ task.
diff --git a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md
index 19972b79d5e0..cb86c340dc81 100644
--- a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md
+++ b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md
@@ -1,50 +1,50 @@
---
title: Expensify Card - Transaction Disputes & Fraud
-description: Learn how to dispute an Expensify Card transaction.
+description: Understand how to dispute an Expensify Card transaction.
---
-# Overview
-When using your Expensify Visa® Commercial Card, you may come across transaction errors, which can include things like:
-- Unrecognized, unauthorized, or fraudulent charges.
-- Transactions of an incorrect amount.
+# Disputing Expensify Card Transactions
+While using your Expensify Visa® Commercial Card, you might encounter transaction errors, such as:
+- Unauthorized transaction activity
+- Incorrect transaction amounts.
- Duplicate charges for a single transaction.
-- Missing a promised merchant refund.
+- Missing merchant refunds.
-You’ll find all the relevant information on handling these below.
+When that happens, you may need to file a dispute for one or more transactions.
-# How to Navigate the Dispute Process
## Disputing a Transaction
-
-If you spot an Expensify Card transaction error, please contact us immediately at [concierge@expensify.com](mailto:concierge@expensify.com). After that, we'll ask a few questions to better understand the situation. If the transaction has already settled in your account (no longer pending), we can file a dispute with our card processor on your behalf.
-
-If you suspect fraud on your Expensify Card, don't hesitate to cancel it by heading to Settings > Account > Credit Card Import > Request A New Card. Better safe than sorry!
-
-Lastly, if you haven’t enabled Two-Factor Authentication (2FA) yet, please do so ASAP to add an additional layer of security to your account.
+If you notice a transaction error on your Expensify Card, contact us immediately at concierge@expensify.com. We will ask a few questions to understand the situation better, and file a dispute with our card processor on your behalf.
## Types of Disputes
+The most common types of disputes are:
+- Unauthorized or fraudulent disputes
+- Service disputes
-There are two main dispute types:
+### Unauthorized or fraudulent disputes
+- Charges made after your card was lost or stolen.
+- Unauthorized charges while your card is in your possession (indicating compromised information).
+- Continued charges for a canceled recurring subscription.
-1. Unauthorized charges/fraud disputes, which include:
- - Charges made with your card after it was lost or stolen.
- - Unauthorized charges while your card is still in your possession (indicating compromised card information).
- - Continued charges for a canceled recurring subscription.
+**If there are transactions made with your Expensify Card you don't recognize, you'll want to do the following right away:**
+1. Cancel your card by going to _**Settings > Account > Credit Card Import > Request A New Card**_.
+2. Enable Two-Factor Authentication (2FA) for added security under _**Settings > Account > Account Details > Two Factor Authentication**_.
-2. Service disputes, which include:
- - Received damaged or defective merchandise.
- - Charged for merchandise but never received it.
- - Double-charged for a purchase made with another method (e.g., cash).
- - Made a return but didn't receive a timely refund.
- - Multiple charges for a single transaction.
- - Charges settled for an incorrect amount.
+### Service Disputes
+- Received damaged or defective merchandise.
+- Charged for merchandise that was never received.
+- Double-charged for a purchase made with another method (e.g., cash).
+- Made a return but didn't receive a refund.
+- Multiple charges for a single transaction.
+- Charges settled for an incorrect amount.
-You don't need to categorize your dispute; we'll handle that. However, this may help you assess if a situation warrants a dispute. In most cases, the initial step for resolving a dispute should be contacting the merchant, as they can often address the issue promptly.
+For service disputes, contacting the merchant is often the quickest way to resolve the dispute.
## Simplifying the Dispute Process
-
-To ensure the dispute process goes smoothly, please:
-- Provide detailed information about the disputed charge, including why you're disputing it, what occurred, and any steps you've taken to address the issue.
-- If you recognize the merchant but not the charge, and you've transacted with them before, contact the merchant directly, as it may be a non-fraudulent error.
-- Include supporting documentation like receipts or cancellation confirmations when submitting your dispute to enhance the likelihood of a favorable resolution (not required but highly recommended).
+To ensure a smooth dispute process, please:
+- Provide detailed information about the disputed charge, including why you're disputing it and any steps you've taken to address the issue.
+- If you recognize the merchant but not the charge, contact the merchant directly.
+- Include supporting documentation (e.g., receipts, cancellation confirmations) when submitting your dispute to increase the chances of a favorable resolution (recommended but not required).
+- Make sure the transaction isn't pending (pending transactions cannot be disputed).
+
{% include faq-begin.md %}
diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
index 724745f458ef..b65c66c986ad 100644
--- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
+++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
@@ -2,45 +2,35 @@
title: Request the Card
description: Details on requesting the Expensify Card as an employee
---
-# Overview
-
-Once your organization is approved for the Expensify Visa® Commercial Card, you can request a card!
-
-This article covers how to request, activate, and replace your physical and virtual Expensify Cards.
-
-# How to get your first Expensify Card
-
-An admin in your organization must first enable the Expensify Cards before you can receive a card. After that, an admin may assign you a card by setting a limit. You can think of setting a card limit as “unlocking” access to the card.
-
-If you haven’t been assigned a limit yet, look for the task on your account's homepage that says, “Ask your admin for the card!” This task allows you to message your admin team to make that request.
-
-Once you’re assigned a card limit, we’ll notify you via email to let you know you can request a card. A link within the notification email will take you to your account’s homepage, where you can provide your shipping address for the physical card. Enter your address, and we’ll ship the card to arrive within 3-5 business days.
-
-Once your physical card arrives in the mail, activate it in Expensify by entering the last four digits of the card in the activation task on your account’s homepage.
-
-# Virtual Card
-
-Once assigned a limit, a virtual card is available immediately. You can view the virtual card details via **Settings > Account > Credit Card Import > Show Details**. Feel free to begin transacting with the virtual card while your physical card is in transit – your virtual card and physical card share a limit.
-
-Please note that you must enable two-factor authentication on your account if you want to have the option to dispute transactions made on your virtual card.
-
-# Notifications
-
-To stay up-to-date on your card’s limit and spending activity, download the Expensify mobile app and enable push notifications. Your card is connected to your Expensify account, so each transaction on your card will trigger a push notification. We’ll also send you a push notification if we detect potentially fraudulent activity and allow you to confirm your purchase.
-
-# How to request a replacement Expensify Card
-
-You can request a new card anytime if your Expensify Card is lost, stolen, or damaged. From your Expensify account on the web, head to **Settings > Account > Credit Card Import** and click **Request a New Card**. Confirm the shipping information, complete the prompts, and your new card will arrive in 2 - 3 business days.
-
-Selecting the “lost” or “stolen” options will deactivate your current card to prevent potentially fraudulent activity. However, choosing the “damaged” option will leave your current card active so you can use it while the new one is shipped to you.
-
-If you need to cancel your Expensify Card and cannot access the website or mobile app, call our interactive voice recognition phone service (available 24/7). Call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally).
-
-It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account.
-
-# Card Expiration Date
-
-If you notice that your card expiration date is soon, it's time for a new Expensify card. Expensify will automatically input a notification in your account's Home (Inbox) tab. This notice will ask you to input your address, but this is more if you have changed your address since your card was issued to you. You can ignore it and do nothing; the new Expensify card will ship to your address on file. The new Expensify card will have a new, unique card number and will not be associated with the old one.
+To start using the Expensify Card, do the following:
+1. **Enable Expensify Cards:** An admin must first enable the cards. Then, an admin can assign you a card by setting a limit, which allows access to the card.
+2. **Request the Card:**
+ - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!” Use this task to message your admin team.
+ - Once you’re assigned a card limit, you’ll receive an email notification. Click the link in the email to provide your shipping address on your account’s homepage.
+ - Enter your address, and the physical card will be shipped within 3-5 business days.
+3. **Activate the Card:** When your physical card arrives, activate it in Expensify by entering the last four digits of the card in the activation task on your homepage.
+
+### Virtual Cards
+Once you've been assigned a limit, a virtual card is available immediately. You can view its details via _**Settings > Account > Credit Card Import > Show Details**_.
+
+To protect your account and card spend, enable two-factor authentication under _**Settings > Account > Account Details**_.
+
+### Notifications
+- Download the Expensify mobile app and enable push notifications to stay updated on your card’s limit and spending.
+- Each transaction triggers a push notification.
+- You’ll also get notifications for potentially fraudulent activity, allowing you to confirm or dispute charges.
+
+## Request a Replacement Expensify Card
+### If the card is lost, stolen, or damaged Card:
+ - Go to _**Settings > Account > Credit Card Import** and click **Request a New Card**_.
+ - Confirm your shipping information and complete the prompts. The new card will arrive in 2-3 business days.
+ - Selecting “lost” or “stolen” deactivates your current card to prevent fraud. Choosing “damaged” keeps the current card active until the new one arrives.
+ - If you can’t access the website or app, call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally) to cancel your card.
+
+### If the card is expiring
+- If your card is about to expire, Expensify will notify you via your account’s Home (Inbox) tab.
+- Enter your address if it has changed; otherwise, do nothing, and the new card will ship to your address on file.
+- The new card will have a unique number and will not be linked to the old one.
{% include faq-begin.md %}
diff --git a/docs/articles/expensify-classic/expensify-card/Statements.md b/docs/articles/expensify-classic/expensify-card/Statements.md
index 894dfa3d8b9a..eb797f0cee4b 100644
--- a/docs/articles/expensify-classic/expensify-card/Statements.md
+++ b/docs/articles/expensify-classic/expensify-card/Statements.md
@@ -1,73 +1,62 @@
---
title: — Expensify Card Statements and Settlements
-description: Learn how the Expensify Card statement and settlements work!
+description: Understand how to access your Expensify Card Statement
---
-# Overview
-Expensify offers several settlement types and a statement that provides a detailed view of transactions and settlements. We discuss specifics on both below.
+## Expensify Card Statements
+Expensify offers several settlement types and a detailed statement of transactions and settlements.
-# How to use Expensify Visa® Commercial Card Statement and Settlements
-## Using the statement
-If your domain uses the Expensify Card and you have a validated Business Bank Account, access the Expensify Card statement at Settings > Domains > Company Cards > Reconciliation Tab > Settlements.
+### Accessing the Statement
+- If your domain uses the Expensify Card and you have a validated Business Bank Account, access the statement at _**Settings > Domains > Company Cards > Reconciliation Tab > Settlements**_.
+- The statement shows individual transactions (debits) and their corresponding settlements (credits).
-The Expensify Card statement displays individual transactions (debits) and their corresponding settlements (credits). Each Expensify Cardholder has a Digital Card and a Physical Card, which are treated the same in settlement, reconciliation, and exporting to your accounting system.
-
-Here's a breakdown of crucial information in the statement:
-- **Date:** For card payments, it shows the debit date; for card transactions, it displays the purchase date.
-- **Entry ID:** This unique ID groups card payments and transactions together.
-- **Withdrawn Amount:** This applies to card payments, matching the debited amount from the Business Bank Account.
-- **Transaction Amount:** This applies to card transactions, matching the expense purchase amount.
-- **User email:** Applies to card transactions, indicating the cardholder's Expensify email address.
-- **Transaction ID:** A unique ID for locating transactions and assisting Expensify Support in case of issues. Transaction IDs are handy for reconciling pre-authorizations. To find the original purchase, locate the Transaction ID in the Settlements tab of the reconciliation dashboard, download the settlements as a CSV, and search for the Transaction ID within it.
+### Key Information in the Statement
+- **Date:** Debit date for card payments; purchase date for transactions.
+- **Entry ID:** Unique ID grouping card payments and transactions.
+- **Withdrawn Amount:** Amount debited from the Business Bank Account for card payments.
+- **Transaction Amount:** Expense purchase amount for card transactions.
+- **User Email:** Cardholder’s Expensify email address.
+- **Transaction ID:** Unique ID for locating transactions and assisting support.
![Expanded card settlement that shows the various items that make up each card settlement.](https://help.expensify.com/assets/images/ExpensifyHelp_SettlementExpanded.png){:width="100%"}
-The Expensify Card statement only shows payments from existing Business Bank Accounts under Settings > Account > Payments > Business Accounts. If a Business Account is deleted, the statement won't contain data for payments from that account.
-
-## Exporting your statement
-When using the Expensify Card, you can export your statement to a CSV with these steps:
+**Note:** The statement only includes payments from existing Business Bank Accounts under **Settings > Account > Payments > Business Accounts**. Deleted accounts' payments won't appear.
- 1. Login to your account on the web app and click on Settings > Domains > Company Cards.
- 2. Click the Reconciliation tab at the top right, then select Settlements.
- 3. Enter your desired statement dates using the Start and End fields.
- 4. Click Search to access the statement for that period.
- 5. You can view the table or select Download to export it as a CSV.
+## Exporting Statements
+1. Log in to the web app and go to **Settings > Domains > Company Cards**.
+2. Click the **Reconciliation** tab and select **Settlements**.
+3. Enter the start and end dates for your statement.
+4. Click **Search** to view the statement.
+5. Click **Download** to export it as a CSV.
![Click the Download CSV button in the middle of the page to export your card settlements.](https://help.expensify.com/assets/images/ExpensifyHelp_SettlementExport.png){:width="100%"}
## Expensify Card Settlement Frequency
-Paying your Expensify Card balance is simple with automatic settlement. There are two settlement frequency options:
- - **Daily Settlement:** Your Expensify Card balance is paid in full every business day, meaning you’ll see an itemized debit each business day.
- - **Monthly Settlement:** Expensify Cards are settled monthly, with your settlement date determined during the card activation process. With monthly, you’ll see only one itemized debit per month. (Available for Plaid-connected bank accounts with no recent negative balance.)
+- **Daily Settlement:** Balance paid in full every business day with an itemized debit each day.
+- **Monthly Settlement:** Balance settled monthly on a predetermined date with one itemized debit per month (available for Plaid-connected accounts with no recent negative balance).
-## How settlement works
-Each business day (Monday through Friday, excluding US bank holidays) or on your monthly settlement date, we calculate the total of posted Expensify Card transactions since the last settlement. The settlement amount represents what you must pay to bring your Expensify Card balance back to $0.
+## How Settlement Works
+- Each business day or on your monthly settlement date, the total of posted transactions is calculated.
+- The settlement amount is withdrawn from the Verified Business Bank Account linked to the primary domain admin, resetting your card balance to $0.
+- To change your settlement frequency or bank account, go to _**Settings > Domains > [Domain Name] > Company Cards**_, click the **Settings** tab, and select the new options from the dropdown menu. Click **Save** to confirm.
-We'll automatically withdraw this settlement amount from the Verified Business Bank Account linked to the primary domain admin. You can set up this bank account in the web app under Settings > Account > Payments > Bank Accounts.
+![Change your card settlement account or settlement frequency via the dropdown menus in the middle of the screen.](https://help.expensify.com/assets/images/ExpensifyHelp_CardSettings.png){:width="100%"}
-Once the payment is made, your Expensify Card balance will be $0, and the transactions are considered "settled."
-To change your settlement frequency or bank account, go to Settings > Domains > [Domain Name] > Company Cards. On the Company Cards page, click the Settings tab, choose a new settlement frequency or account from the dropdown menu, and click Save to confirm the change.
+# FAQ
-![Change your card settlement account or settlement frequency via the dropdown menus in the middle of the screen.](https://help.expensify.com/assets/images/ExpensifyHelp_CardSettings.png){:width="100%"}
+## Can you pay your balance early if you’ve reached your Domain Limit?
+- For Monthly Settlement, use the “Settle Now” button to manually initiate settlement.
+- For Daily Settlement, balances settle automatically with no additional action required.
-# Expensify Card Statement and Settlements FAQs
-## Can you pay your balance early if you've reached your Domain Limit?
-If you've chosen Monthly Settlement, you can manually initiate settlement using the "Settle Now" button. We'll settle the outstanding balance and then perform settlement again on your selected predetermined monthly settlement date.
-
-If you opt for Daily Settlement, the Expensify Card statement will automatically settle daily through an automatic withdrawal from your business bank account. No additional action is needed on your part.
-
## Will our domain limit change if our Verified Bank Account has a higher balance?
-Your domain limit may fluctuate based on your cash balance, spending patterns, and history with Expensify. Suppose you've recently transferred funds to the business bank account linked to Expensify card settlements. In that case, you should expect a change in your domain limit within 24 hours of the transfer (assuming your business bank account is connected through Plaid).
-
+Domain limits may change based on cash balance, spending patterns, and history with Expensify. If your bank account is connected through Plaid, expect changes within 24 hours of transferring funds.
+
## How is the “Amount Owed” figure on the card list calculated?
-The amount owed consists of all Expensify Card transactions, both pending and posted, since the last settlement date. The settlement amount withdrawn from your designated Verified Business Bank Account only includes posted transactions.
-
-Your amount owed decreases when the settlement clears. Any pending transactions that don't post timely will automatically expire, reducing your amount owed.
-
-## **How do I view all unsettled expenses?**
-To view unsettled expenses since the last settlement, use the Reconciliation Dashboard's Expenses tab. Follow these steps:
- 1. Note the dates of expenses in your last settlement.
- 2. Switch to the Expenses tab on the Reconciliation Dashboard.
- 3. Set the start date just after the last settled expenses and the end date to today.
- 4. The Imported Total will show the outstanding amount, and you can click through to view individual expenses.
+It includes all pending and posted transactions since the last settlement date. The settlement amount withdrawn only includes posted transactions.
+
+## How do I view all unsettled expenses?
+1. Note the dates of expenses in your last settlement.
+2. Go to the **Expenses** tab on the Reconciliation Dashboard.
+3. Set the start date after the last settled expenses and the end date to today.
+4. The **Imported Total** shows the outstanding amount, and you can click to view individual expenses.
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 881a91fcee5d..2ccce98e6a21 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.5.5
+ 9.0.5.12
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index dd3dbc8264da..8248e7db0454 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.5.5
+ 9.0.5.12
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index c75fa81b19e1..87cdb420af38 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.5
CFBundleVersion
- 9.0.5.5
+ 9.0.5.12
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index a5ffdcb4b63c..29ab90c4b7db 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -282,7 +282,7 @@ PODS:
- nanopb/encode (= 2.30908.0)
- nanopb/decode (2.30908.0)
- nanopb/encode (2.30908.0)
- - Onfido (29.7.1)
+ - Onfido (29.7.2)
- onfido-react-native-sdk (10.6.0):
- glog
- hermes-engine
@@ -1871,7 +1871,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.88):
+ - RNLiveMarkdown (0.1.91):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1889,9 +1889,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNLiveMarkdown/common (= 0.1.88)
+ - RNLiveMarkdown/common (= 0.1.91)
- Yoga
- - RNLiveMarkdown/common (0.1.88):
+ - RNLiveMarkdown/common (0.1.91):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2531,7 +2531,7 @@ SPEC CHECKSUMS:
MapboxMaps: 87ef0003e6db46e45e7a16939f29ae87e38e7ce2
MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
- Onfido: 342cbecd7a4383e98dfe7f9c35e98aaece599062
+ Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af
onfido-react-native-sdk: 3e3b0dd70afa97410fb318d54c6a415137968ef2
Plaid: 7829e84db6d766a751c91a402702946d2977ddcb
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
@@ -2614,7 +2614,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: e33d2c97863d5480f8f4b45f8b25f801cc43c7f5
+ RNLiveMarkdown: 24fbb7370eefee2f325fb64cfe904b111ffcd81b
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c
RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216
@@ -2631,7 +2631,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055
- Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70
+ Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312
PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14
diff --git a/package-lock.json b/package-lock.json
index 2871cf3c1bb0..c401dfe77198 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "new.expensify",
- "version": "9.0.5-5",
+ "version": "9.0.5-12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.5-5",
+ "version": "9.0.5-12",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.88",
+ "@expensify/react-native-live-markdown": "0.1.91",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -55,7 +55,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "2.0.19",
+ "expensify-common": "2.0.26",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -79,7 +79,7 @@
"react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
- "react-fast-pdf": "1.0.13",
+ "react-fast-pdf": "1.0.14",
"react-map-gl": "^7.1.3",
"react-native": "0.73.4",
"react-native-android-location-enabler": "^2.0.1",
@@ -3784,9 +3784,9 @@
}
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.88",
- "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.88.tgz",
- "integrity": "sha512-78X5ACV+OL+aL6pfJAXyHkNuMGUc4Rheo4qLkIwLpmUIAiAxmY0B2lch5XHSNGf1a5ofvVbdQ6kl84+4E6DwlQ==",
+ "version": "0.1.91",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.91.tgz",
+ "integrity": "sha512-6uQTgwhpvLqQKdtNqSgh45sRuQRXzv/WwyhdvQNge6EYtulyGFqT82GIP+LIGW8Xnl73nzFZTuMKwWxFFR/Cow==",
"workspaces": [
"parser",
"example",
@@ -25974,9 +25974,9 @@
}
},
"node_modules/expensify-common": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.19.tgz",
- "integrity": "sha512-GdWlYiHOAapy/jxjcvL9NKGOofhoEuKIwvJNGNVHbDXcA+0NxVCNYrHt1yrLnVcE4KtK6PGT6fQ2Lp8NTCoA+g==",
+ "version": "2.0.26",
+ "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.26.tgz",
+ "integrity": "sha512-3GORs2xfx78SoKLDh4lXpk4Bx61sAVNnlo23VB803zs7qZz8/Oq3neKedtEJuRAmUps0C1Y5y9xZE8nrPO31nQ==",
"dependencies": {
"awesome-phonenumber": "^5.4.0",
"classnames": "2.5.0",
@@ -25988,7 +25988,7 @@
"prop-types": "15.8.1",
"react": "16.12.0",
"react-dom": "16.12.0",
- "semver": "^7.6.0",
+ "semver": "^7.6.2",
"simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5",
"ua-parser-js": "^1.0.37"
}
@@ -36987,9 +36987,9 @@
}
},
"node_modules/react-fast-pdf": {
- "version": "1.0.13",
- "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.13.tgz",
- "integrity": "sha512-rF7NQZ26rJAI8ysRJaG71dl2c7AIq48ibbn7xCyF3lEZ/yOjA8BeR0utRwDjaHGtswQscgETboilhaaH5UtIYg==",
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.14.tgz",
+ "integrity": "sha512-iWomykxvnZtokIKpRK5xpaRfXz9ufrY7AVANtIBYsAZtX5/7VDlpIQwieljfMZwFc96TyceCnneufsgXpykTQw==",
"dependencies": {
"react-pdf": "^7.7.0",
"react-window": "^1.8.10"
@@ -39473,11 +39473,9 @@
}
},
"node_modules/semver": {
- "version": "7.6.0",
- "license": "ISC",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+ "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"bin": {
"semver": "bin/semver.js"
},
diff --git a/package.json b/package.json
index 3ff6021ced4f..5420a3e886ef 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.5-5",
+ "version": "9.0.5-12",
"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.",
@@ -67,7 +67,7 @@
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.88",
+ "@expensify/react-native-live-markdown": "0.1.91",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -109,7 +109,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "2.0.19",
+ "expensify-common": "2.0.26",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -133,7 +133,7 @@
"react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
- "react-fast-pdf": "1.0.13",
+ "react-fast-pdf": "1.0.14",
"react-map-gl": "^7.1.3",
"react-native": "0.73.4",
"react-native-android-location-enabler": "^2.0.1",
diff --git a/patches/@expensify+react-native-live-markdown+0.1.85.patch b/patches/@expensify+react-native-live-markdown+0.1.85.patch
deleted file mode 100644
index f745786a088e..000000000000
--- a/patches/@expensify+react-native-live-markdown+0.1.85.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js b/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js
-index e975fb2..6a4b510 100644
---- a/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js
-+++ b/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js
-@@ -53,7 +53,7 @@ function setCursorPosition(target, start, end = null) {
- // 3. Caret at the end of whole input, when pressing enter
- // 4. All other placements
- if (prevChar === '\n' && prevTextLength !== undefined && prevTextLength < textCharacters.length) {
-- if (nextChar !== '\n') {
-+ if (nextChar !== '\n' && i !== n - 1 && nextChar) {
- range.setStart(textNodes[i + 1], 0);
- } else if (i !== textNodes.length - 1) {
- range.setStart(textNodes[i], 1);
diff --git a/patches/@expensify+react-native-live-markdown+0.1.91.patch b/patches/@expensify+react-native-live-markdown+0.1.91.patch
new file mode 100644
index 000000000000..c77e46accae3
--- /dev/null
+++ b/patches/@expensify+react-native-live-markdown+0.1.91.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts b/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts
+index 1cda659..ba5c3c3 100644
+--- a/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts
++++ b/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts
+@@ -66,7 +66,7 @@ function setCursorPosition(target: HTMLElement, start: number, end: number | nul
+ // 3. Caret at the end of whole input, when pressing enter
+ // 4. All other placements
+ if (prevChar === '\n' && prevTextLength !== undefined && prevTextLength < textCharacters.length) {
+- if (nextChar !== '\n') {
++ if (nextChar && nextChar !== '\n' && i !== n - 1) {
+ range.setStart(textNodes[i + 1] as Node, 0);
+ } else if (i !== textNodes.length - 1) {
+ range.setStart(textNodes[i] as Node, 1);
diff --git a/patches/react-native-keyboard-controller+1.12.2.patch.patch b/patches/react-native-keyboard-controller+1.12.2.patch
similarity index 100%
rename from patches/react-native-keyboard-controller+1.12.2.patch.patch
rename to patches/react-native-keyboard-controller+1.12.2.patch
diff --git a/patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.8.1+001+fix-boost-dependency.patch
similarity index 100%
rename from patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch
rename to patches/react-native-reanimated+3.8.1+001+fix-boost-dependency.patch
diff --git a/patches/react-native-reanimated+3.7.2+002+copy-state.patch b/patches/react-native-reanimated+3.8.1+002+copy-state.patch
similarity index 100%
rename from patches/react-native-reanimated+3.7.2+002+copy-state.patch
rename to patches/react-native-reanimated+3.8.1+002+copy-state.patch
diff --git a/patches/react-native-reanimated+3.7.2.patch b/patches/react-native-reanimated+3.8.1.patch
similarity index 100%
rename from patches/react-native-reanimated+3.7.2.patch
rename to patches/react-native-reanimated+3.8.1.patch
diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh
new file mode 100755
index 000000000000..a4be88984561
--- /dev/null
+++ b/scripts/applyPatches.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+
+# This script is a simple wrapper around patch-package that fails if any errors or warnings are detected.
+# This is useful because patch-package does not fail on errors or warnings by default,
+# which means that broken patches are easy to miss, and leads to developer frustration and wasted time.
+
+SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}")
+source "$SCRIPTS_DIR/shellUtils.sh"
+
+# Wrapper to run patch-package.
+# We use `script` to preserve colorization when the output of patch-package is piped to tee
+# and we provide /dev/null to discard the output rather than sending it to a file
+# `script` has different syntax on macOS vs linux, so that's why we need a wrapper function
+function patchPackage {
+ OS="$(uname)"
+ if [[ "$OS" == "Darwin" ]]; then
+ # macOS
+ script -q /dev/null npx patch-package --error-on-fail
+ elif [[ "$OS" == "Linux" ]]; then
+ # Ubuntu/Linux
+ script -q -c "npx patch-package --error-on-fail" /dev/null
+ else
+ error "Unsupported OS: $OS"
+ fi
+}
+
+# Run patch-package and capture its output and exit code, while still displaying the original output to the terminal
+# (we use `script -q /dev/null` to preserve colorization in the output)
+TEMP_OUTPUT="$(mktemp)"
+patchPackage 2>&1 | tee "$TEMP_OUTPUT"
+EXIT_CODE=${PIPESTATUS[0]}
+OUTPUT="$(cat "$TEMP_OUTPUT")"
+rm -f "$TEMP_OUTPUT"
+
+# Check if the output contains a warning message
+echo "$OUTPUT" | grep -q "Warning:"
+WARNING_FOUND=$?
+
+printf "\n";
+
+# Determine the final exit code
+if [ "$EXIT_CODE" -eq 0 ]; then
+ if [ $WARNING_FOUND -eq 0 ]; then
+ # patch-package succeeded but warning was found
+ error "It looks like you upgraded a dependency without upgrading the patch. Please review the patch, determine if it's still needed, and port it to the new version of the dependency."
+ exit 1
+ else
+ # patch-package succeeded and no warning was found
+ success "patch-package succeeded without errors or warnings"
+ exit 0
+ fi
+else
+ # patch-package failed
+ error "patch-package failed to apply a patch"
+ exit "$EXIT_CODE"
+fi
diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh
index 339fdf25cb10..782c8ef5822c 100755
--- a/scripts/postInstall.sh
+++ b/scripts/postInstall.sh
@@ -1,11 +1,14 @@
#!/bin/bash
+# Exit immediately if any command exits with a non-zero status
+set -e
+
# Go to project root
ROOT_DIR=$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)")
cd "$ROOT_DIR" || exit 1
-# Run patch-package
-npx patch-package
+# Apply packages using patch-package
+scripts/applyPatches.sh
# Install node_modules in subpackages, unless we're in a CI/CD environment,
# where the node_modules for subpackages are cached separately.
diff --git a/src/App.tsx b/src/App.tsx
index 21025d34a661..98b5d4afeb1d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -20,6 +20,7 @@ import OnyxProvider from './components/OnyxProvider';
import PopoverContextProvider from './components/PopoverProvider';
import SafeArea from './components/SafeArea';
import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider';
+import {SearchContextProvider} from './components/Search/SearchContext';
import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider';
import ThemeProvider from './components/ThemeProvider';
import ThemeStylesProvider from './components/ThemeStylesProvider';
@@ -91,6 +92,7 @@ function App({url}: AppProps) {
VolumeContextProvider,
VideoPopoverMenuContextProvider,
KeyboardProvider,
+ SearchContextProvider,
]}
>
diff --git a/src/CONST.ts b/src/CONST.ts
index 13d44ee883be..42be5a24cca3 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -609,6 +609,7 @@ const CONST = {
TRAVEL_TERMS_URL: `${USE_EXPENSIFY_URL}/travelterms`,
EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT: 'https://www.expensify.com/tools/integrations/downloadPackage',
EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT_FILE_NAME: 'ExpensifyPackageForSageIntacct',
+ SAGE_INTACCT_INSTRUCTIONS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct',
HOW_TO_CONNECT_TO_SAGE_INTACCT: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct#how-to-connect-to-sage-intacct',
PRICING: `https://www.expensify.com/pricing`,
@@ -1345,18 +1346,75 @@ const CONST = {
},
},
+ SAGE_INTACCT_MAPPING_VALUE: {
+ NONE: 'NONE',
+ DEFAULT: 'DEFAULT',
+ TAG: 'TAG',
+ REPORT_FIELD: 'REPORT_FIELD',
+ },
+
+ SAGE_INTACCT_CONFIG: {
+ MAPPINGS: {
+ DEPARTMENTS: 'departments',
+ CLASSES: 'classes',
+ LOCATIONS: 'locations',
+ CUSTOMERS: 'customers',
+ PROJECTS: 'projects',
+ },
+ SYNC_ITEMS: 'syncItems',
+ TAX: 'tax',
+ EXPORT: 'export',
+ EXPORT_DATE: 'exportDate',
+ NON_REIMBURSABLE_CREDIT_CARD_VENDOR: 'nonReimbursableCreditCardChargeDefaultVendor',
+ NON_REIMBURSABLE_VENDOR: 'nonReimbursableVendor',
+ REIMBURSABLE_VENDOR: 'reimbursableExpenseReportDefaultVendor',
+ NON_REIMBURSABLE_ACCOUNT: 'nonReimbursableAccount',
+ NON_REIMBURSABLE: 'nonReimbursable',
+ EXPORTER: 'exporter',
+ REIMBURSABLE: 'reimbursable',
+ AUTO_SYNC: 'autoSync',
+ AUTO_SYNC_ENABLED: 'enabled',
+ IMPORT_EMPLOYEES: 'importEmployees',
+ APPROVAL_MODE: 'approvalMode',
+ SYNC: 'sync',
+ SYNC_REIMBURSED_REPORTS: 'syncReimbursedReports',
+ REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID',
+ },
+
+ SAGE_INTACCT: {
+ APPROVAL_MODE: {
+ APPROVAL_MANUAL: 'APPROVAL_MANUAL',
+ },
+ },
+
QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE: {
VENDOR_BILL: 'bill',
CHECK: 'check',
JOURNAL_ENTRY: 'journal_entry',
},
+ SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE: {
+ EXPENSE_REPORT: 'EXPENSE_REPORT',
+ VENDOR_BILL: 'VENDOR_BILL',
+ },
+
+ SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE: {
+ CREDIT_CARD_CHARGE: 'CREDIT_CARD_CHARGE',
+ VENDOR_BILL: 'VENDOR_BILL',
+ },
+
XERO_EXPORT_DATE: {
LAST_EXPENSE: 'LAST_EXPENSE',
REPORT_EXPORTED: 'REPORT_EXPORTED',
REPORT_SUBMITTED: 'REPORT_SUBMITTED',
},
+ SAGE_INTACCT_EXPORT_DATE: {
+ LAST_EXPENSE: 'LAST_EXPENSE',
+ EXPORTED: 'EXPORTED',
+ SUBMITTED: 'SUBMITTED',
+ },
+
NETSUITE_CONFIG: {
SUBSIDIARY: 'subsidiary',
EXPORTER: 'exporter',
@@ -1389,7 +1447,12 @@ const CONST = {
3: 'createAccessToken',
4: 'enterCredentials',
},
- IMPORT_CUSTOM_FIELDS: ['customSegments', 'customLists'],
+ IMPORT_CUSTOM_FIELDS: {
+ CUSTOM_SEGMENTS: 'customSegments',
+ CUSTOM_LISTS: 'customLists',
+ },
+ CUSTOM_SEGMENT_FIELDS: ['segmentName', 'internalID', 'scriptID', 'mapping'],
+ CUSTOM_LIST_FIELDS: ['listName', 'internalID', 'transactionFieldID', 'mapping'],
CUSTOM_FORM_ID_TYPE: {
REIMBURSABLE: 'reimbursable',
NON_REIMBURSABLE: 'nonReimbursable',
@@ -1408,6 +1471,40 @@ const CONST = {
JOBS: 'jobs',
},
},
+ NETSUITE_CUSTOM_LIST_LIMIT: 8,
+ NETSUITE_ADD_CUSTOM_LIST_STEP_NAMES: ['1', '2,', '3', '4'],
+ NETSUITE_ADD_CUSTOM_SEGMENT_STEP_NAMES: ['1', '2,', '3', '4', '5', '6,'],
+ },
+
+ NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES: {
+ CUSTOM_LISTS: {
+ CUSTOM_LIST_PICKER: 0,
+ TRANSACTION_FIELD_ID: 1,
+ MAPPING: 2,
+ CONFIRM: 3,
+ },
+ CUSTOM_SEGMENTS: {
+ SEGMENT_TYPE: 0,
+ SEGMENT_NAME: 1,
+ INTERNAL_ID: 2,
+ SCRIPT_ID: 3,
+ MAPPING: 4,
+ CONFIRM: 5,
+ },
+ },
+
+ NETSUITE_CUSTOM_RECORD_TYPES: {
+ CUSTOM_SEGMENT: 'customSegment',
+ CUSTOM_RECORD: 'customRecord',
+ },
+
+ NETSUITE_FORM_STEPS_HEADER_HEIGHT: 40,
+
+ NETSUITE_IMPORT: {
+ HELP_LINKS: {
+ CUSTOM_SEGMENTS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite#custom-segments',
+ CUSTOM_LISTS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite#custom-lists',
+ },
},
NETSUITE_EXPORT_DATE: {
@@ -2130,6 +2227,10 @@ const CONST = {
CARD_NAME: 'CardName',
CONFIRMATION: 'Confirmation',
},
+ CARD_TYPE: {
+ PHYSICAL: 'physical',
+ VIRTUAL: 'virtual',
+ },
},
AVATAR_ROW_SIZE: {
DEFAULT: 4,
@@ -3796,6 +3897,7 @@ const CONST = {
},
EVENTS: {
SCROLLING: 'scrolling',
+ ON_RETURN_TO_OLD_DOT: 'onReturnToOldDot',
},
CHAT_HEADER_LOADER_HEIGHT: 36,
@@ -5062,6 +5164,9 @@ const CONST = {
DONE: 'done',
PAID: 'paid',
VIEW: 'view',
+ REVIEW: 'review',
+ HOLD: 'hold',
+ UNHOLD: 'unhold',
},
TRANSACTION_TYPE: {
CASH: 'cash',
@@ -5159,6 +5264,10 @@ const CONST = {
DATE: 'date',
LIST: 'dropdown',
},
+
+ NAVIGATION_ACTIONS: {
+ RESET: 'RESET',
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 5d6b5492d15c..bd4b294a6d68 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -320,6 +320,9 @@ const ONYXKEYS = {
/** Onboarding Purpose selected by the user during Onboarding flow */
ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected',
+ /** Onboarding error message to be displayed to the user */
+ ONBOARDING_ERROR_MESSAGE: 'onboardingErrorMessage',
+
/** Onboarding policyID selected by the user during Onboarding flow */
ONBOARDING_POLICY_ID: 'onboardingPolicyID',
@@ -460,8 +463,8 @@ const ONYXKEYS = {
WORKSPACE_RATE_AND_UNIT_FORM_DRAFT: 'workspaceRateAndUnitFormDraft',
WORKSPACE_TAX_CUSTOM_NAME: 'workspaceTaxCustomName',
WORKSPACE_TAX_CUSTOM_NAME_DRAFT: 'workspaceTaxCustomNameDraft',
- WORKSPACE_REPORT_FIELDS_FORM: 'workspaceReportFieldsForm',
- WORKSPACE_REPORT_FIELDS_FORM_DRAFT: 'workspaceReportFieldsFormDraft',
+ WORKSPACE_REPORT_FIELDS_FORM: 'workspaceReportFieldForm',
+ WORKSPACE_REPORT_FIELDS_FORM_DRAFT: 'workspaceReportFieldFormDraft',
POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm',
POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft',
POLICY_DISTANCE_RATE_EDIT_FORM: 'policyDistanceRateEditForm',
@@ -556,14 +559,22 @@ const ONYXKEYS = {
NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft',
SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm',
SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft',
- ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCardForm',
- ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardFormDraft',
+ ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard',
+ ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft',
SAGE_INTACCT_CREDENTIALS_FORM: 'sageIntacctCredentialsForm',
SAGE_INTACCT_CREDENTIALS_FORM_DRAFT: 'sageIntacctCredentialsFormDraft',
+ NETSUITE_CUSTOM_FIELD_FORM: 'netSuiteCustomFieldForm',
+ NETSUITE_CUSTOM_FIELD_FORM_DRAFT: 'netSuiteCustomFieldFormDraft',
+ NETSUITE_CUSTOM_SEGMENT_ADD_FORM: 'netSuiteCustomSegmentAddForm',
+ NETSUITE_CUSTOM_SEGMENT_ADD_FORM_DRAFT: 'netSuiteCustomSegmentAddFormDraft',
+ NETSUITE_CUSTOM_LIST_ADD_FORM: 'netSuiteCustomListAddForm',
+ NETSUITE_CUSTOM_LIST_ADD_FORM_DRAFT: 'netSuiteCustomListAddFormDraft',
NETSUITE_TOKEN_INPUT_FORM: 'netsuiteTokenInputForm',
NETSUITE_TOKEN_INPUT_FORM_DRAFT: 'netsuiteTokenInputFormDraft',
NETSUITE_CUSTOM_FORM_ID_FORM: 'netsuiteCustomFormIDForm',
NETSUITE_CUSTOM_FORM_ID_FORM_DRAFT: 'netsuiteCustomFormIDFormDraft',
+ SAGE_INTACCT_DIMENSION_TYPE_FORM: 'sageIntacctDimensionTypeForm',
+ SAGE_INTACCT_DIMENSION_TYPE_FORM_DRAFT: 'sageIntacctDimensionTypeFormDraft',
},
} as const;
@@ -576,7 +587,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm;
[ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName;
- [ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldsForm;
+ [ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
[ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
[ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm;
@@ -627,8 +638,12 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm;
[ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm;
[ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm;
+ [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm;
+ [ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm;
+ [ONYXKEYS.FORMS.NETSUITE_CUSTOM_SEGMENT_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm;
[ONYXKEYS.FORMS.NETSUITE_TOKEN_INPUT_FORM]: FormTypes.NetSuiteTokenInputForm;
[ONYXKEYS.FORMS.NETSUITE_CUSTOM_FORM_ID_FORM]: FormTypes.NetSuiteCustomFormIDForm;
+ [ONYXKEYS.FORMS.SAGE_INTACCT_DIMENSION_TYPE_FORM]: FormTypes.SageIntacctDimensionForm;
};
type OnyxFormDraftValuesMapping = {
@@ -782,6 +797,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.MAX_CANVAS_HEIGHT]: number;
[ONYXKEYS.MAX_CANVAS_WIDTH]: number;
[ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string;
+ [ONYXKEYS.ONBOARDING_ERROR_MESSAGE]: string;
[ONYXKEYS.ONBOARDING_POLICY_ID]: string;
[ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string;
[ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index d548297cb854..a54bb4f5cca5 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -3,6 +3,7 @@ import type CONST from './CONST';
import type {IOUAction, IOUType} from './CONST';
import type {IOURequestType} from './libs/actions/IOU';
import type {AuthScreensParamList} from './libs/Navigation/types';
+import type {SageIntacctMappingName} from './types/onyx/Policy';
import type {SearchQuery} from './types/onyx/SearchResults';
import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
@@ -53,6 +54,11 @@ const ROUTES = {
getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const,
},
+ TRANSACTION_HOLD_REASON_RHP: {
+ route: '/search/:query/hold/:transactionID',
+ getRoute: (query: string, transactionID: string) => `search/${query}/hold/${transactionID}` as const,
+ },
+
// This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated
CONCIERGE: 'concierge',
FLAG_COMMENT: {
@@ -787,41 +793,43 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/reportFields',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields` as const,
},
- WORKSPACE_REPORT_FIELD_SETTINGS: {
- route: 'settings/workspaces/:policyID/reportField/:reportFieldKey',
- getRoute: (policyID: string, reportFieldKey: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldKey)}` as const,
- },
WORKSPACE_CREATE_REPORT_FIELD: {
route: 'settings/workspaces/:policyID/reportFields/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new` as const,
},
+ WORKSPACE_REPORT_FIELD_SETTINGS: {
+ route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit',
+ getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit` as const,
+ },
WORKSPACE_REPORT_FIELD_LIST_VALUES: {
- route: 'settings/workspaces/:policyID/reportFields/new/listValues',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new/listValues` as const,
+ route: 'settings/workspaces/:policyID/reportField/listValues/:reportFieldID?',
+ getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const,
},
WORKSPACE_REPORT_FIELD_ADD_VALUE: {
- route: 'settings/workspaces/:policyID/reportFields/new/addValue',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new/addValue` as const,
+ route: 'settings/workspaces/:policyID/reportField/addValue/:reportFieldID?',
+ getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const,
},
WORKSPACE_REPORT_FIELD_VALUE_SETTINGS: {
- route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex',
- getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}` as const,
+ route: 'settings/workspaces/:policyID/reportField/:valueIndex/:reportFieldID?',
+ getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) =>
+ `settings/workspaces/${policyID}/reportField/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const,
},
WORKSPACE_REPORT_FIELD_EDIT_VALUE: {
- route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit',
- getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}/edit` as const,
+ route: 'settings/workspaces/:policyID/reportField/new/:valueIndex/edit',
+ getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportField/new/${valueIndex}/edit` as const,
+ },
+ WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE: {
+ route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit/initialValue',
+ getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const,
},
WORKSPACE_EXPENSIFY_CARD: {
route: 'settings/workspaces/:policyID/expensify-card',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const,
},
- // TODO: uncomment after development is done
- // WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: {
- // route: 'settings/workspaces/:policyID/expensify-card/issues-new',
- // getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const,
- // },
- // TODO: remove after development is done - this one is for testing purposes
- WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: 'settings/workspaces/expensify-card/issue-new',
+ WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: {
+ route: 'settings/workspaces/:policyID/expensify-card/issue-new',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const,
+ },
WORKSPACE_DISTANCE_RATES: {
route: 'settings/workspaces/:policyID/distance-rates',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const,
@@ -1009,6 +1017,29 @@ const ROUTES = {
getRoute: (policyID: string, importField: TupleToUnion) =>
`settings/workspaces/${policyID}/accounting/netsuite/import/mapping/${importField}` as const,
},
+ POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField',
+ getRoute: (policyID: string, importCustomField: ValueOf) =>
+ `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_VIEW: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField/view/:valueIndex',
+ getRoute: (policyID: string, importCustomField: ValueOf, valueIndex: number) =>
+ `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}/view/${valueIndex}` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_EDIT: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField/edit/:valueIndex/:fieldName',
+ getRoute: (policyID: string, importCustomField: ValueOf, valueIndex: number, fieldName: string) =>
+ `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}/edit/${valueIndex}/${fieldName}` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_ADD: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom-list/new',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/custom-list/new` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom-segment/new',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/custom-segment/new` as const,
+ },
POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS: {
route: 'settings/workspaces/:policyID/accounting/netsuite/import/customer-projects',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/customer-projects` as const,
@@ -1119,6 +1150,66 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/sage-intacct/existing-connections',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/existing-connections` as const,
},
+ POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/import',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/toggle-mapping/:mapping',
+ getRoute: (policyID: string, mapping: SageIntacctMappingName) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/toggle-mapping/${mapping}` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/mapping-type/:mapping',
+ getRoute: (policyID: string, mapping: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/mapping-type/${mapping}` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/user-dimensions',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/user-dimensions` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_ADD_USER_DIMENSION: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/add-user-dimension',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/add-user-dimension` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_EDIT_USER_DIMENSION: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/edit-user-dimension/:dimensionName',
+ getRoute: (policyID: string, dimensionName: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/edit-user-dimension/${dimensionName}` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/export',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_PREFERRED_EXPORTER: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/preferred-exporter',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/preferred-exporter` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT_DATE: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/date',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/date` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/reimbursable',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/reimbursable` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/nonreimbursable',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/nonreimbursable` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/:reimbursable/default-vendor',
+ getRoute: (policyID: string, reimbursable: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/${reimbursable}/default-vendor` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/nonreimbursable/credit-card-account',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/nonreimbursable/credit-card-account` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced` as const,
+ },
+ POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced/payment-account',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced/payment-account` as const,
+ },
} as const;
/**
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index e2e3f7acc5b0..d2a6b7c19ddd 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -30,6 +30,7 @@ const SCREENS = {
SEARCH: {
CENTRAL_PANE: 'Search_Central_Pane',
REPORT_RHP: 'Search_Report_RHP',
+ TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
BOTTOM_TAB: 'Search_Bottom_Tab',
},
SETTINGS: {
@@ -280,6 +281,11 @@ const SCREENS = {
XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector',
XERO_EXPORT_BANK_ACCOUNT_SELECT: 'Policy_Accounting_Xero_Export_Bank_Account_Select',
NETSUITE_IMPORT_MAPPING: 'Policy_Accounting_NetSuite_Import_Mapping',
+ NETSUITE_IMPORT_CUSTOM_FIELD: 'Policy_Accounting_NetSuite_Import_Custom_Field',
+ NETSUITE_IMPORT_CUSTOM_FIELD_VIEW: 'Policy_Accounting_NetSuite_Import_Custom_Field_View',
+ NETSUITE_IMPORT_CUSTOM_FIELD_EDIT: 'Policy_Accounting_NetSuite_Import_Custom_Field_Edit',
+ NETSUITE_IMPORT_CUSTOM_LIST_ADD: 'Policy_Accounting_NetSuite_Import_Custom_List_Add',
+ NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD: 'Policy_Accounting_NetSuite_Import_Custom_Segment_Add',
NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS: 'Policy_Accounting_NetSuite_Import_CustomersOrProjects',
NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT: 'Policy_Accounting_NetSuite_Import_CustomersOrProjects_Select',
NETSUITE_TOKEN_INPUT: 'Policy_Accounting_NetSuite_Token_Input',
@@ -309,6 +315,21 @@ const SCREENS = {
SAGE_INTACCT_PREREQUISITES: 'Policy_Accounting_Sage_Intacct_Prerequisites',
ENTER_SAGE_INTACCT_CREDENTIALS: 'Policy_Enter_Sage_Intacct_Credentials',
EXISTING_SAGE_INTACCT_CONNECTIONS: 'Policy_Existing_Sage_Intacct_Connections',
+ SAGE_INTACCT_IMPORT: 'Policy_Accounting_Sage_Intacct_Import',
+ SAGE_INTACCT_TOGGLE_MAPPING: 'Policy_Accounting_Sage_Intacct_Toggle_Mapping',
+ SAGE_INTACCT_MAPPING_TYPE: 'Policy_Accounting_Sage_Intacct_Mapping_Type',
+ SAGE_INTACCT_USER_DIMENSIONS: 'Policy_Accounting_Sage_Intacct_User_Dimensions',
+ SAGE_INTACCT_ADD_USER_DIMENSION: 'Policy_Accounting_Sage_Intacct_Add_User_Dimension',
+ SAGE_INTACCT_EDIT_USER_DIMENSION: 'Policy_Accounting_Sage_Intacct_Edit_User_Dimension',
+ SAGE_INTACCT_EXPORT: 'Policy_Accounting_Sage_Intacct_Export',
+ SAGE_INTACCT_PREFERRED_EXPORTER: 'Policy_Accounting_Sage_Intacct_Preferred_Exporter',
+ SAGE_INTACCT_EXPORT_DATE: 'Policy_Accounting_Sage_Intacct_Export_Date',
+ SAGE_INTACCT_REIMBURSABLE_EXPENSES: 'Policy_Accounting_Sage_Intacct_Reimbursable_Expenses',
+ SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Expenses',
+ SAGE_INTACCT_DEFAULT_VENDOR: 'Policy_Accounting_Sage_Intacct_Default_Vendor',
+ 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',
},
INITIAL: 'Workspace_Initial',
PROFILE: 'Workspace_Profile',
@@ -338,6 +359,7 @@ const SCREENS = {
REPORT_FIELDS_ADD_VALUE: 'Workspace_ReportFields_AddValue',
REPORT_FIELDS_VALUE_SETTINGS: 'Workspace_ReportFields_ValueSettings',
REPORT_FIELDS_EDIT_VALUE: 'Workspace_ReportFields_EditValue',
+ REPORT_FIELDS_EDIT_INITIAL_VALUE: 'Workspace_ReportFields_EditInitialValue',
TAX_EDIT: 'Workspace_Tax_Edit',
TAX_NAME: 'Workspace_Tax_Name',
TAX_VALUE: 'Workspace_Tax_Value',
diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx
index bd9b623bcfb4..2b2c53eaaa18 100644
--- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx
+++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx
@@ -13,6 +13,7 @@ import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {removePolicyConnection} from '@libs/actions/connections';
import getQuickBooksOnlineSetupLink from '@libs/actions/connections/QuickBooksOnline';
+import * as PolicyAction from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Session} from '@src/types/onyx';
@@ -49,6 +50,8 @@ function ConnectToQuickbooksOnlineButton({
setIsDisconnectModalOpen(true);
return;
}
+ // Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO
+ PolicyAction.enablePolicyTaxes(policyID, false);
setWebViewOpen(true);
}}
text={translate('workspace.accounting.setup')}
@@ -59,6 +62,8 @@ function ConnectToQuickbooksOnlineButton({
{shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect && isDisconnectModalOpen && (
{
+ // Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO
+ PolicyAction.enablePolicyTaxes(policyID, false);
removePolicyConnection(policyID, integrationToDisconnect);
setIsDisconnectModalOpen(false);
setWebViewOpen(true);
diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx
index 4bcfdc61077f..3809b4f4f110 100644
--- a/src/components/ConnectionLayout.tsx
+++ b/src/components/ConnectionLayout.tsx
@@ -61,8 +61,8 @@ type ConnectionLayoutProps = {
/** Name of the current connection */
connectionName: ConnectionName;
- /** Block the screen when the connection is not empty */
- reverseConnectionEmptyCheck?: boolean;
+ /** Whether the screen should load for an empty connection */
+ shouldLoadForEmptyConnection?: boolean;
/** Handler for back button press */
onBackButtonPress?: () => void;
@@ -100,7 +100,7 @@ function ConnectionLayout({
shouldUseScrollView = true,
headerTitleAlreadyTranslated,
titleAlreadyTranslated,
- reverseConnectionEmptyCheck = false,
+ shouldLoadForEmptyConnection = false,
onBackButtonPress = () => Navigation.goBack(),
shouldBeBlocked = false,
}: ConnectionLayoutProps) {
@@ -122,12 +122,14 @@ function ConnectionLayout({
[title, titleStyle, children, titleAlreadyTranslated],
);
+ const shouldBlockByConnection = shouldLoadForEmptyConnection ? !isConnectionEmpty : isConnectionEmpty;
+
return (
{
+ if (ReportActionComposeFocusManager.isFocused()) {
+ return false;
+ }
+ return element;
+ },
}}
>
{children}
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index a4b9df5916af..793154d95b02 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -73,6 +73,9 @@ type FormProviderProps = FormProvider
/** Whether to apply flex to the submit button */
submitFlexEnabled?: boolean;
+
+ /** Whether button is disabled */
+ isSubmitDisabled?: boolean;
};
function FormProvider(
diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx
index 5c74fd466a15..77ef44343792 100644
--- a/src/components/Form/FormWrapper.tsx
+++ b/src/components/Form/FormWrapper.tsx
@@ -38,6 +38,9 @@ type FormWrapperProps = ChildrenProps &
/** Assuming refs are React refs */
inputRefs: RefObject;
+ /** Whether the submit button is disabled */
+ isSubmitDisabled?: boolean;
+
/** Callback to submit the form */
onSubmit: () => void;
};
@@ -57,9 +60,11 @@ function FormWrapper({
enabledWhenOffline,
isSubmitActionDangerous = false,
formID,
+ shouldUseScrollView = true,
scrollContextEnabled = false,
shouldHideFixErrorsAlert = false,
disablePressOnEnter = true,
+ isSubmitDisabled = false,
}: FormWrapperProps) {
const styles = useThemeStyles();
const formRef = useRef(null);
@@ -108,6 +113,7 @@ function FormWrapper({
{isSubmitButtonVisible && (
{({safeAreaPaddingBottomStyle}) =>
diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx
index b5535a2fe6c1..c966dd4456e9 100644
--- a/src/components/Form/InputWrapper.tsx
+++ b/src/components/Form/InputWrapper.tsx
@@ -54,7 +54,7 @@ function computeComponentSpecificRegistrationParams({
shouldSetTouchedOnBlurOnly: false,
// Forward the originally provided value
blurOnSubmit,
- shouldSubmitForm: false,
+ shouldSubmitForm: !!shouldSubmitForm,
};
}
diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts
index afbe2bb124b5..5f56bbeceea6 100644
--- a/src/components/Form/types.ts
+++ b/src/components/Form/types.ts
@@ -20,6 +20,10 @@ import type TextInput from '@components/TextInput';
import type TextPicker from '@components/TextPicker';
import type ValuePicker from '@components/ValuePicker';
import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker';
+import type DimensionTypeSelector from '@pages/workspace/accounting/intacct/import/DimensionTypeSelector';
+import type NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker';
+import type NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker';
+import type NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm';
import type {Country} from '@src/CONST';
import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS';
import type {BaseForm} from '@src/types/form/Form';
@@ -39,6 +43,7 @@ type ValidInputs =
| typeof CurrencySelector
| typeof AmountForm
| typeof BusinessTypePicker
+ | typeof DimensionTypeSelector
| typeof StateSelector
| typeof RoomNameInput
| typeof ValuePicker
@@ -47,7 +52,10 @@ type ValidInputs =
| typeof AmountPicker
| typeof TextPicker
| typeof AddPlaidBankAccount
- | typeof EmojiPickerButtonDropdown;
+ | typeof EmojiPickerButtonDropdown
+ | typeof NetSuiteCustomListPicker
+ | typeof NetSuiteCustomFieldMappingPicker
+ | typeof NetSuiteMenuWithTopDescriptionForm;
type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues';
type ValueTypeMap = {
@@ -126,6 +134,9 @@ type FormProps = {
/** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */
scrollContextEnabled?: boolean;
+ /** Whether to use ScrollView */
+ shouldUseScrollView?: boolean;
+
/** Container styles */
style?: StyleProp;
diff --git a/src/components/HybridAppMiddleware.tsx b/src/components/HybridAppMiddleware.tsx
deleted file mode 100644
index 5c6934f4fc3d..000000000000
--- a/src/components/HybridAppMiddleware.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import {useNavigation} from '@react-navigation/native';
-import type {StackNavigationProp} from '@react-navigation/stack';
-import React, {useCallback, useEffect, useMemo, useState} from 'react';
-import {NativeModules} from 'react-native';
-import useSplashScreen from '@hooks/useSplashScreen';
-import BootSplash from '@libs/BootSplash';
-import Log from '@libs/Log';
-import Navigation from '@libs/Navigation/Navigation';
-import type {RootStackParamList} from '@libs/Navigation/types';
-import * as Welcome from '@userActions/Welcome';
-import CONST from '@src/CONST';
-import type {Route} from '@src/ROUTES';
-
-type HybridAppMiddlewareProps = {
- children: React.ReactNode;
-};
-
-type HybridAppMiddlewareContextType = {
- navigateToExitUrl: (exitUrl: Route) => void;
- showSplashScreenOnNextStart: () => void;
-};
-const HybridAppMiddlewareContext = React.createContext({
- navigateToExitUrl: () => {},
- showSplashScreenOnNextStart: () => {},
-});
-
-/*
- * HybridAppMiddleware is responsible for handling BootSplash visibility correctly.
- * It is crucial to make transitions between OldDot and NewDot look smooth.
- */
-function HybridAppMiddleware(props: HybridAppMiddlewareProps) {
- const {isSplashHidden, setIsSplashHidden} = useSplashScreen();
- const [startedTransition, setStartedTransition] = useState(false);
- const [finishedTransition, setFinishedTransition] = useState(false);
- const navigation = useNavigation>();
-
- /*
- * Handles navigation during transition from OldDot. For ordinary NewDot app it is just pure navigation.
- */
- const navigateToExitUrl = useCallback((exitUrl: Route) => {
- if (NativeModules.HybridAppModule) {
- setStartedTransition(true);
- Log.info(`[HybridApp] Started transition to ${exitUrl}`, true);
- }
-
- Navigation.navigate(exitUrl);
- }, []);
-
- /**
- * This function only affects iOS. If during a single app lifecycle we frequently transition from OldDot to NewDot,
- * we need to artificially show the bootsplash because the app is only booted once.
- */
- const showSplashScreenOnNextStart = useCallback(() => {
- setIsSplashHidden(false);
- setStartedTransition(false);
- setFinishedTransition(false);
- }, [setIsSplashHidden]);
-
- useEffect(() => {
- if (!finishedTransition || isSplashHidden) {
- return;
- }
-
- Log.info('[HybridApp] Finished transtion', true);
- BootSplash.hide().then(() => {
- setIsSplashHidden(true);
- Log.info('[HybridApp] Handling onboarding flow', true);
- Welcome.handleHybridAppOnboarding();
- });
- }, [finishedTransition, isSplashHidden, setIsSplashHidden]);
-
- useEffect(() => {
- if (!startedTransition) {
- return;
- }
-
- // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout.
- const timeout = setTimeout(() => {
- setFinishedTransition(true);
- }, CONST.SCREEN_TRANSITION_END_TIMEOUT);
-
- const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => {
- clearTimeout(timeout);
- setFinishedTransition(true);
- });
-
- return () => {
- clearTimeout(timeout);
- unsubscribeTransitionEnd();
- };
- }, [navigation, startedTransition]);
-
- const contextValue = useMemo(
- () => ({
- navigateToExitUrl,
- showSplashScreenOnNextStart,
- }),
- [navigateToExitUrl, showSplashScreenOnNextStart],
- );
-
- return {props.children};
-}
-
-HybridAppMiddleware.displayName = 'HybridAppMiddleware';
-
-export default HybridAppMiddleware;
-export type {HybridAppMiddlewareContextType};
-export {HybridAppMiddlewareContext};
diff --git a/src/components/HybridAppMiddleware/index.ios.tsx b/src/components/HybridAppMiddleware/index.ios.tsx
new file mode 100644
index 000000000000..5b06e5626c6e
--- /dev/null
+++ b/src/components/HybridAppMiddleware/index.ios.tsx
@@ -0,0 +1,130 @@
+import type React from 'react';
+import {useContext, useEffect, useState} from 'react';
+import {NativeEventEmitter, NativeModules} from 'react-native';
+import type {NativeModule} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import {InitialURLContext} from '@components/InitialURLContextProvider';
+import useExitTo from '@hooks/useExitTo';
+import useSplashScreen from '@hooks/useSplashScreen';
+import BootSplash from '@libs/BootSplash';
+import Log from '@libs/Log';
+import Navigation from '@libs/Navigation/Navigation';
+import * as SessionUtils from '@libs/SessionUtils';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {HybridAppRoute, Route} from '@src/ROUTES';
+
+type HybridAppMiddlewareProps = {
+ authenticated: boolean;
+ children: React.ReactNode;
+};
+
+/*
+ * HybridAppMiddleware is responsible for handling BootSplash visibility correctly.
+ * It is crucial to make transitions between OldDot and NewDot look smooth.
+ * The middleware assumes that the entry point for HybridApp is the /transition route.
+ */
+function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) {
+ const {isSplashHidden, setIsSplashHidden} = useSplashScreen();
+ const [startedTransition, setStartedTransition] = useState(false);
+ const [finishedTransition, setFinishedTransition] = useState(false);
+
+ const initialURL = useContext(InitialURLContext);
+ const exitToParam = useExitTo();
+ const [exitTo, setExitTo] = useState();
+
+ const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false});
+ const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
+
+ // In iOS, the HybridApp defines the `onReturnToOldDot` event.
+ // If we frequently transition from OldDot to NewDot during a single app lifecycle,
+ // we need to artificially display the bootsplash since the app is booted only once.
+ // Therefore, isSplashHidden needs to be updated at the appropriate time.
+ useEffect(() => {
+ if (!NativeModules.HybridAppModule) {
+ return;
+ }
+ const HybridAppEvents = new NativeEventEmitter(NativeModules.HybridAppModule as unknown as NativeModule);
+ const listener = HybridAppEvents.addListener(CONST.EVENTS.ON_RETURN_TO_OLD_DOT, () => {
+ Log.info('[HybridApp] `onReturnToOldDot` event received. Resetting state of HybridAppMiddleware', true);
+ setIsSplashHidden(false);
+ setStartedTransition(false);
+ setFinishedTransition(false);
+ setExitTo(undefined);
+ });
+
+ return () => {
+ listener.remove();
+ };
+ }, [setIsSplashHidden]);
+
+ // Save `exitTo` when we reach /transition route.
+ // `exitTo` should always exist during OldDot -> NewDot transitions.
+ useEffect(() => {
+ if (!NativeModules.HybridAppModule || !exitToParam || exitTo) {
+ return;
+ }
+
+ Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam});
+ setExitTo(exitToParam);
+
+ Log.info(`[HybridApp] Started transition`, true);
+ setStartedTransition(true);
+ }, [exitTo, exitToParam]);
+
+ useEffect(() => {
+ if (!startedTransition || finishedTransition) {
+ return;
+ }
+
+ const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL;
+ const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail);
+
+ // We need to wait with navigating to exitTo until all login-related actions are complete.
+ if (!authenticated || isLoggingInAsNewUser || isAccountLoading) {
+ return;
+ }
+
+ if (exitTo) {
+ Navigation.isNavigationReady().then(() => {
+ // We need to remove /transition from route history.
+ // `useExitTo` returns undefined for routes other than /transition.
+ if (exitToParam) {
+ Log.info('[HybridApp] Removing /transition route from history', true);
+ Navigation.goBack();
+ }
+
+ Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo});
+ Navigation.navigate(Navigation.parseHybridAppUrl(exitTo));
+ setExitTo(undefined);
+
+ setTimeout(() => {
+ Log.info('[HybridApp] Setting `finishedTransition` to true', true);
+ setFinishedTransition(true);
+ }, CONST.SCREEN_TRANSITION_END_TIMEOUT);
+ });
+ }
+ }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]);
+
+ useEffect(() => {
+ if (!finishedTransition || isSplashHidden) {
+ return;
+ }
+
+ Log.info('[HybridApp] Finished transition, hiding BootSplash', true);
+ BootSplash.hide().then(() => {
+ setIsSplashHidden(true);
+ if (authenticated) {
+ Log.info('[HybridApp] Handling onboarding flow', true);
+ Welcome.handleHybridAppOnboarding();
+ }
+ });
+ }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]);
+
+ return children;
+}
+
+HybridAppMiddleware.displayName = 'HybridAppMiddleware';
+
+export default HybridAppMiddleware;
diff --git a/src/components/HybridAppMiddleware/index.tsx b/src/components/HybridAppMiddleware/index.tsx
new file mode 100644
index 000000000000..b8c72d9200ac
--- /dev/null
+++ b/src/components/HybridAppMiddleware/index.tsx
@@ -0,0 +1,107 @@
+import type React from 'react';
+import {useContext, useEffect, useState} from 'react';
+import {NativeModules} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import {InitialURLContext} from '@components/InitialURLContextProvider';
+import useExitTo from '@hooks/useExitTo';
+import useSplashScreen from '@hooks/useSplashScreen';
+import BootSplash from '@libs/BootSplash';
+import Log from '@libs/Log';
+import Navigation from '@libs/Navigation/Navigation';
+import * as SessionUtils from '@libs/SessionUtils';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {HybridAppRoute, Route} from '@src/ROUTES';
+
+type HybridAppMiddlewareProps = {
+ authenticated: boolean;
+ children: React.ReactNode;
+};
+
+/*
+ * HybridAppMiddleware is responsible for handling BootSplash visibility correctly.
+ * It is crucial to make transitions between OldDot and NewDot look smooth.
+ * The middleware assumes that the entry point for HybridApp is the /transition route.
+ */
+function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) {
+ const {isSplashHidden, setIsSplashHidden} = useSplashScreen();
+ const [startedTransition, setStartedTransition] = useState(false);
+ const [finishedTransition, setFinishedTransition] = useState(false);
+
+ const initialURL = useContext(InitialURLContext);
+ const exitToParam = useExitTo();
+ const [exitTo, setExitTo] = useState();
+
+ const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false});
+ const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
+
+ // Save `exitTo` when we reach /transition route.
+ // `exitTo` should always exist during OldDot -> NewDot transitions.
+ useEffect(() => {
+ if (!NativeModules.HybridAppModule || !exitToParam || exitTo) {
+ return;
+ }
+
+ Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam});
+ setExitTo(exitToParam);
+
+ Log.info(`[HybridApp] Started transition`, true);
+ setStartedTransition(true);
+ }, [exitTo, exitToParam]);
+
+ useEffect(() => {
+ if (!startedTransition || finishedTransition) {
+ return;
+ }
+
+ const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL;
+ const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail);
+
+ // We need to wait with navigating to exitTo until all login-related actions are complete.
+ if (!authenticated || isLoggingInAsNewUser || isAccountLoading) {
+ return;
+ }
+
+ if (exitTo) {
+ Navigation.isNavigationReady().then(() => {
+ // We need to remove /transition from route history.
+ // `useExitTo` returns undefined for routes other than /transition.
+ if (exitToParam) {
+ Log.info('[HybridApp] Removing /transition route from history', true);
+ Navigation.goBack();
+ }
+
+ Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo});
+ Navigation.navigate(Navigation.parseHybridAppUrl(exitTo));
+ setExitTo(undefined);
+
+ setTimeout(() => {
+ Log.info('[HybridApp] Setting `finishedTransition` to true', true);
+ setFinishedTransition(true);
+ }, CONST.SCREEN_TRANSITION_END_TIMEOUT);
+ });
+ }
+ }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]);
+
+ useEffect(() => {
+ if (!finishedTransition || isSplashHidden) {
+ return;
+ }
+
+ Log.info('[HybridApp] Finished transition, hiding BootSplash', true);
+ BootSplash.hide().then(() => {
+ setIsSplashHidden(true);
+ if (authenticated) {
+ Log.info('[HybridApp] Handling onboarding flow', true);
+ Welcome.handleHybridAppOnboarding();
+ }
+ });
+ }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]);
+
+ return children;
+}
+
+HybridAppMiddleware.displayName = 'HybridAppMiddleware';
+
+export default HybridAppMiddleware;
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index bd0824372799..7a8186d2f38e 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -8,6 +8,7 @@ import ConciergeExclamation from '@assets/images/product-illustrations/concierge
import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg';
import EmptyStateExpenses from '@assets/images/product-illustrations/emptystate__expenses.svg';
import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg';
+import FolderWithPapers from '@assets/images/product-illustrations/folder-with-papers.svg';
import GpsTrackOrange from '@assets/images/product-illustrations/gps-track--orange.svg';
import Hands from '@assets/images/product-illustrations/home-illustration-hands.svg';
import InvoiceOrange from '@assets/images/product-illustrations/invoice--orange.svg';
@@ -92,6 +93,7 @@ import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustrati
import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg';
import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg';
import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg';
+import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg';
import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg';
import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg';
import ExpensifyApprovedLogoLight from '@assets/images/subscription-details__approvedlogo--light.svg';
@@ -196,4 +198,6 @@ export {
CheckmarkCircle,
CreditCardEyes,
LockClosedOrange,
+ FolderWithPapers,
+ VirtualCard,
};
diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx
index a520693cff57..8c980838b841 100644
--- a/src/components/ReceiptImage.tsx
+++ b/src/components/ReceiptImage.tsx
@@ -74,8 +74,11 @@ type ReceiptImageProps = (
/** The size of the fallback icon */
fallbackIconSize?: number;
- /** The colod of the fallback icon */
+ /** The color of the fallback icon */
fallbackIconColor?: string;
+
+ /** The background color of fallback icon */
+ fallbackIconBackground?: string;
};
function ReceiptImage({
@@ -93,6 +96,7 @@ function ReceiptImage({
fallbackIconSize,
shouldUseInitialObjectPosition = false,
fallbackIconColor,
+ fallbackIconBackground,
}: ReceiptImageProps) {
const styles = useThemeStyles();
@@ -129,6 +133,7 @@ function ReceiptImage({
fallbackIcon={fallbackIcon}
fallbackIconSize={fallbackIconSize}
fallbackIconColor={fallbackIconColor}
+ fallbackIconBackground={fallbackIconBackground}
objectPosition={shouldUseInitialObjectPosition ? CONST.IMAGE_OBJECT_POSITION.INITIAL : CONST.IMAGE_OBJECT_POSITION.TOP}
/>
);
diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx
new file mode 100644
index 000000000000..3911780d3965
--- /dev/null
+++ b/src/components/Search/SearchContext.tsx
@@ -0,0 +1,58 @@
+import React, {useCallback, useContext, useMemo, useState} from 'react';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+import type {SearchContext} from './types';
+
+const defaultSearchContext = {
+ currentSearchHash: -1,
+ selectedTransactionIDs: [],
+ setCurrentSearchHash: () => {},
+ setSelectedTransactionIds: () => {},
+};
+
+const Context = React.createContext(defaultSearchContext);
+
+function SearchContextProvider({children}: ChildrenProps) {
+ const [searchContextData, setSearchContextData] = useState>({
+ currentSearchHash: defaultSearchContext.currentSearchHash,
+ selectedTransactionIDs: defaultSearchContext.selectedTransactionIDs,
+ });
+
+ const setCurrentSearchHash = useCallback(
+ (searchHash: number) => {
+ setSearchContextData({
+ ...searchContextData,
+ currentSearchHash: searchHash,
+ });
+ },
+ [searchContextData],
+ );
+
+ const setSelectedTransactionIds = useCallback(
+ (selectedTransactionIDs: string[]) => {
+ setSearchContextData({
+ ...searchContextData,
+ selectedTransactionIDs,
+ });
+ },
+ [searchContextData],
+ );
+
+ const searchContext = useMemo(
+ () => ({
+ ...searchContextData,
+ setCurrentSearchHash,
+ setSelectedTransactionIds,
+ }),
+ [searchContextData, setCurrentSearchHash, setSelectedTransactionIds],
+ );
+
+ return {children};
+}
+
+function useSearchContext() {
+ return useContext(Context);
+}
+
+SearchContextProvider.displayName = 'SearchContextProvider';
+
+export {SearchContextProvider, useSearchContext};
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 8445cb3bc72e..fc5c23d5c9ec 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -13,7 +13,6 @@ import * as SearchActions from '@libs/actions/Search';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import * as ReportUtils from '@libs/ReportUtils';
-import type {SearchColumnType, SortOrder} from '@libs/SearchUtils';
import * as SearchUtils from '@libs/SearchUtils';
import Navigation from '@navigation/Navigation';
import type {AuthScreensParamList} from '@navigation/types';
@@ -25,8 +24,10 @@ import ROUTES from '@src/ROUTES';
import type SearchResults from '@src/types/onyx/SearchResults';
import type {SearchDataTypes, SearchQuery} from '@src/types/onyx/SearchResults';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+import {useSearchContext} from './SearchContext';
import SearchListWithHeader from './SearchListWithHeader';
import SearchPageHeader from './SearchPageHeader';
+import type {SearchColumnType, SortOrder} from './types';
type SearchProps = {
query: SearchQuery;
@@ -47,6 +48,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
const {isLargeScreenWidth} = useWindowDimensions();
const navigation = useNavigation>();
const lastSearchResultsRef = useRef>();
+ const {setCurrentSearchHash} = useSearchContext();
const getItemHeight = useCallback(
(item: TransactionListItemType | ReportListItemType) => {
@@ -83,6 +85,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
return;
}
+ setCurrentSearchHash(hash);
SearchActions.search({hash, query, policyIDs, offset: 0, sortBy, sortOrder});
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [hash, isOffline]);
@@ -197,7 +200,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
getItemHeight={getItemHeight}
shouldDebounceRowSelect
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
- listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ listHeaderWrapperStyle={[styles.ph8, styles.pv3, styles.pb5]}
containerStyle={[styles.pv0]}
showScrollIndicator={false}
onEndReachedThreshold={0.75}
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 3ebc2797947a..cff74fe08a0a 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -1,3 +1,6 @@
+import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils';
+import type CONST from '@src/CONST';
+
/** Model of the selected transaction */
type SelectedTransactionInfo = {
/** Whether the transaction is selected */
@@ -13,5 +16,14 @@ type SelectedTransactionInfo = {
/** Model of selected results */
type SelectedTransactions = Record;
-// eslint-disable-next-line import/prefer-default-export
-export type {SelectedTransactionInfo, SelectedTransactions};
+type SortOrder = ValueOf;
+type SearchColumnType = ValueOf;
+
+type SearchContext = {
+ currentSearchHash: number;
+ selectedTransactionIDs: string[];
+ setCurrentSearchHash: (hash: number) => void;
+ setSelectedTransactionIds: (selectedTransactionIds: string[]) => void;
+};
+
+export type {SelectedTransactionInfo, SelectedTransactions, SearchColumnType, SortOrder, SearchContext};
diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx
index 5af3d84bf32f..ad77070c1b99 100644
--- a/src/components/SelectionList/Search/ActionCell.tsx
+++ b/src/components/SelectionList/Search/ActionCell.tsx
@@ -1,46 +1,78 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
+import {useSearchContext} from '@components/Search/SearchContext';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
+import * as SearchActions from '@userActions/Search';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ROUTES from '@src/ROUTES';
+import type {SearchTransactionAction} from '@src/types/onyx/SearchResults';
+
+const actionTranslationsMap: Record = {
+ view: 'common.view',
+ review: 'common.review',
+ done: 'common.done',
+ paid: 'iou.settledExpensify',
+ hold: 'iou.hold',
+ unhold: 'iou.unhold',
+};
type ActionCellProps = {
- onButtonPress: () => void;
- action?: string;
+ action?: SearchTransactionAction;
+ transactionID?: string;
isLargeScreenWidth?: boolean;
isSelected?: boolean;
+ goToItem: () => void;
};
-function ActionCell({onButtonPress, action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true, isSelected = false}: ActionCellProps) {
+function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isLargeScreenWidth = true, isSelected = false, goToItem}: ActionCellProps) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+
+ const {currentSearchHash} = useSearchContext();
+
+ const onButtonPress = useCallback(() => {
+ if (!transactionID) {
+ return;
+ }
+
+ if (action === CONST.SEARCH.ACTION_TYPES.HOLD) {
+ Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(CONST.SEARCH.TAB.ALL, transactionID));
+ } else if (action === CONST.SEARCH.ACTION_TYPES.UNHOLD) {
+ SearchActions.unholdMoneyRequestOnSearch(currentSearchHash, [transactionID]);
+ }
+ }, [action, currentSearchHash, transactionID]);
+
if (!isLargeScreenWidth) {
return null;
}
+ const text = translate(actionTranslationsMap[action]);
+
if (action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE) {
- const buttonTextKey = action === CONST.SEARCH.ACTION_TYPES.PAID ? 'iou.settledExpensify' : 'common.done';
return (
+ );
+ }
+
return (
);
}
diff --git a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
index 8f46a5388da8..f634f84509b1 100644
--- a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
+++ b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
@@ -6,7 +6,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
-import type {SearchAccountDetails} from '@src/types/onyx/SearchResults';
+import type {SearchAccountDetails, SearchTransactionAction} from '@src/types/onyx/SearchResults';
import ActionCell from './ActionCell';
import UserInfoCell from './UserInfoCell';
@@ -15,11 +15,12 @@ type ExpenseItemHeaderNarrowProps = {
participantTo: SearchAccountDetails;
participantFromDisplayName: string;
participantToDisplayName: string;
+ action?: SearchTransactionAction;
+ transactionID?: string;
onButtonPress: () => void;
- action?: string;
};
-function ExpenseItemHeaderNarrow({participantFrom, participantFromDisplayName, participantTo, participantToDisplayName, onButtonPress, action}: ExpenseItemHeaderNarrowProps) {
+function ExpenseItemHeaderNarrow({participantFrom, participantFromDisplayName, participantTo, participantToDisplayName, action, transactionID, onButtonPress}: ExpenseItemHeaderNarrowProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const theme = useTheme();
@@ -48,9 +49,9 @@ function ExpenseItemHeaderNarrow({participantFrom, participantFromDisplayName, p
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index 7119cee06cd9..29c7bc2ca60c 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -145,7 +145,7 @@ function ReportListItem({
onButtonPress={handleOnButtonPress}
/>
)}
-
+
{canSelectMultiple && (
@@ -177,9 +177,8 @@ function ReportListItem({
diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx
index b00ae0703c2e..6db308831baa 100644
--- a/src/components/SelectionList/Search/TransactionListItem.tsx
+++ b/src/components/SelectionList/Search/TransactionListItem.tsx
@@ -22,7 +22,7 @@ function TransactionListItem({
const {isLargeScreenWidth} = useWindowDimensions();
- const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive];
+ const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, styles.ph3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive];
const listItemWrapperStyle = [
styles.flex1,
diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx
index 23f9234819c3..f9ca70536e4b 100644
--- a/src/components/SelectionList/Search/TransactionListItemRow.tsx
+++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx
@@ -72,13 +72,15 @@ function ReceiptCell({transactionItem}: TransactionCellProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const backgroundStyles = transactionItem.isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border);
+
return (
@@ -251,8 +254,9 @@ function TransactionListItemRow({
participantFromDisplayName={item.formattedFrom}
participantTo={item.to}
participantToDisplayName={item.formattedTo}
- onButtonPress={onButtonPress}
action={item.action}
+ transactionID={item.transactionID}
+ onButtonPress={onButtonPress}
/>
)}
@@ -314,7 +318,7 @@ function TransactionListItemRow({
style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled]}
/>
)}
-
+
diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx
index 95e4b680692b..235cff294f8f 100644
--- a/src/components/SelectionList/SearchTableHeader.tsx
+++ b/src/components/SelectionList/SearchTableHeader.tsx
@@ -1,11 +1,11 @@
import React from 'react';
import {View} from 'react-native';
+import type {SearchColumnType, SortOrder} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as SearchUtils from '@libs/SearchUtils';
-import type {SearchColumnType, SortOrder} from '@libs/SearchUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type * as OnyxTypes from '@src/types/onyx';
@@ -108,7 +108,7 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, isSortingAllowed,
return (
-
+
{SearchColumns.map(({columnName, translationKey, shouldShow, isColumnSortable}) => {
if (!shouldShow(data, metadata)) {
return null;
diff --git a/src/components/SelectionList/SortableHeaderText.tsx b/src/components/SelectionList/SortableHeaderText.tsx
index 8b0accf45711..47a894d79f53 100644
--- a/src/components/SelectionList/SortableHeaderText.tsx
+++ b/src/components/SelectionList/SortableHeaderText.tsx
@@ -4,10 +4,10 @@ import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import type {SortOrder} from '@components/Search/types';
import Text from '@components/Text';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import type {SortOrder} from '@libs/SearchUtils';
import CONST from '@src/CONST';
type SearchTableHeaderColumnProps = {
diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx
index af1cdfd171ea..8e1b0a88c875 100644
--- a/src/components/SelectionScreen.tsx
+++ b/src/components/SelectionScreen.tsx
@@ -1,12 +1,18 @@
import {isEmpty} from 'lodash';
import React from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
import * as PolicyUtils from '@libs/PolicyUtils';
import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {TranslationPaths} from '@src/languages/types';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {ConnectionName, PolicyFeatureName} from '@src/types/onyx/Policy';
+import type {ReceiptErrors} from '@src/types/onyx/Transaction';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import HeaderWithBackButton from './HeaderWithBackButton';
+import OfflineWithFeedback from './OfflineWithFeedback';
import ScreenWrapper from './ScreenWrapper';
import SelectionList from './SelectionList';
import type RadioListItem from './SelectionList/RadioListItem';
@@ -63,6 +69,18 @@ type SelectionScreenProps = {
/** Name of the current connection */
connectionName: ConnectionName;
+
+ /** The type of action that's pending */
+ pendingAction?: OnyxCommon.PendingAction | null;
+
+ /** The errors to display */
+ errors?: OnyxCommon.Errors | ReceiptErrors | null;
+
+ /** Additional style object for the error row */
+ errorRowStyles?: StyleProp;
+
+ /** A function to run when the X button next to the error is clicked */
+ onClose?: () => void;
};
function SelectionScreen({
@@ -81,8 +99,13 @@ function SelectionScreen({
featureName,
shouldBeBlocked,
connectionName,
+ pendingAction,
+ errors,
+ errorRowStyles,
+ onClose,
}: SelectionScreenProps) {
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const policy = PolicyUtils.getPolicy(policyID);
const isConnectionEmpty = isEmpty(policy?.connections?.[connectionName]);
@@ -95,24 +118,33 @@ function SelectionScreen({
shouldBeBlocked={isConnectionEmpty || shouldBeBlocked}
>
-
+ {headerContent}
+
+
+
);
diff --git a/src/components/StateSelector.tsx b/src/components/StateSelector.tsx
index 7e598a929ae3..e2be9281d0bb 100644
--- a/src/components/StateSelector.tsx
+++ b/src/components/StateSelector.tsx
@@ -89,7 +89,7 @@ function StateSelector(
brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={errorText}
onPress={() => {
- const activeRoute = Navigation.getActiveRouteWithoutParams();
+ const activeRoute = Navigation.getActiveRoute();
didOpenStateSelector.current = true;
Navigation.navigate(stateSelectorRoute.getRoute(stateCode, activeRoute, label));
}}
diff --git a/src/components/TextPicker/TextSelectorModal.tsx b/src/components/TextPicker/TextSelectorModal.tsx
index 2016133559d4..bea95df3dbfe 100644
--- a/src/components/TextPicker/TextSelectorModal.tsx
+++ b/src/components/TextPicker/TextSelectorModal.tsx
@@ -1,6 +1,7 @@
import {useFocusEffect} from '@react-navigation/native';
-import React, {useCallback, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Keyboard, View} from 'react-native';
+import type {TextInput as TextInputType} from 'react-native';
import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
@@ -23,6 +24,7 @@ function TextSelectorModal({value, description = '', subtitle, onValueSelected,
const paddingStyle = usePaddingStyle();
const inputRef = useRef(null);
+ const inputValueRef = useRef(value);
const focusTimeoutRef = useRef(null);
const hide = useCallback(() => {
@@ -32,11 +34,16 @@ function TextSelectorModal({value, description = '', subtitle, onValueSelected,
}
}, [onClose, shouldClearOnClose]);
+ useEffect(() => {
+ inputValueRef.current = currentValue;
+ }, [currentValue]);
+
useFocusEffect(
useCallback(() => {
focusTimeoutRef.current = setTimeout(() => {
if (inputRef.current && isVisible) {
inputRef.current.focus();
+ (inputRef.current as TextInputType).setSelection?.(inputValueRef.current?.length ?? 0, inputValueRef.current?.length ?? 0);
}
return () => {
if (!focusTimeoutRef.current || !isVisible) {
diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx
index 3b89b7c3a7ad..04d0200ea228 100644
--- a/src/components/ThumbnailImage.tsx
+++ b/src/components/ThumbnailImage.tsx
@@ -41,9 +41,12 @@ type ThumbnailImageProps = {
/** The size of the fallback icon */
fallbackIconSize?: number;
- /** The colod of the fallback icon */
+ /** The color of the fallback icon */
fallbackIconColor?: string;
+ /** The background color of fallback icon */
+ fallbackIconBackground?: string;
+
/** Should the image be resized on load or just fit container */
shouldDynamicallyResize?: boolean;
@@ -66,6 +69,7 @@ function ThumbnailImage({
fallbackIcon = Expensicons.Gallery,
fallbackIconSize = variables.iconSizeSuperLarge,
fallbackIconColor,
+ fallbackIconBackground,
objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL,
}: ThumbnailImageProps) {
const styles = useThemeStyles();
@@ -107,8 +111,10 @@ function ThumbnailImage({
const sizeStyles = shouldDynamicallyResize ? [thumbnailDimensionsStyles] : [styles.w100, styles.h100];
if (failedToLoad || previewSourceURL === '') {
+ const fallbackColor = StyleUtils.getBackgroundColorStyle(fallbackIconBackground ?? theme.border);
+
return (
-
+
{title}
- {!!subtitle && (
+ {(!!subtitle || !!subtitleComponent) && (
- {subtitle}
+ {subtitleComponent ?? {subtitle}}
)}
diff --git a/src/hooks/useExitTo.ts b/src/hooks/useExitTo.ts
new file mode 100644
index 000000000000..74226453d3f6
--- /dev/null
+++ b/src/hooks/useExitTo.ts
@@ -0,0 +1,17 @@
+import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
+import type {PublicScreensParamList, RootStackParamList} from '@libs/Navigation/types';
+import SCREENS from '@src/SCREENS';
+
+export default function useExitTo() {
+ const activeRouteParams = useNavigationState((state) => {
+ const focusedRoute = findFocusedRoute(state);
+
+ if (focusedRoute?.name !== SCREENS.TRANSITION_BETWEEN_APPS) {
+ return undefined;
+ }
+
+ return focusedRoute?.params as PublicScreensParamList[typeof SCREENS.TRANSITION_BETWEEN_APPS];
+ });
+
+ return activeRouteParams?.exitTo;
+}
diff --git a/src/hooks/useHybridAppMiddleware.ts b/src/hooks/useHybridAppMiddleware.ts
deleted file mode 100644
index 18ebd9730630..000000000000
--- a/src/hooks/useHybridAppMiddleware.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import {useContext} from 'react';
-import {HybridAppMiddlewareContext} from '@components/HybridAppMiddleware';
-
-type SplashScreenHiddenContextType = {isSplashHidden: boolean};
-
-export default function useHybridAppMiddleware() {
- const {navigateToExitUrl, showSplashScreenOnNextStart} = useContext(HybridAppMiddlewareContext);
- return {navigateToExitUrl, showSplashScreenOnNextStart};
-}
-
-export type {SplashScreenHiddenContextType};
diff --git a/src/hooks/useLastAccessedReportID.ts b/src/hooks/useLastAccessedReportID.ts
deleted file mode 100644
index 16a4a6bc2a31..000000000000
--- a/src/hooks/useLastAccessedReportID.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import {useCallback, useSyncExternalStore} from 'react';
-import type {OnyxCollection} from 'react-native-onyx';
-import Onyx from 'react-native-onyx';
-import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Policy, Report, ReportMetadata} from '@src/types/onyx';
-import useActiveWorkspace from './useActiveWorkspace';
-import usePermissions from './usePermissions';
-
-/*
- * This hook is used to get the lastAccessedReportID.
- * This is a piece of data that's derived from a lot of frequently-changing Onyx values: (reports, reportMetadata, policies, etc...)
- * We don't want any component that needs access to the lastAccessedReportID to have to re-render any time any of those values change, just when the lastAccessedReportID changes.
- * So we have a custom implementation in this file that leverages useSyncExternalStore to connect to a "store" of multiple Onyx values, and re-render only when the one derived value changes.
- */
-
-const subscribers: Array<() => void> = [];
-
-let reports: OnyxCollection = {};
-let reportMetadata: OnyxCollection = {};
-let policies: OnyxCollection = {};
-let accountID: number | undefined;
-let isFirstTimeNewExpensifyUser = false;
-
-let reportsConnection: number;
-let reportMetadataConnection: number;
-let policiesConnection: number;
-let accountIDConnection: number;
-let isFirstTimeNewExpensifyUserConnection: number;
-
-function notifySubscribers() {
- subscribers.forEach((subscriber) => subscriber());
-}
-
-function subscribeToOnyxData() {
- // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
- reportsConnection = Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT,
- waitForCollectionCallback: true,
- callback: (value) => {
- reports = value;
- notifySubscribers();
- },
- });
- // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
- reportMetadataConnection = Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT_METADATA,
- waitForCollectionCallback: true,
- callback: (value) => {
- reportMetadata = value;
- notifySubscribers();
- },
- });
- // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
- policiesConnection = Onyx.connect({
- key: ONYXKEYS.COLLECTION.POLICY,
- waitForCollectionCallback: true,
- callback: (value) => {
- policies = value;
- notifySubscribers();
- },
- });
- // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
- accountIDConnection = Onyx.connect({
- key: ONYXKEYS.SESSION,
- callback: (value) => {
- accountID = value?.accountID;
- notifySubscribers();
- },
- });
- // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
- isFirstTimeNewExpensifyUserConnection = Onyx.connect({
- key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
- callback: (value) => {
- isFirstTimeNewExpensifyUser = !!value;
- notifySubscribers();
- },
- });
-}
-
-function unsubscribeFromOnyxData() {
- if (reportsConnection) {
- Onyx.disconnect(reportsConnection);
- reportsConnection = 0;
- }
- if (reportMetadataConnection) {
- Onyx.disconnect(reportMetadataConnection);
- reportMetadataConnection = 0;
- }
- if (policiesConnection) {
- Onyx.disconnect(policiesConnection);
- policiesConnection = 0;
- }
- if (accountIDConnection) {
- Onyx.disconnect(accountIDConnection);
- accountIDConnection = 0;
- }
- if (isFirstTimeNewExpensifyUserConnection) {
- Onyx.disconnect(isFirstTimeNewExpensifyUserConnection);
- isFirstTimeNewExpensifyUserConnection = 0;
- }
-}
-
-function removeSubscriber(subscriber: () => void) {
- const subscriberIndex = subscribers.indexOf(subscriber);
- if (subscriberIndex < 0) {
- return;
- }
- subscribers.splice(subscriberIndex, 1);
- if (subscribers.length === 0) {
- unsubscribeFromOnyxData();
- }
-}
-
-function addSubscriber(subscriber: () => void) {
- subscribers.push(subscriber);
- if (!reportsConnection) {
- subscribeToOnyxData();
- }
- return () => removeSubscriber(subscriber);
-}
-
-/**
- * Get the last accessed reportID.
- */
-export default function useLastAccessedReportID(shouldOpenOnAdminRoom: boolean) {
- const {canUseDefaultRooms} = usePermissions();
- const {activeWorkspaceID} = useActiveWorkspace();
-
- const getSnapshot = useCallback(() => {
- const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID);
- return ReportUtils.findLastAccessedReport(
- reports,
- !canUseDefaultRooms,
- policies,
- isFirstTimeNewExpensifyUser,
- shouldOpenOnAdminRoom,
- reportMetadata,
- activeWorkspaceID,
- policyMemberAccountIDs,
- )?.reportID;
- }, [activeWorkspaceID, canUseDefaultRooms, shouldOpenOnAdminRoom]);
-
- // We need access to all the data from these Onyx.connect calls, but we don't want to re-render the consuming component
- // unless the derived value (lastAccessedReportID) changes. To address these, we'll wrap everything with useSyncExternalStore
- return useSyncExternalStore(addSubscriber, getSnapshot);
-}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 543dfbf5e541..1ac9684ac22e 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2,7 +2,7 @@ import {CONST as COMMON_CONST, Str} from 'expensify-common';
import {startCase} from 'lodash';
import CONST from '@src/CONST';
import type {Country} from '@src/CONST';
-import type {ConnectionName, PolicyConnectionSyncStage} from '@src/types/onyx/Policy';
+import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy';
import type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -171,6 +171,7 @@ export default {
wallet: 'Wallet',
preferences: 'Preferences',
view: 'View',
+ review: 'Review',
not: 'Not',
signIn: 'Sign in',
signInWithGoogle: 'Sign in with Google',
@@ -1462,6 +1463,7 @@ export default {
title: 'What do you want to do today?',
errorSelection: 'Please make a selection to continue.',
errorContinue: 'Please press continue to get set up.',
+ errorBackButton: 'Please finish the setup questions to start using the app.',
[CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Get paid back by my employer',
[CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: "Manage my team's expenses",
[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Track and budget expenses',
@@ -2005,6 +2007,7 @@ export default {
categories: 'Categories',
tags: 'Tags',
reportFields: 'Report fields',
+ reportField: 'Report field',
taxes: 'Taxes',
bills: 'Bills',
invoices: 'Invoices',
@@ -2037,6 +2040,10 @@ export default {
welcomeNote: ({workspaceName}: WelcomeNoteParams) =>
`You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`,
subscription: 'Subscription',
+ letsDoubleCheck: "Let's double check that everything looks right.",
+ lineItemLevel: 'Line-item level',
+ reportLevel: 'Report level',
+ appliedOnExport: 'Not imported into Expensify, applied on export',
},
qbo: {
importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.',
@@ -2213,6 +2220,63 @@ export default {
noAccountsFound: 'No accounts found',
noAccountsFoundDescription: 'Add the account in Xero and sync the connection again.',
},
+ sageIntacct: {
+ preferredExporter: 'Preferred exporter',
+ notConfigured: 'Not configured',
+ exportDate: {
+ label: 'Export date',
+ description: 'Use this date when exporting reports to Sage Intacct.',
+ values: {
+ [CONST.SAGE_INTACCT_EXPORT_DATE.LAST_EXPENSE]: {
+ label: 'Date of last expense',
+ description: 'Date of the most recent expense on the report.',
+ },
+ [CONST.SAGE_INTACCT_EXPORT_DATE.EXPORTED]: {
+ label: 'Export date',
+ description: 'Date the report was exported to Sage Intacct.',
+ },
+ [CONST.SAGE_INTACCT_EXPORT_DATE.SUBMITTED]: {
+ label: 'Submitted date',
+ description: 'Date the report was submitted for approval.',
+ },
+ },
+ },
+ reimbursableExpenses: {
+ label: 'Export reimbursable expenses as',
+ description: 'Reimbursable expenses will export as expense reports to Sage Intacct. Bills will export as vendor bills.',
+ values: {
+ [CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT]: 'Expense reports',
+ [CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL]: 'Vendor bills',
+ },
+ },
+ nonReimbursableExpenses: {
+ label: 'Export non-reimbursable expenses as',
+ description:
+ 'Non-reimbursable expenses will export to Sage Intacct as either credit card transactions or vendor bills and credit the account selected below. Learn more about assigning cards to individual accounts.',
+ values: {
+ [CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE]: 'Credit card transactions',
+ [CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL]: 'Vendor bills',
+ },
+ },
+ creditCardAccount: 'Credit card account',
+ defaultVendor: 'Default vendor',
+ defaultVendorDescription: (isReimbursable: boolean): string =>
+ `Set a default vendor that will apply to ${isReimbursable ? '' : 'non-'}reimbursable expenses that don't have a matching vendor in Sage Intacct.`,
+ exportDescription: 'Configure how data in Expensify gets exported to Sage Intacct.',
+ exportPreferredExporterNote:
+ 'The preferred exporter can be any workspace admin, but must also be a Domain Admin if you set different export accounts for individual company cards in Domain Settings.',
+ exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.',
+ noAccountsFound: 'No accounts found',
+ noAccountsFoundDescription: `Add the account in Sage Intacct and sync the connection again.`,
+ autoSync: 'Auto-sync',
+ autoSyncDescription: 'Sync Sage Intacct and Expensify automatically, every day.',
+ inviteEmployees: 'Invite employees',
+ inviteEmployeesDescription:
+ 'Import Sage Intacct employee records and invite employees to this workspace. Your approval workflow will default to manager approval and can be furthered configured on the Members page.',
+ syncReimbursedReports: 'Sync reimbursed reports',
+ syncReimbursedReportsDescription: 'When a report is reimbursed using Expensify ACH, the corresponding puchase bill will be created in the Sage Intacct account below.',
+ paymentAccount: 'Sage Intacct payment account',
+ },
netsuite: {
subsidiary: 'Subsidiary',
subsidiarySelectDescription: "Choose the subsidiary in NetSuite that you'd like to import data from.",
@@ -2416,8 +2480,77 @@ export default {
},
importTaxDescription: 'Import tax groups from NetSuite.',
importCustomFields: {
- customSegments: 'Custom segments/records',
- customLists: 'Custom lists',
+ chooseOptionBelow: 'Choose an option below:',
+ requiredFieldError: (fieldName: string) => `Please enter the ${fieldName}`,
+ customSegments: {
+ title: 'Custom segments/records',
+ addText: 'Add custom segment/record',
+ recordTitle: 'Custom segment',
+ helpLink: CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS,
+ helpLinkText: 'View detailed instructions',
+ helpText: ' on configuring custom segments/records.',
+ emptyTitle: 'Add a custom segment or custom record',
+ fields: {
+ segmentName: 'Name',
+ internalID: 'Internal ID',
+ scriptID: 'Script ID',
+ customRecordScriptID: 'Transaction column ID',
+ mapping: 'Displayed as',
+ },
+ removeTitle: 'Remove custom segment/record',
+ removePrompt: 'Are you sure you want to remove this custom segment/record?',
+ addForm: {
+ customSegmentName: 'custom segment name',
+ customRecordName: 'custom record name',
+ segmentTitle: 'Custom segment',
+ customSegmentAddTitle: 'Add custom segment',
+ customRecordAddTitle: 'Add custom record',
+ recordTitle: 'Custom record',
+ segmentRecordType: 'Do you want to add a custom segment or a custom record?',
+ customSegmentNameTitle: "What's the custom segment name?",
+ customRecordNameTitle: "What's the custom record name?",
+ customSegmentNameFooter: `You can find custom segment names in NetSuite under *Customizations > Links, Records & Fields > Custom Segments* page.\n\n_For more detailed instructions, [visit our help site](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
+ customRecordNameFooter: `You can find custom record names in NetSuite by entering the "Transaction Column Field" in global search.\n\n_For more detailed instructions, [visit our help site](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
+ customSegmentInternalIDTitle: "What's the internal ID?",
+ customSegmentInternalIDFooter: `First, make sure you've enabled internal IDs in NetSuite under *Home > Set Preferences > Show Internal ID.*\n\nYou can find custom segment internal IDs in NetSuite under:\n\n1. *Customization > Lists, Records, & Fields > Custom Segments*.\n2. Click into a custom segment.\n3. Click the hyperlink next to *Custom Record Type*.\n4. Find the internal ID in the table at the bottom.\n\n_For more detailed instructions, [visit our help site](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_LISTS})_.`,
+ customRecordInternalIDFooter: `You can find custom record internal IDs in NetSuite by following these steps:\n\n1. Enter "Transaction Line Fields" in global search.\n2. Click into a custom record.\n3. Find the internal ID on the left-hand side.\n\n_For more detailed instructions, [visit our help site](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
+ customSegmentScriptIDTitle: "What's the script ID?",
+ customSegmentScriptIDFooter: `You can find custom segment script IDs in NetSuite under: \n\n1. *Customization > Lists, Records, & Fields > Custom Segments*.\n2. Click into a custom segment.\n3. Click the *Application and Sourcing* tab near the bottom, then:\n a. If you want to display the custom segment as a *tag* (at the line-item level) in Expensify, click the *Transaction Columns* sub-tab and use the *Field ID*.\n b. If you want to display the custom segment as a *report field* (at the report level) in Expensify, click the *Transactions* sub-tab and use the *Field ID*.\n\n_For more detailed instructions, [visit our help site](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_LISTS})_.`,
+ customRecordScriptIDTitle: "What's the transaction column ID?",
+ customRecordScriptIDFooter: `You can find custom record script IDs in NetSuite under:\n\n1. Enter "Transaction Line Fields" in global search.\n2. Click into a custom record.\n3. Find the script ID on the left-hand side.\n\n_For more detailed instructions, [visit our help site](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
+ customSegmentMappingTitle: 'How should this custom segment be displayed in Expensify?',
+ customRecordMappingTitle: 'How should this custom record be displayed in Expensify?',
+ },
+ errors: {
+ uniqueFieldError: (fieldName: string) => `A custom segment/record with this ${fieldName?.toLowerCase()} already exists.`,
+ },
+ },
+ customLists: {
+ title: 'Custom lists',
+ addText: 'Add custom list',
+ recordTitle: 'Custom list',
+ helpLink: CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_LISTS,
+ helpLinkText: 'View detailed instructions',
+ helpText: ' on configuring custom lists.',
+ emptyTitle: 'Add a custom list',
+ fields: {
+ listName: 'Name',
+ internalID: 'Internal ID',
+ transactionFieldID: 'Transaction field ID',
+ mapping: 'Displayed as',
+ },
+ removeTitle: 'Remove custom list',
+ removePrompt: 'Are you sure you want to remove this custom list?',
+ addForm: {
+ listNameTitle: 'Choose a custom list',
+ transactionFieldIDTitle: "What's the transaction field ID?",
+ transactionFieldIDFooter: `You can find transaction field IDs in NetSuite by following these steps:\n\n1. Enter "Transaction Line Fields" in global search.\n2. Click into a custom list.\n3. Find the transaction field ID on the left-hand side.\n\n_For more detailed instructions, [visit our help site](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_LISTS})_.`,
+ mappingTitle: 'How should this custom list be displayed in Expensify?',
+ },
+ errors: {
+ uniqueTransactionFieldIDError: `A custom list with this transaction field ID already exists.`,
+ },
+ },
},
importTypes: {
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: {
@@ -2449,6 +2582,42 @@ export default {
reuseExistingConnection: 'Reuse existing connection',
existingConnections: 'Existing connections',
sageIntacctLastSync: (formattedDate: string) => `Sage Intacct - Last synced ${formattedDate}`,
+ employeeDefault: 'Sage Intacct employee default',
+ employeeDefaultDescription: "The employee's default department will be applied to their expenses in Sage Intacct if one exists.",
+ displayedAsTagDescription: "Department will be selectable for each individual expense on an employee's report.",
+ displayedAsReportFieldDescription: "Department selection will apply to all expenses on an employee's report.",
+ toggleImportTitleFirstPart: 'Choose how to handle Sage Intacct ',
+ toggleImportTitleSecondPart: ' in Expensify.',
+ expenseTypes: 'Expense types',
+ expenseTypesDescription: 'Sage Intacct expense types import into Expensify as categories.',
+ importTaxDescription: 'Import purchase tax rate from Sage Intacct.',
+ userDefinedDimensions: 'User-defined dimensions',
+ addUserDefinedDimension: 'Add user-defined dimension',
+ integrationName: 'Integration name',
+ dimensionExists: 'A dimension with this name already exists.',
+ removeDimension: 'Remove user-defined dimension',
+ removeDimensionPrompt: 'Are you sure you want to remove this user-defined dimension?',
+ userDefinedDimension: 'User-defined dimension',
+ addAUserDefinedDimension: 'Add a user-defined dimension',
+ detailedInstructionsLink: 'View detailed instructions',
+ detailedInstructionsRestOfSentence: ' on adding user-defined dimensions.',
+ userDimensionsAdded: (dimensionsCount: number) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} added`,
+ mappingTitle: (mappingName: SageIntacctMappingName): string => {
+ switch (mappingName) {
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS:
+ return 'departments';
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CLASSES:
+ return 'classes';
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.LOCATIONS:
+ return 'locations';
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CUSTOMERS:
+ return 'customers';
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.PROJECTS:
+ return 'projects (jobs)';
+ default:
+ return 'mappings';
+ }
+ },
},
type: {
free: 'Free',
@@ -2565,7 +2734,9 @@ export default {
reportFields: {
addField: 'Add field',
delete: 'Delete field',
- deleteConfirmation: 'Are you sure that you want to delete this field?',
+ deleteFields: 'Delete fields',
+ deleteConfirmation: 'Are you sure you want to delete this report field?',
+ deleteFieldsConfirmation: 'Are you sure you want to delete these report fields?',
emptyReportFields: {
title: "You haven't created any report fields",
subtitle: 'Add a custom field (text, date, or dropdown) that appears on reports.',
@@ -2886,6 +3057,8 @@ export default {
return 'Updating people list';
case 'quickbooksOnlineSyncApplyClassesLocations':
return 'Updating report fields';
+ case 'jobDone':
+ return 'Waiting for imported data to load';
case 'xeroSyncImportChartOfAccounts':
return 'Syncing chart of accounts';
case 'xeroSyncImportCategories':
diff --git a/src/languages/es.ts b/src/languages/es.ts
index e7f4faab2725..ea9186daee78 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1,6 +1,6 @@
import {Str} from 'expensify-common';
import CONST from '@src/CONST';
-import type {ConnectionName, PolicyConnectionSyncStage} from '@src/types/onyx/Policy';
+import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy';
import type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -161,6 +161,7 @@ export default {
wallet: 'Billetera',
preferences: 'Preferencias',
view: 'Ver',
+ review: 'Revisar',
not: 'No',
privacyPolicy: 'la PolĂtica de Privacidad de Expensify',
addCardTermsOfService: 'TĂ©rminos de Servicio',
@@ -1470,6 +1471,7 @@ export default {
title: '¿Qué quieres hacer hoy?',
errorSelection: 'Por favor selecciona una opciĂłn para continuar.',
errorContinue: 'Por favor, haz click en continuar para configurar tu cuenta.',
+ errorBackButton: 'Por favor, finaliza las preguntas de configuraciĂłn para empezar a utilizar la aplicaciĂłn.',
[CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Cobrar de mi empresa',
[CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo',
[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Controlar y presupuestar gastos',
@@ -2068,6 +2070,11 @@ export default {
welcomeNote: ({workspaceName}: WelcomeNoteParams) =>
`¡Has sido invitado a ${workspaceName}! Descargue la aplicación móvil Expensify en use.expensify.com/download para comenzar a rastrear sus gastos.`,
subscription: 'SuscripciĂłn',
+ letsDoubleCheck: 'Verifiquemos que todo esté correcto',
+ reportField: 'Campo del informe',
+ lineItemLevel: 'Nivel de partida',
+ reportLevel: 'Nivel de informe',
+ appliedOnExport: 'No se importa en Expensify, se aplica en la exportaciĂłn',
},
qbo: {
importDescription: 'Elige que configuraciĂłnes de codificaciĂłn son importadas desde QuickBooks Online a Expensify.',
@@ -2253,6 +2260,65 @@ export default {
noAccountsFound: 'No se ha encontrado ninguna cuenta',
noAccountsFoundDescription: 'Añade la cuenta en Xero y sincroniza de nuevo la conexión.',
},
+
+ sageIntacct: {
+ preferredExporter: 'Exportador preferido',
+ notConfigured: 'No configurado',
+ exportDate: {
+ label: 'Fecha de exportaciĂłn',
+ description: 'Utilice esta fecha cuando exporte informes a Sage Intacct.',
+ values: {
+ [CONST.SAGE_INTACCT_EXPORT_DATE.LAST_EXPENSE]: {
+ label: 'Fecha del Ăşltimo gasto',
+ description: 'Fecha del gasto más reciente del informe.',
+ },
+ [CONST.SAGE_INTACCT_EXPORT_DATE.EXPORTED]: {
+ label: 'Fecha de exportaciĂłn',
+ description: 'Fecha en la que se exportĂł el informe a Sage Intacct.',
+ },
+ [CONST.SAGE_INTACCT_EXPORT_DATE.SUBMITTED]: {
+ label: 'Fecha de envĂo',
+ description: 'Fecha de presentaciĂłn del informe para su aprobaciĂłn.',
+ },
+ },
+ },
+ reimbursableExpenses: {
+ label: 'Gastos reembolsables de exportaciĂłn como',
+ description: 'Los gastos reembolsables se exportarán como informes de gastos a Sage Intacct. Las facturas se exportarán como facturas de proveedores.',
+ values: {
+ [CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT]: 'Informes de gastos',
+ [CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL]: 'Facturas de proveedores',
+ },
+ },
+ nonReimbursableExpenses: {
+ label: 'Exportar gastos no reembolsables como',
+ description:
+ 'Los gastos no reembolsables se exportarán a Sage Intacct como transacciones de tarjetas de crédito o facturas de proveedores y se abonarán en la cuenta seleccionada a continuación. Más información sobre la asignación de tarjetas a cuentas individuales.',
+ values: {
+ [CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE]: 'Transacciones con tarjeta de crédito',
+ [CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL]: 'Facturas de proveedores',
+ },
+ },
+ creditCardAccount: 'Cuenta de tarjeta de crédito',
+ defaultVendor: 'Proveedor por defecto',
+ defaultVendorDescription: (isReimbursable: boolean): string =>
+ `Establezca un proveedor predeterminado que se aplicará a los gastos ${isReimbursable ? '' : 'no '}reembolsables que no tienen un proveedor coincidente en Sage Intacct.`,
+ exportDescription: 'Configure cĂłmo se exportan los datos de Expensify a Sage Intacct.',
+ exportPreferredExporterNote:
+ 'El exportador preferido puede ser cualquier administrador del área de trabajo, pero también debe ser un administrador del dominio si establece diferentes cuentas de exportación para tarjetas de empresa individuales en Configuración del dominio.',
+ exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en su cuenta.',
+ noAccountsFound: 'No se ha encontrado ninguna cuenta',
+ noAccountsFoundDescription: 'Añade la cuenta en Sage Intacct y sincroniza de nuevo la conexión.',
+ autoSync: 'Sincronización automática',
+ autoSyncDescription: 'Sincronice Sage Intacct y Expensify automáticamente, todos los dĂas.',
+ inviteEmployees: 'Invitar a los empleados',
+ inviteEmployeesDescription:
+ 'Importe los registros de empleados de Sage Intacct e invite a los empleados a este espacio de trabajo. Su flujo de trabajo de aprobación será por defecto la aprobación del gerente y se puede configurar aún más en la página Miembros.',
+ syncReimbursedReports: 'Sincronizar informes reembolsados',
+ syncReimbursedReportsDescription:
+ 'Cuando un informe se reembolsa utilizando Expensify ACH, la factura de compra correspondiente se creará en la cuenta de Sage Intacct a continuación.',
+ paymentAccount: 'Cuenta de pago Sage Intacct',
+ },
netsuite: {
subsidiary: 'Subsidiaria',
subsidiarySelectDescription: 'Elige la subsidiaria de NetSuite de la que deseas importar datos.',
@@ -2459,8 +2525,77 @@ export default {
},
importTaxDescription: 'Importar grupos de impuestos desde NetSuite.',
importCustomFields: {
- customSegments: 'Segmentos/registros personalizados',
- customLists: 'Listas personalizado',
+ chooseOptionBelow: 'Elija una de las opciones siguientes:',
+ requiredFieldError: (fieldName: string) => `Por favor, introduzca el ${fieldName}`,
+ customSegments: {
+ title: 'Segmentos/registros personalizados',
+ addText: 'Añadir segmento/registro personalizado',
+ recordTitle: 'Segmento personalizado',
+ helpLink: CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS,
+ helpLinkText: 'Ver instrucciones detalladas',
+ helpText: ' sobre la configuraciĂłn de segmentos/registros personalizado.',
+ emptyTitle: 'Añadir un segmento personalizado o un registro personalizado',
+ fields: {
+ segmentName: 'Name',
+ internalID: 'IdentificaciĂłn interna',
+ scriptID: 'ID de guiĂłn',
+ mapping: 'Mostrado como',
+ customRecordScriptID: 'ID de columna de transacciĂłn',
+ },
+ removeTitle: 'Eliminar segmento/registro personalizado',
+ removePrompt: '¿Está seguro de que desea eliminar este segmento/registro personalizado?',
+ addForm: {
+ customSegmentName: 'nombre de segmento personalizado',
+ customRecordName: 'nombre de registro personalizado',
+ segmentTitle: 'Segmento personalizado',
+ customSegmentAddTitle: 'Añadir segmento personalizado',
+ customRecordAddTitle: 'Añadir registro personalizado',
+ recordTitle: 'Registro personalizado',
+ segmentRecordType: '¿Desea añadir un segmento personalizado o un registro personalizado?',
+ customSegmentNameTitle: '¿Cuál es el nombre del segmento personalizado?',
+ customRecordNameTitle: '¿Cuál es el nombre del registro personalizado?',
+ customSegmentNameFooter: `Puede encontrar los nombres de los segmentos personalizados en NetSuite en la página *Personalizaciones > VĂnculos, registros y campos > Segmentos personalizados*.\nn_Para obtener instrucciones más detalladas, [visite nuestro sitio de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
+ customRecordNameFooter: `Puede encontrar nombres de registros personalizados en NetSuite introduciendo el "Campo de columna de transacción" en la búsqueda global.\nn_Para obtener instrucciones más detalladas, [visite nuestro sitio de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
+ customSegmentInternalIDTitle: '¿Cuál es la identificación interna?',
+ customSegmentInternalIDFooter: `En primer lugar, asegĂşrese de que ha habilitado los ID internos en NetSuite en *Inicio > Establecer preferencias > Mostrar ID interno*. *PersonalizaciĂłn > Listas, registros y campos > Segmentos personalizados*.\n2. Haga clic en un segmento personalizado. Haga clic en un segmento personalizado. Haga clic en el hipervĂnculo situado junto a *Tipo de registro personalizado*.\n4. Para obtener instrucciones más detalladas, [visite nuestro sitio de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_LISTS})_.`,
+ customRecordInternalIDFooter: `Puede encontrar IDs internos de registros personalizados en NetSuite siguiendo estos pasos:\n\n1. Introduzca "Campos de lĂnea de transacciĂłn" en la bĂşsqueda global. Haga clic en un registro personalizado. Para obtener instrucciones más detalladas, [visite nuestro sitio de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
+ customSegmentScriptIDTitle: '¿Cuál es el ID del guión?',
+ customSegmentScriptIDFooter: `Puede encontrar IDs de script de segmentos personalizados en NetSuite en: \n\n1. *Personalización > Listas, Registros y Campos > Segmentos Personalizados*.\n2. Haga clic en un segmento personalizado. a. Si desea mostrar el segmento personalizado como una *etiqueta* (a nivel de partida) en Expensify, haga clic en la subpestaña *Columnas de transacción* y utilice el *ID de campo*. b. Si desea mostrar el segmento personalizado como una *etiqueta* (a nivel de partida) en Expensify, haga clic en la subpestaña *Columnas de transacción* y utilice el *ID de campo*. Si desea mostrar el segmento personalizado como un *campo de informe* (a nivel de informe) en Expensify, haga clic en la subpestaña *Transacciones* y utilice el *ID de campo*. Para obtener instrucciones más detalladas, [visite nuestro sitio de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_LISTS})_.`,
+ customRecordScriptIDTitle: '¿Cuál es el ID de columna de la transacción?',
+ customRecordScriptIDFooter: `Puede encontrar IDs de script de registro personalizados en NetSuite en:\n\n1. Introduzca "Campos de lĂnea de transacciĂłn" en la bĂşsqueda global.\n2. Haga clic en un registro personalizado.\n3. Para obtener instrucciones más detalladas, [visite nuestro sitio de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
+ customSegmentMappingTitle: 'ÂżCĂłmo deberĂa mostrarse este segmento personalizado en Expensify?',
+ customRecordMappingTitle: 'ÂżCĂłmo deberĂa mostrarse este registro de segmento personalizado en Expensify?',
+ },
+ errors: {
+ uniqueFieldError: (fieldName: string) => `Ya existe un segmento/registro personalizado con este ${fieldName?.toLowerCase()}.`,
+ },
+ },
+ customLists: {
+ title: 'Listas personalizados',
+ addText: 'Añadir lista personalizado',
+ recordTitle: 'Lista personalizado',
+ helpLink: CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_LISTS,
+ helpLinkText: 'Ver instrucciones detalladas',
+ helpText: ' sobre cĂłmo configurar listas personalizado.',
+ emptyTitle: 'Añadir una lista personalizado',
+ fields: {
+ listName: 'Nombre',
+ internalID: 'IdentificaciĂłn interna',
+ transactionFieldID: 'ID del campo de transacciĂłn',
+ mapping: 'Mostrado como',
+ },
+ removeTitle: 'Eliminar lista personalizado',
+ removePrompt: '¿Está seguro de que desea eliminar esta lista personalizado?',
+ addForm: {
+ listNameTitle: 'Elija una lista personalizada',
+ transactionFieldIDTitle: '¿Cuál es el ID del campo de transacción?',
+ transactionFieldIDFooter: `Puede encontrar los ID de campo de transacciĂłn en NetSuite siguiendo estos pasos:\n\n1. Introduzca "Campos de lĂnea de transacciĂłn" en bĂşsqueda global. Introduzca "Campos de lĂnea de transacciĂłn" en la bĂşsqueda global.\n2. Haga clic en una lista personalizada.\n3. Para obtener instrucciones más detalladas, [visite nuestro sitio de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_LISTS})_.`,
+ mappingTitle: 'ÂżCĂłmo deberĂa mostrarse esta lista personalizada en Expensify?',
+ },
+ errors: {
+ uniqueTransactionFieldIDError: `Ya existe una lista personalizada con este ID de campo de transacciĂłn.`,
+ },
+ },
},
importTypes: {
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: {
@@ -2492,6 +2627,42 @@ export default {
reuseExistingConnection: 'Reutilizar la conexiĂłn existente',
existingConnections: 'Conexiones existentes',
sageIntacctLastSync: (formattedDate: string) => `Sage Intacct - Ăšltima sincronizaciĂłn ${formattedDate}`,
+ employeeDefault: 'Sage Intacct empleado por defecto',
+ employeeDefaultDescription: 'El departamento por defecto del empleado se aplicará a sus gastos en Sage Intacct si existe.',
+ displayedAsTagDescription: 'Se podrá seleccionar el departamento para cada gasto individual en el informe de un empleado.',
+ displayedAsReportFieldDescription: 'La selección de departamento se aplicará a todos los gastos que figuren en el informe de un empleado.',
+ toggleImportTitleFirstPart: 'Elija cĂłmo gestionar Sage Intacct ',
+ toggleImportTitleSecondPart: ' en Expensify.',
+ expenseTypes: 'Tipos de gastos',
+ expenseTypesDescription: 'Los tipos de gastos de Sage Intacct se importan a Expensify como categorĂas.',
+ importTaxDescription: 'Importar el tipo impositivo de compra desde Sage Intacct.',
+ userDefinedDimensions: 'Dimensiones definidas por el usuario',
+ addUserDefinedDimension: 'Añadir dimensión definida por el usuario',
+ integrationName: 'Nombre de la integraciĂłn',
+ dimensionExists: 'Ya existe una dimensiĂłn con ese nombre.',
+ removeDimension: 'Eliminar dimensiĂłn definida por el usuario',
+ removeDimensionPrompt: 'Está seguro de que desea eliminar esta dimensión definida por el usuario?',
+ userDefinedDimension: 'DimensiĂłn definida por el usuario',
+ addAUserDefinedDimension: 'Añadir una dimensión definida por el usuario',
+ detailedInstructionsLink: 'Ver instrucciones detalladas',
+ detailedInstructionsRestOfSentence: ' para añadir dimensiones definidas por el usuario.',
+ userDimensionsAdded: (dimensionsCount: number) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} añadido`,
+ mappingTitle: (mappingName: SageIntacctMappingName): string => {
+ switch (mappingName) {
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS:
+ return 'departamentos';
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CLASSES:
+ return 'clases';
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.LOCATIONS:
+ return 'lugares';
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CUSTOMERS:
+ return 'clientes';
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.PROJECTS:
+ return 'proyectos (empleos)';
+ default:
+ return 'asignaciones';
+ }
+ },
},
type: {
free: 'Gratis',
@@ -2608,8 +2779,10 @@ export default {
},
reportFields: {
addField: 'Añadir campo',
- delete: 'Eliminar campos',
- deleteConfirmation: '¿Estás seguro de que quieres eliminar esta campos?',
+ delete: 'Eliminar campo',
+ deleteFields: 'Eliminar campos',
+ deleteConfirmation: '¿Está seguro de que desea eliminar este campo del informe?',
+ deleteFieldsConfirmation: '¿Está seguro de que desea eliminar estos campos del informe?',
emptyReportFields: {
title: 'No has creado ningĂşn campo de informe',
subtitle: 'Añade un campo personalizado (texto, fecha o desplegable) que aparezca en los informes.',
@@ -2867,6 +3040,8 @@ export default {
return 'Actualizando empleados';
case 'quickbooksOnlineSyncApplyClassesLocations':
return 'Actualizando clases';
+ case 'jobDone':
+ return 'Esperando a que se carguen los datos importados';
case 'xeroSyncImportChartOfAccounts':
return 'Sincronizando plan de cuentas';
case 'xeroSyncImportCategories':
@@ -2922,6 +3097,7 @@ export default {
case 'intacctImportTitle':
return 'Importando datos desde Sage Intacct';
default: {
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `Translation missing for stage: ${stage}`;
}
}
diff --git a/src/libs/API/parameters/CreateWorkspaceReportFieldListValueParams.ts b/src/libs/API/parameters/CreateWorkspaceReportFieldListValueParams.ts
new file mode 100644
index 000000000000..950287bc5d04
--- /dev/null
+++ b/src/libs/API/parameters/CreateWorkspaceReportFieldListValueParams.ts
@@ -0,0 +1,10 @@
+type CreateWorkspaceReportFieldListValueParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array
+ */
+ reportFields: string;
+};
+
+export default CreateWorkspaceReportFieldListValueParams;
diff --git a/src/libs/API/parameters/CreateWorkspaceReportFieldParams.ts b/src/libs/API/parameters/CreateWorkspaceReportFieldParams.ts
index 13844a279905..33692d210959 100644
--- a/src/libs/API/parameters/CreateWorkspaceReportFieldParams.ts
+++ b/src/libs/API/parameters/CreateWorkspaceReportFieldParams.ts
@@ -1,5 +1,9 @@
type CreateWorkspaceReportFieldParams = {
policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array
+ */
reportFields: string;
};
diff --git a/src/libs/API/parameters/EnableWorkspaceReportFieldListValueParams.ts b/src/libs/API/parameters/EnableWorkspaceReportFieldListValueParams.ts
new file mode 100644
index 000000000000..7c54a2f4c68b
--- /dev/null
+++ b/src/libs/API/parameters/EnableWorkspaceReportFieldListValueParams.ts
@@ -0,0 +1,10 @@
+type EnableWorkspaceReportFieldListValueParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array
+ */
+ reportFields: string;
+};
+
+export default EnableWorkspaceReportFieldListValueParams;
diff --git a/src/libs/API/parameters/RemoveWorkspaceReportFieldListValueParams.ts b/src/libs/API/parameters/RemoveWorkspaceReportFieldListValueParams.ts
new file mode 100644
index 000000000000..94d90a8dbaae
--- /dev/null
+++ b/src/libs/API/parameters/RemoveWorkspaceReportFieldListValueParams.ts
@@ -0,0 +1,10 @@
+type RemoveWorkspaceReportFieldListValueParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array
+ */
+ reportFields: string;
+};
+
+export default RemoveWorkspaceReportFieldListValueParams;
diff --git a/src/libs/API/parameters/Search.ts b/src/libs/API/parameters/Search.ts
index 0a8345b0b7e0..60ea54419492 100644
--- a/src/libs/API/parameters/Search.ts
+++ b/src/libs/API/parameters/Search.ts
@@ -1,4 +1,4 @@
-import type {SortOrder} from '@libs/SearchUtils';
+import type {SortOrder} from '@components/Search/types';
type SearchParams = {
hash: number;
diff --git a/src/libs/API/parameters/UpdateSageIntacctGenericParams.ts b/src/libs/API/parameters/UpdateSageIntacctGenericParams.ts
new file mode 100644
index 000000000000..5ac0ccc21218
--- /dev/null
+++ b/src/libs/API/parameters/UpdateSageIntacctGenericParams.ts
@@ -0,0 +1,7 @@
+type UpdateSageIntacctGenericParams = {
+ [K2 in K]: Type;
+} & {
+ policyID: string;
+};
+
+export default UpdateSageIntacctGenericParams;
diff --git a/src/libs/API/parameters/UpdateSageIntacctGenericTypeParams.ts b/src/libs/API/parameters/UpdateSageIntacctGenericTypeParams.ts
new file mode 100644
index 000000000000..509a06cadb2c
--- /dev/null
+++ b/src/libs/API/parameters/UpdateSageIntacctGenericTypeParams.ts
@@ -0,0 +1,7 @@
+type UpdateSageIntacctGenericTypeParams = {
+ [K2 in K]: Type;
+} & {
+ policyID: string;
+};
+
+export default UpdateSageIntacctGenericTypeParams;
diff --git a/src/libs/API/parameters/UpdateWorkspaceReportFieldInitialValueParams.ts b/src/libs/API/parameters/UpdateWorkspaceReportFieldInitialValueParams.ts
new file mode 100644
index 000000000000..a72781ff1c37
--- /dev/null
+++ b/src/libs/API/parameters/UpdateWorkspaceReportFieldInitialValueParams.ts
@@ -0,0 +1,10 @@
+type UpdateWorkspaceReportFieldInitialValueParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array
+ */
+ reportFields: string;
+};
+
+export default UpdateWorkspaceReportFieldInitialValueParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index edd678914a99..ff8465cfeec7 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -245,7 +245,12 @@ export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSu
export type {default as PolicyReportFieldsReplace} from './PolicyReportFieldsReplace';
export type {default as ConnectPolicyToNetSuiteParams} from './ConnectPolicyToNetSuiteParams';
export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspaceReportFieldParams';
+export type {default as UpdateWorkspaceReportFieldInitialValueParams} from './UpdateWorkspaceReportFieldInitialValueParams';
+export type {default as EnableWorkspaceReportFieldListValueParams} from './EnableWorkspaceReportFieldListValueParams';
+export type {default as CreateWorkspaceReportFieldListValueParams} from './CreateWorkspaceReportFieldListValueParams';
+export type {default as RemoveWorkspaceReportFieldListValueParams} from './RemoveWorkspaceReportFieldListValueParams';
export type {default as OpenPolicyExpensifyCardsPageParams} from './OpenPolicyExpensifyCardsPageParams';
export type {default as RequestExpensifyCardLimitIncreaseParams} from './RequestExpensifyCardLimitIncreaseParams';
export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams';
export type {default as UpdateNetSuiteCustomFormIDParams} from './UpdateNetSuiteCustomFormIDParams';
+export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIntacctGenericTypeParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index eb536deddd23..ca284321e3bb 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -1,5 +1,6 @@
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
+import type {SageIntacctDimension, SageIntacctMappingValue} from '@src/types/onyx/Policy';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import type * as Parameters from './parameters';
import type SignInUserParams from './parameters/SignInUserParams';
@@ -131,7 +132,7 @@ const WRITE_COMMANDS = {
RENAME_POLICY_TAG: 'RenamePolicyTag',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories',
- POLICY_REPORT_FIELDS_REPLACE: 'Policy_ReportFields_Replace',
+ DELETE_POLICY_REPORT_FIELD: 'DeletePolicyReportField',
SET_POLICY_TAGS_REQUIRED: 'SetPolicyTagsRequired',
SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag',
RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist',
@@ -238,6 +239,10 @@ const WRITE_COMMANDS = {
REQUEST_REFUND: 'User_RefundPurchase',
UPDATE_NETSUITE_SUBSIDIARY: 'UpdateNetSuiteSubsidiary',
CREATE_WORKSPACE_REPORT_FIELD: 'CreatePolicyReportField',
+ UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE: 'SetPolicyReportFieldDefault',
+ ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'EnablePolicyReportFieldOption',
+ CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'CreatePolicyReportFieldOption',
+ REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'RemovePolicyReportFieldOption',
UPDATE_NETSUITE_SYNC_TAX_CONFIGURATION: 'UpdateNetSuiteSyncTaxConfiguration',
UPDATE_NETSUITE_CROSS_SUBSIDIARY_CUSTOMER_CONFIGURATION: 'UpdateNetSuiteCrossSubsidiaryCustomerConfiguration',
UPDATE_NETSUITE_DEPARTMENTS_MAPPING: 'UpdateNetSuiteDepartmentsMapping',
@@ -260,6 +265,8 @@ const WRITE_COMMANDS = {
UPDATE_NETSUITE_TAX_POSTING_ACCOUNT: 'UpdateNetSuiteTaxPostingAccount',
UPDATE_NETSUITE_ALLOW_FOREIGN_CURRENCY: 'UpdateNetSuiteAllowForeignCurrency',
UPDATE_NETSUITE_EXPORT_TO_NEXT_OPEN_PERIOD: 'UpdateNetSuiteExportToNextOpenPeriod',
+ UPDATE_NETSUITE_CUSTOM_SEGMENTS: 'UpdateNetSuiteCustomSegments',
+ UPDATE_NETSUITE_CUSTOM_LISTS: 'UpdateNetSuiteCustomLists',
UPDATE_NETSUITE_AUTO_SYNC: 'UpdateNetSuiteAutoSync',
UPDATE_NETSUITE_SYNC_REIMBURSED_REPORTS: 'UpdateNetSuiteSyncReimbursedReports',
UPDATE_NETSUITE_SYNC_PEOPLE: 'UpdateNetSuiteSyncPeople',
@@ -276,8 +283,29 @@ const WRITE_COMMANDS = {
UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE: 'UpdateNetSuiteCustomFormIDOptionsNonReimbursable',
REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE: 'RequestExpensifyCardLimitIncrease',
CONNECT_POLICY_TO_SAGE_INTACCT: 'ConnectPolicyToSageIntacct',
+ UPDATE_SAGE_INTACCT_AUTO_SYNC: 'UpdateSageIntacctAutoSync',
+ UPDATE_SAGE_INTACCT_IMPORT_EMPLOYEES: 'UpdateSageIntacctImportEmployees',
+ UPDATE_SAGE_INTACCT_APPROVAL_MODE: 'UpdateSageIntacctApprovalMode',
+ UPDATE_SAGE_INTACCT_SYNC_REIMBURSED_REPORTS: 'UpdateSageIntacctSyncReimbursedReports',
+ UPDATE_SAGE_INTACCT_SYNC_REIMBURSEMENT_ACCOUNT_ID: 'UpdateSageIntacctSyncReimbursementAccountID',
CONNECT_POLICY_TO_NETSUITE: 'ConnectPolicyToNetSuite',
CLEAR_OUTSTANDING_BALANCE: 'ClearOutstandingBalance',
+ UPDATE_SAGE_INTACCT_BILLABLE: 'UpdateSageIntacctBillable',
+ UPDATE_SAGE_INTACCT_DEPARTMENT_MAPPING: 'UpdateSageIntacctDepartmentsMapping',
+ UPDATE_SAGE_INTACCT_CLASSES_MAPPING: 'UpdateSageIntacctClassesMapping',
+ UPDATE_SAGE_INTACCT_LOCATIONS_MAPPING: 'UpdateSageIntacctLocationsMapping',
+ UPDATE_SAGE_INTACCT_CUSTOMERS_MAPPING: 'UpdateSageIntacctCustomersMapping',
+ UPDATE_SAGE_INTACCT_PROJECTS_MAPPING: 'UpdateSageIntacctProjectsMapping',
+ UPDATE_SAGE_INTACCT_SYNC_TAX_CONFIGURATION: 'UpdateSageIntacctSyncTaxConfiguration',
+ UPDATE_SAGE_INTACCT_USER_DIMENSION: 'UpdateSageIntacctUserDimension',
+ UPDATE_SAGE_INTACCT_EXPORTER: 'UpdateSageIntacctExporter',
+ UPDATE_SAGE_INTACCT_EXPORT_DATE: 'UpdateSageIntacctExportDate',
+ UPDATE_SAGE_INTACCT_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'UpdateSageIntacctReimbursableExpensesExportDestination',
+ UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'UpdateSageIntacctNonreimbursableExpensesExportDestination',
+ UPDATE_SAGE_INTACCT_REIMBURSABLE_EXPENSES_REPORT_EXPORT_DEFAULT_VENDOR: 'UpdateSageIntacctReimbursableExpensesReportExportDefaultVendor',
+ UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_CREDIT_CARD_CHARGE_EXPORT_DEFAULT_VENDOR: 'UpdateSageIntacctNonreimbursableExpensesCreditCardChargeExportDefaultVendor',
+ UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_ACCOUNT: 'UpdateSageIntacctNonreimbursableExpensesExportAccount',
+ UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_VENDOR: 'UpdateSageIntacctNonreimbursableExpensesExportVendor',
} as const;
type WriteCommand = ValueOf;
@@ -398,7 +426,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams;
- [WRITE_COMMANDS.POLICY_REPORT_FIELDS_REPLACE]: Parameters.PolicyReportFieldsReplace;
+ [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.PolicyReportFieldsReplace;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
[WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED]: Parameters.SetPolicyTagsRequired;
[WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglistParams;
@@ -517,14 +545,32 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.REQUEST_REFUND]: null;
[WRITE_COMMANDS.CONNECT_POLICY_TO_SAGE_INTACCT]: Parameters.ConnectPolicyToSageIntacctParams;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_EXPORTER]: Parameters.UpdateSageIntacctGenericTypeParams<'email', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_EXPORT_DATE]: Parameters.UpdateSageIntacctGenericTypeParams<'value', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateSageIntacctGenericTypeParams<'value', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateSageIntacctGenericTypeParams<'value', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_REIMBURSABLE_EXPENSES_REPORT_EXPORT_DEFAULT_VENDOR]: Parameters.UpdateSageIntacctGenericTypeParams<'vendorID', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_CREDIT_CARD_CHARGE_EXPORT_DEFAULT_VENDOR]: Parameters.UpdateSageIntacctGenericTypeParams<'vendorID', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_ACCOUNT]: Parameters.UpdateSageIntacctGenericTypeParams<'creditCardAccountID', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_VENDOR]: Parameters.UpdateSageIntacctGenericTypeParams<'vendorID', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_AUTO_SYNC]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_IMPORT_EMPLOYEES]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_APPROVAL_MODE]: Parameters.UpdateSageIntacctGenericTypeParams<'value', string>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_REIMBURSED_REPORTS]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_REIMBURSEMENT_ACCOUNT_ID]: Parameters.UpdateSageIntacctGenericTypeParams<'vendorID', string>;
[WRITE_COMMANDS.UPGRADE_TO_CORPORATE]: Parameters.UpgradeToCorporateParams;
+
// Netsuite parameters
[WRITE_COMMANDS.UPDATE_NETSUITE_SUBSIDIARY]: Parameters.UpdateNetSuiteSubsidiaryParams;
[WRITE_COMMANDS.CONNECT_POLICY_TO_NETSUITE]: Parameters.ConnectPolicyToNetSuiteParams;
// Workspace report field parameters
[WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD]: Parameters.CreateWorkspaceReportFieldParams;
+ [WRITE_COMMANDS.UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE]: Parameters.UpdateWorkspaceReportFieldInitialValueParams;
+ [WRITE_COMMANDS.ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.EnableWorkspaceReportFieldListValueParams;
+ [WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.CreateWorkspaceReportFieldListValueParams;
+ [WRITE_COMMANDS.REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.RemoveWorkspaceReportFieldListValueParams;
[WRITE_COMMANDS.UPDATE_NETSUITE_SYNC_TAX_CONFIGURATION]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_NETSUITE_CROSS_SUBSIDIARY_CUSTOMER_CONFIGURATION]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
@@ -548,6 +594,8 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_NETSUITE_TAX_POSTING_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'bankAccountID', string>;
[WRITE_COMMANDS.UPDATE_NETSUITE_ALLOW_FOREIGN_CURRENCY]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_TO_NEXT_OPEN_PERIOD]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_SEGMENTS]: Parameters.UpdateNetSuiteGenericTypeParams<'customSegments', string>; // JSON string NetSuiteCustomSegment[]
+ [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_LISTS]: Parameters.UpdateNetSuiteGenericTypeParams<'customLists', string>; // JSON string NetSuiteCustomList[]
[WRITE_COMMANDS.UPDATE_NETSUITE_AUTO_SYNC]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_NETSUITE_SYNC_REIMBURSED_REPORTS]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_NETSUITE_SYNC_PEOPLE]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
@@ -562,6 +610,14 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_NETSUITE_APPROVAL_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'value', string>;
[WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE]: Parameters.UpdateNetSuiteCustomFormIDParams;
[WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE]: Parameters.UpdateNetSuiteCustomFormIDParams;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_BILLABLE]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_DEPARTMENT_MAPPING]: Parameters.UpdateSageIntacctGenericTypeParams<'mapping', SageIntacctMappingValue>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_CLASSES_MAPPING]: Parameters.UpdateSageIntacctGenericTypeParams<'mapping', SageIntacctMappingValue>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_LOCATIONS_MAPPING]: Parameters.UpdateSageIntacctGenericTypeParams<'mapping', SageIntacctMappingValue>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_CUSTOMERS_MAPPING]: Parameters.UpdateSageIntacctGenericTypeParams<'mapping', SageIntacctMappingValue>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_PROJECTS_MAPPING]: Parameters.UpdateSageIntacctGenericTypeParams<'mapping', SageIntacctMappingValue>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_TAX_CONFIGURATION]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_USER_DIMENSION]: Parameters.UpdateSageIntacctGenericTypeParams<'dimensions', SageIntacctDimension[]>;
};
const READ_COMMANDS = {
diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts
index be7ce9aca8b5..862b0ae5e928 100644
--- a/src/libs/CurrencyUtils.ts
+++ b/src/libs/CurrencyUtils.ts
@@ -97,11 +97,11 @@ function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string
*
* @note we do not support any currencies with more than two decimal places.
*/
-function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string = CONST.CURRENCY.USD): string {
+function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string = CONST.CURRENCY.USD, withDecimals = true): string {
if (amountAsInt === null || amountAsInt === undefined) {
return '';
}
- const decimals = getCurrencyDecimals(currency);
+ const decimals = withDecimals ? getCurrencyDecimals(currency) : 0;
return convertToFrontendAmountAsInteger(amountAsInt, currency).toFixed(decimals);
}
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index 8a8888902e92..0b4a847f3733 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -246,7 +246,7 @@ function datetimeToCalendarTime(locale: Locale, datetime: string, includeTimeZon
*/
function datetimeToRelative(locale: Locale, datetime: string): string {
const date = getLocalDateFromDatetime(locale, datetime);
- return formatDistanceToNow(date, {addSuffix: true});
+ return formatDistanceToNow(date, {addSuffix: true, locale: locale === CONST.LOCALES.EN ? enGB : es});
}
/**
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 856e2e090ebb..0b94972b2aa9 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -250,7 +250,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/tags/WorkspaceEditTagsPage').default,
[SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../../pages/workspace/tags/WorkspaceCreateTagPage').default,
[SCREENS.WORKSPACE.TAG_EDIT]: () => require('../../../../pages/workspace/tags/EditTagPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldSettingsPage').default,
[SCREENS.WORKSPACE.TAXES_SETTINGS]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsPage').default,
[SCREENS.WORKSPACE.TAXES_SETTINGS_CUSTOM_TAX_NAME]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName').default,
[SCREENS.WORKSPACE.TAXES_SETTINGS_FOREIGN_CURRENCY_DEFAULT]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency').default,
@@ -326,6 +325,16 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteTokenInputPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT]: () => require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_MAPPING]: () => require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD]: () =>
+ require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_VIEW]: () =>
+ require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_EDIT]: () =>
+ require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_ADD]: () =>
+ require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD]: () =>
+ require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS]: () =>
require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectsPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT]: () =>
@@ -368,10 +377,25 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteApprovalAccountSelectPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_CUSTOM_FORM_ID]: () => require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage').default,
- [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: () => require('../../../../pages/workspace/accounting/intacct/IntacctPrerequisitesPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: () => require('../../../../pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS]: () =>
require('../../../../pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.EXISTING_SAGE_INTACCT_CONNECTIONS]: () => require('../../../../pages/workspace/accounting/intacct/ExistingConnectionsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT]: () => require('../../../../pages/workspace/accounting/intacct/export/SageIntacctExportPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREFERRED_EXPORTER]: () =>
+ require('../../../../pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT_DATE]: () => require('../../../../pages/workspace/accounting/intacct/export/SageIntacctDatePage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_EXPENSES]: () =>
+ require('../../../../pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES]: () =>
+ require('../../../../pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_DEFAULT_VENDOR]: () =>
+ require('../../../../pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT]: () =>
+ require('../../../../pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED]: () => require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: () =>
+ require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default,
[SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../../pages/workspace/taxes/WorkspaceEditTaxPage').default,
@@ -384,11 +408,24 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default,
[SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard').default,
[SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/WorkspaceCreateReportFieldPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/CreateReportFieldPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldSettingsPage').default,
[SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reportFields/ReportFieldListValuesPage').default,
[SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldAddListValuePage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ValueSettingsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldValueSettingsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldInitialValuePage').default,
[SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldEditValuePage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctImportPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING]: () =>
+ require('../../../../pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_MAPPING_TYPE]: () =>
+ require('../../../../pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_USER_DIMENSIONS]: () =>
+ require('../../../../pages/workspace/accounting/intacct/import/SageIntacctUserDimensionsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADD_USER_DIMENSION]: () =>
+ require('../../../../pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EDIT_USER_DIMENSION]: () =>
+ require('../../../../pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage').default,
});
const EnablePaymentsStackNavigator = createModalStackNavigator({
@@ -444,6 +481,7 @@ const TransactionDuplicateStackNavigator = createModalStackNavigator({
[SCREENS.SEARCH.REPORT_RHP]: () => require('../../../../pages/home/ReportScreen').default,
+ [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: () => require('../../../../pages/Search/SearchHoldReasonPage').default,
});
const RestrictedActionModalStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx
index 29a2205b2e37..61adcd77da76 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx
@@ -4,9 +4,11 @@ import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import NoDropZone from '@components/DragAndDrop/NoDropZone';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
+import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useOnboardingLayout from '@hooks/useOnboardingLayout';
import useThemeStyles from '@hooks/useThemeStyles';
+import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import OnboardingModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/OnboardingModalNavigatorScreenOptions';
import Navigation from '@libs/Navigation/Navigation';
import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types';
@@ -26,15 +28,11 @@ function OnboardingModalNavigator() {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useOnboardingLayout();
const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
- selector: (onboarding) => {
- // onboarding is an array for old accounts and accounts created from olddot
- if (Array.isArray(onboarding)) {
- return true;
- }
- return onboarding?.hasCompletedGuidedSetupFlow;
- },
+ selector: hasCompletedGuidedSetupFlowSelector,
});
+ useDisableModalDismissOnEscape();
+
useEffect(() => {
if (!hasCompletedGuidedSetupFlow) {
return;
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx
index 8c531a918af8..2e1c4c012156 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx
@@ -15,7 +15,9 @@ import * as Session from '@libs/actions/Session';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
-import Navigation from '@libs/Navigation/Navigation';
+import linkingConfig from '@libs/Navigation/linkingConfig';
+import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
+import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import type {RootStackParamList, State} from '@libs/Navigation/types';
import {isCentralPaneName} from '@libs/NavigationUtils';
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
@@ -53,7 +55,12 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
return;
}
- Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_ROOT)});
+ Welcome.isOnboardingFlowCompleted({
+ onNotCompleted: () => {
+ const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config);
+ navigationRef.resetRoot(adaptedState);
+ },
+ });
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isLoadingApp]);
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts
index a1768df5e0d6..5b3cefb63a2d 100644
--- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts
+++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts
@@ -1,13 +1,16 @@
-import type {RouterConfigOptions, StackNavigationState} from '@react-navigation/native';
-import {getPathFromState, StackRouter} from '@react-navigation/native';
+import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native';
+import {findFocusedRoute, getPathFromState, StackRouter} from '@react-navigation/native';
import type {ParamListBase} from '@react-navigation/routers';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
+import * as Localize from '@libs/Localize';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import linkingConfig from '@libs/Navigation/linkingConfig';
import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
-import {isCentralPaneName} from '@libs/NavigationUtils';
+import {isCentralPaneName, isOnboardingFlowName} from '@libs/NavigationUtils';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import type {ResponsiveStackNavigatorRouterOptions} from './types';
@@ -97,6 +100,23 @@ function compareAndAdaptState(state: StackNavigationState) {
}
}
+function shouldPreventReset(state: StackNavigationState, action: CommonActions.Action | StackActionType) {
+ if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) {
+ return false;
+ }
+ const currentFocusedRoute = findFocusedRoute(state);
+ const targetFocusedRoute = findFocusedRoute(action?.payload);
+
+ // We want to prevent the user from navigating back to a non-onboarding screen if they are currently on an onboarding screen
+ if (isOnboardingFlowName(currentFocusedRoute?.name) && !isOnboardingFlowName(targetFocusedRoute?.name)) {
+ Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
+ // We reset the URL as the browser sets it in a way that doesn't match the navigation state
+ // eslint-disable-next-line no-restricted-globals
+ history.replaceState({}, '', getPathFromState(state, linkingConfig.config));
+ return true;
+ }
+}
+
function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) {
const stackRouter = StackRouter(options);
@@ -107,6 +127,12 @@ function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) {
const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList});
return state;
},
+ getStateForAction(state: StackNavigationState, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) {
+ if (shouldPreventReset(state, action)) {
+ return state;
+ }
+ return stackRouter.getStateForAction(state, action, configOptions);
+ },
};
}
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index 15d4ac6e4b31..6f803ae1e497 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -16,10 +16,12 @@ import type {Report} from '@src/types/onyx';
import originalCloseRHPFlow from './closeRHPFlow';
import originalDismissModal from './dismissModal';
import originalDismissModalWithReport from './dismissModalWithReport';
+import getTopmostBottomTabRoute from './getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import originalGetTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
+import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState';
import linkTo from './linkTo';
import navigationRef from './navigationRef';
import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotaskQueue';
@@ -38,8 +40,8 @@ let shouldPopAllStateOnUP = false;
/**
* Inform the navigation that next time user presses UP we should pop all the state back to LHN.
*/
-function setShouldPopAllStateOnUP() {
- shouldPopAllStateOnUP = true;
+function setShouldPopAllStateOnUP(shouldPopAllStateFlag: boolean) {
+ shouldPopAllStateOnUP = shouldPopAllStateFlag;
}
function canNavigate(methodName: string, params: Record = {}): boolean {
@@ -229,16 +231,40 @@ function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopT
const isCentralPaneFocused = isCentralPaneName(findFocusedRoute(navigationRef.current.getState())?.name);
const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute ?? '');
- // Allow CentralPane to use UP with fallback route if the path is not found in root navigator.
- if (isCentralPaneFocused && fallbackRoute && distanceFromPathInRootNavigator === -1) {
- navigate(fallbackRoute, CONST.NAVIGATION.TYPE.UP);
- return;
+ if (isCentralPaneFocused && fallbackRoute) {
+ // Allow CentralPane to use UP with fallback route if the path is not found in root navigator.
+ if (distanceFromPathInRootNavigator === -1) {
+ navigate(fallbackRoute, CONST.NAVIGATION.TYPE.UP);
+ return;
+ }
+
+ // Add possibility to go back more than one screen in root navigator if that screen is on the stack.
+ if (distanceFromPathInRootNavigator > 0) {
+ navigationRef.current.dispatch(StackActions.pop(distanceFromPathInRootNavigator));
+ return;
+ }
}
- // Add possibility to go back more than one screen in root navigator if that screen is on the stack.
- if (isCentralPaneFocused && fallbackRoute && distanceFromPathInRootNavigator > 0) {
- navigationRef.current.dispatch(StackActions.pop(distanceFromPathInRootNavigator));
- return;
+ // If the central pane is focused, it's possible that we navigated from other central pane with different matching bottom tab.
+ if (isCentralPaneFocused) {
+ const rootState = navigationRef.getRootState();
+ const stateAfterPop = {routes: rootState.routes.slice(0, -1)} as State;
+ const topmostCentralPaneRouteAfterPop = getTopmostCentralPaneRoute(stateAfterPop);
+
+ const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState as State);
+ const matchingBottomTabRoute = getMatchingBottomTabRouteForState(stateAfterPop);
+
+ // If the central pane is defined after the pop action, we need to check if it's synced with the bottom tab screen.
+ // If not, we need to pop to the bottom tab screen/screens to sync it with the new central pane.
+ if (topmostCentralPaneRouteAfterPop && topmostBottomTabRoute?.name !== matchingBottomTabRoute.name) {
+ const bottomTabNavigator = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state;
+
+ if (bottomTabNavigator && bottomTabNavigator.index) {
+ const matchingIndex = bottomTabNavigator.routes.findLastIndex((item) => item.name === matchingBottomTabRoute.name);
+ const indexToPop = matchingIndex !== -1 ? bottomTabNavigator.index - matchingIndex : undefined;
+ navigationRef.current.dispatch({...StackActions.pop(indexToPop), target: bottomTabNavigator?.key});
+ }
+ }
}
navigationRef.current.goBack();
diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx
index db64aea7ffe8..a225831b56ff 100644
--- a/src/libs/Navigation/NavigationRoot.tsx
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -1,6 +1,7 @@
import type {NavigationState} from '@react-navigation/native';
import {DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native';
import React, {useContext, useEffect, useMemo, useRef} from 'react';
+import {useOnyx} from 'react-native-onyx';
import HybridAppMiddleware from '@components/HybridAppMiddleware';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
@@ -8,11 +9,14 @@ import useCurrentReportID from '@hooks/useCurrentReportID';
import useTheme from '@hooks/useTheme';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {FSPage} from '@libs/Fullstory';
+import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import Log from '@libs/Log';
import {getPathFromURL} from '@libs/Url';
import {updateLastVisitedPath} from '@userActions/App';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
import AppNavigator from './AppNavigator';
import getPolicyIDFromState from './getPolicyIDFromState';
import linkingConfig from './linkingConfig';
@@ -77,25 +81,37 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
const {isSmallScreenWidth} = useWindowDimensions();
const {setActiveWorkspaceID} = useActiveWorkspace();
- const initialState = useMemo(
- () => {
- if (!lastVisitedPath) {
- return undefined;
- }
+ const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
+ selector: hasCompletedGuidedSetupFlowSelector,
+ });
- const path = initialUrl ? getPathFromURL(initialUrl) : null;
+ const initialState = useMemo(() => {
+ // If the user haven't completed the flow, we want to always redirect them to the onboarding flow.
+ if (!hasCompletedGuidedSetupFlow) {
+ const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config);
+ return adaptedState;
+ }
- // For non-nullable paths we don't want to set initial state
- if (path) {
- return;
- }
+ // If there is no lastVisitedPath, we can do early return. We won't modify the default behavior.
+ if (!lastVisitedPath) {
+ return undefined;
+ }
- const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config);
- return adaptedState;
- },
+ const path = initialUrl ? getPathFromURL(initialUrl) : null;
+
+ // If the user opens the root of app "/" it will be parsed to empty string "".
+ // If the path is defined and different that empty string we don't want to modify the default behavior.
+ if (path) {
+ return;
+ }
+
+ // Otherwise we want to redirect the user to the last visited path.
+ const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config);
+ return adaptedState;
+
+ // The initialState value is relevant only on the first render.
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- [],
- );
+ }, []);
// https://reactnavigation.org/docs/themes
const navigationTheme = useMemo(
@@ -119,10 +135,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
firstRenderRef.current = false;
return;
}
- if (!isSmallScreenWidth) {
- return;
- }
- Navigation.setShouldPopAllStateOnUP();
+
+ Navigation.setShouldPopAllStateOnUP(!isSmallScreenWidth);
}, [isSmallScreenWidth]);
const handleStateChange = (state: NavigationState | undefined) => {
@@ -154,7 +168,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
}}
>
{/* HybridAppMiddleware needs to have access to navigation ref and SplashScreenHidden context */}
-
+
diff --git a/src/libs/Navigation/isReportOpenInRHP.ts b/src/libs/Navigation/isReportOpenInRHP.ts
new file mode 100644
index 000000000000..51e8a95bb66b
--- /dev/null
+++ b/src/libs/Navigation/isReportOpenInRHP.ts
@@ -0,0 +1,17 @@
+import type {NavigationState} from '@react-navigation/native';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+
+const isReportOpenInRHP = (state: NavigationState | undefined): boolean => {
+ const lastRoute = state?.routes?.at(-1);
+ if (!lastRoute) {
+ return false;
+ }
+ const params = lastRoute.params;
+ if (params && 'screen' in params && typeof params.screen === 'string' && params.screen === SCREENS.RIGHT_MODAL.SEARCH_REPORT) {
+ return true;
+ }
+ return !!(lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state?.routes?.some((route) => route?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT));
+};
+
+export default isReportOpenInRHP;
diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts
index d8312937ed6f..ddef2b1411b3 100644
--- a/src/libs/Navigation/linkTo/index.ts
+++ b/src/libs/Navigation/linkTo/index.ts
@@ -3,6 +3,7 @@ import type {NavigationContainerRef, NavigationState, PartialState} from '@react
import {findFocusedRoute} from '@react-navigation/native';
import {omitBy} from 'lodash';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
+import isReportOpenInRHP from '@libs/Navigation/isReportOpenInRHP';
import extractPolicyIDsFromState from '@libs/Navigation/linkingConfig/extractPolicyIDsFromState';
import {isCentralPaneName} from '@libs/NavigationUtils';
import shallowCompare from '@libs/ObjectUtils';
@@ -68,7 +69,7 @@ export default function linkTo(navigation: NavigationContainerRef route?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT);
+ const isReportInRhpOpened = isReportOpenInRHP(rootState);
// If action type is different than NAVIGATE we can't change it to the PUSH safely
if (action?.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) {
diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
index 1192e4649ea0..83929b7e7d02 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -38,7 +38,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> =
[SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS],
[SCREENS.SETTINGS.SAVE_THE_WORLD]: [SCREENS.I_KNOW_A_TEACHER, SCREENS.INTRO_SCHOOL_PRINCIPAL, SCREENS.I_AM_A_TEACHER],
[SCREENS.SETTINGS.TROUBLESHOOT]: [SCREENS.SETTINGS.CONSOLE],
- [SCREENS.SEARCH.CENTRAL_PANE]: [SCREENS.SEARCH.REPORT_RHP],
+ [SCREENS.SEARCH.CENTRAL_PANE]: [SCREENS.SEARCH.REPORT_RHP, SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP],
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [
SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD,
SCREENS.SETTINGS.SUBSCRIPTION.SIZE,
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 75b096fa4bbe..54804a495754 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -66,6 +66,11 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TOKEN_INPUT,
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT,
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_MAPPING,
+ SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD,
+ SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_VIEW,
+ SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_EDIT,
+ SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_ADD,
+ SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD,
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS,
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT,
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT,
@@ -92,6 +97,21 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES,
SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS,
SCREENS.WORKSPACE.ACCOUNTING.EXISTING_SAGE_INTACCT_CONNECTIONS,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_MAPPING_TYPE,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_USER_DIMENSIONS,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADD_USER_DIMENSION,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EDIT_USER_DIMENSION,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREFERRED_EXPORTER,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT_DATE,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_EXPENSES,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_DEFAULT_VENDOR,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED,
+ SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT,
],
[SCREENS.WORKSPACE.TAXES]: [
SCREENS.WORKSPACE.TAXES_SETTINGS,
@@ -122,14 +142,15 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS,
],
[SCREENS.WORKSPACE.REPORT_FIELDS]: [
- SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS,
SCREENS.WORKSPACE.REPORT_FIELDS_CREATE,
+ SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS,
SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES,
SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE,
SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS,
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE,
+ SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
],
- [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [],
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW],
};
export default FULL_SCREEN_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 699cb9b704e1..c0d1a79f635f 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -357,6 +357,11 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TOKEN_INPUT]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT.route},
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT.route},
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_MAPPING]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_MAPPING.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_VIEW]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_VIEW.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_EDIT]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_EDIT.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_ADD]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_ADD.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD.route},
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS.route},
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT.route},
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT]: {
@@ -425,6 +430,23 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES.route},
[SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ENTER_CREDENTIALS.route},
[SCREENS.WORKSPACE.ACCOUNTING.EXISTING_SAGE_INTACCT_CONNECTIONS]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_MAPPING_TYPE]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_USER_DIMENSIONS]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADD_USER_DIMENSION]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADD_USER_DIMENSION.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EDIT_USER_DIMENSION]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EDIT_USER_DIMENSION.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREFERRED_EXPORTER]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREFERRED_EXPORTER.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT_DATE]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT_DATE.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_EXPENSES]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_DEFAULT_VENDOR]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT]: {
+ path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
},
@@ -438,7 +460,7 @@ const config: LinkingOptions['config'] = {
path: ROUTES.WORKSPACE_PROFILE_SHARE.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: {
- path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW,
+ path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.route,
},
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
path: ROUTES.WORKSPACE_RATE_AND_UNIT.route,
@@ -547,12 +569,6 @@ const config: LinkingOptions['config'] = {
orderWeight: Number,
},
},
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_SETTINGS.route,
- parse: {
- reportFieldName: (reportFieldKey: string) => decodeURIComponent(reportFieldKey),
- },
- },
[SCREENS.WORKSPACE.TAXES_SETTINGS]: {
path: ROUTES.WORKSPACE_TAXES_SETTINGS.route,
},
@@ -570,16 +586,37 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: {
path: ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.route,
+ parse: {
+ reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
+ },
},
[SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: {
path: ROUTES.WORKSPACE_REPORT_FIELD_ADD_VALUE.route,
+ parse: {
+ reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
+ },
},
[SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: {
path: ROUTES.WORKSPACE_REPORT_FIELD_VALUE_SETTINGS.route,
+ parse: {
+ reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
+ },
},
[SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: {
path: ROUTES.WORKSPACE_REPORT_FIELD_EDIT_VALUE.route,
},
+ [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: {
+ path: ROUTES.WORKSPACE_REPORT_FIELD_SETTINGS.route,
+ parse: {
+ reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
+ },
+ },
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: {
+ path: ROUTES.WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE.route,
+ parse: {
+ reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
+ },
+ },
[SCREENS.REIMBURSEMENT_ACCOUNT]: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
@@ -892,6 +929,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.RIGHT_MODAL.SEARCH_REPORT]: {
screens: {
[SCREENS.SEARCH.REPORT_RHP]: ROUTES.SEARCH_REPORT.route,
+ [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP.route,
},
},
[SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: {
diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index 2b057bf5edaa..547b766e1ce5 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -6,8 +6,10 @@ import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types';
import {isCentralPaneName} from '@libs/NavigationUtils';
import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils';
+import * as ReportConnection from '@libs/ReportConnection';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
+import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING';
import config from './config';
@@ -139,6 +141,13 @@ function getMatchingRootRouteForRHPRoute(route: NavigationPartialRoute): Navigat
return createFullScreenNavigator({name: fullScreenName as FullScreenName, params: route.params});
}
}
+
+ // check for valid reportID in the route params
+ // if the reportID is valid, we should navigate back to screen report in CPN
+ const reportID = (route.params as Record)?.reportID;
+ if (ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]) {
+ return {name: SCREENS.REPORT, params: {reportID}};
+ }
}
function getAdaptedState(state: PartialState>, policyID?: string): GetAdaptedStateReturnType {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index a60316fb7768..fc67fe6b8cc0 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -11,14 +11,15 @@ import type {
Route,
} from '@react-navigation/native';
import type {TupleToUnion, ValueOf} from 'type-fest';
+import type {SearchColumnType, SortOrder} from '@components/Search/types';
import type {IOURequestType} from '@libs/actions/IOU';
-import type {SearchColumnType, SortOrder} from '@libs/SearchUtils';
import type CONST from '@src/CONST';
import type {Country, IOUAction, IOUType} from '@src/CONST';
import type NAVIGATORS from '@src/NAVIGATORS';
import type {HybridAppRoute, Route as Routes} from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type EXIT_SURVEY_REASON_FORM_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm';
+import type {SageIntacctMappingName} from '@src/types/onyx/Policy';
type NavigationRef = NavigationContainerRefWithCurrent;
@@ -241,10 +242,6 @@ type SettingsNavigatorParamList = {
orderWeight: number;
tagName: string;
};
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: {
- policyID: string;
- reportFieldKey: string;
- };
[SCREENS.WORKSPACE.TAG_LIST_VIEW]: {
policyID: string;
orderWeight: number;
@@ -282,18 +279,29 @@ type SettingsNavigatorParamList = {
};
[SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: {
policyID: string;
+ reportFieldID?: string;
};
[SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: {
policyID: string;
+ reportFieldID?: string;
};
[SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: {
policyID: string;
valueIndex: number;
+ reportFieldID?: string;
};
[SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: {
policyID: string;
valueIndex: number;
};
+ [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: {
+ policyID: string;
+ reportFieldID: string;
+ };
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: {
+ policyID: string;
+ reportFieldID: string;
+ };
[SCREENS.WORKSPACE.MEMBER_DETAILS]: {
policyID: string;
accountID: string;
@@ -441,6 +449,27 @@ type SettingsNavigatorParamList = {
policyID: string;
importField: TupleToUnion;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD]: {
+ policyID: string;
+ importCustomField: TupleToUnion;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_VIEW]: {
+ policyID: string;
+ importCustomField: TupleToUnion;
+ internalID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_EDIT]: {
+ policyID: string;
+ importCustomField: TupleToUnion;
+ internalID: string;
+ fieldName: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_ADD]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT]: {
policyID: string;
};
@@ -510,6 +539,55 @@ type SettingsNavigatorParamList = {
policyID: string;
expenseType: ValueOf;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING]: {
+ policyID: string;
+ mapping: SageIntacctMappingName;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_MAPPING_TYPE]: {
+ policyID: string;
+ mapping: SageIntacctMappingName;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADD_USER_DIMENSION]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_USER_DIMENSIONS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EDIT_USER_DIMENSION]: {
+ policyID: string;
+ dimensionName: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREFERRED_EXPORTER]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT_DATE]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_EXPENSES]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_DEFAULT_VENDOR]: {
+ policyID: string;
+ reimbursable: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {
+ policyID: string;
+ };
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
@@ -542,6 +620,9 @@ type SettingsNavigatorParamList = {
policyID: string;
taxID: string;
};
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: {
+ policyID: string;
+ };
} & ReimbursementAccountNavigatorParamList;
type NewChatNavigatorParamList = {
@@ -993,7 +1074,6 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.DISTANCE_RATES]: {
policyID: string;
};
-
[SCREENS.WORKSPACE.ACCOUNTING.ROOT]: {
policyID: string;
};
@@ -1006,6 +1086,9 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
+ policyID: string;
+ };
};
type OnboardingModalNavigatorParamList = {
@@ -1125,6 +1208,8 @@ type FullScreenName = keyof FullScreenNavigatorParamList;
type CentralPaneName = keyof CentralPaneScreensParamList;
+type OnboardingFlowName = keyof OnboardingModalNavigatorParamList;
+
type SwitchPolicyIDParams = {
policyID?: string;
route?: Routes;
@@ -1155,6 +1240,7 @@ export type {
NewChatNavigatorParamList,
NewTaskNavigatorParamList,
OnboardingModalNavigatorParamList,
+ OnboardingFlowName,
ParticipantsNavigatorParamList,
PrivateNotesNavigatorParamList,
ProfileNavigatorParamList,
diff --git a/src/libs/NavigationUtils.ts b/src/libs/NavigationUtils.ts
index 34fc0b971ef6..aa26268977a2 100644
--- a/src/libs/NavigationUtils.ts
+++ b/src/libs/NavigationUtils.ts
@@ -1,7 +1,7 @@
import cloneDeep from 'lodash/cloneDeep';
import SCREENS from '@src/SCREENS';
import getTopmostBottomTabRoute from './Navigation/getTopmostBottomTabRoute';
-import type {CentralPaneName, RootStackParamList, State} from './Navigation/types';
+import type {CentralPaneName, OnboardingFlowName, RootStackParamList, State} from './Navigation/types';
const CENTRAL_PANE_SCREEN_NAMES = new Set([
SCREENS.SETTINGS.WORKSPACES,
@@ -17,6 +17,8 @@ const CENTRAL_PANE_SCREEN_NAMES = new Set([
SCREENS.REPORT,
]);
+const ONBOARDING_SCREEN_NAMES = new Set([SCREENS.ONBOARDING.PERSONAL_DETAILS, SCREENS.ONBOARDING.PURPOSE, SCREENS.ONBOARDING.WORK, SCREENS.ONBOARDING_MODAL.ONBOARDING]);
+
function isCentralPaneName(screen: string | undefined): screen is CentralPaneName {
if (!screen) {
return false;
@@ -25,6 +27,14 @@ function isCentralPaneName(screen: string | undefined): screen is CentralPaneNam
return CENTRAL_PANE_SCREEN_NAMES.has(screen as CentralPaneName);
}
+function isOnboardingFlowName(screen: string | undefined): screen is OnboardingFlowName {
+ if (!screen) {
+ return false;
+ }
+
+ return ONBOARDING_SCREEN_NAMES.has(screen as OnboardingFlowName);
+}
+
const removePolicyIDParamFromState = (state: State) => {
const stateCopy = cloneDeep(state);
const bottomTabRoute = getTopmostBottomTabRoute(stateCopy);
@@ -34,4 +44,4 @@ const removePolicyIDParamFromState = (state: State) => {
return stateCopy;
};
-export {isCentralPaneName, removePolicyIDParamFromState};
+export {isCentralPaneName, removePolicyIDParamFromState, isOnboardingFlowName};
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 2507c8e24e6b..61a39fe440ef 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -225,6 +225,10 @@ type FilterOptionsConfig = Pick<
'sortByReportTypeInSearch' | 'canInviteUser' | 'betas' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow'
> & {preferChatroomsOverThreads?: boolean};
+type HasText = {
+ text?: string;
+};
+
/**
* OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can
* be configured to display different results based on the options passed to the private getOptions() method. Public
@@ -2578,6 +2582,10 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
};
}
+function sortItemsAlphabetically(membersList: T[]): T[] {
+ return membersList.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase()));
+}
+
export {
getAvatarsForAccountIDs,
isCurrentUser,
@@ -2603,6 +2611,7 @@ export {
getEnabledCategoriesCount,
hasEnabledOptions,
sortCategories,
+ sortItemsAlphabetically,
sortTags,
getCategoryOptionTree,
hasEnabledTags,
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index fc9a04e2507c..140916349c53 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -7,8 +7,20 @@ import type {SelectorType} from '@components/SelectionScreen';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx';
-import type {ConnectionLastSync, Connections, CustomUnit, NetSuiteAccount, NetSuiteConnection, PolicyFeatureName, Rate, Tenant} from '@src/types/onyx/Policy';
+import type {
+ ConnectionLastSync,
+ Connections,
+ CustomUnit,
+ NetSuiteAccount,
+ NetSuiteConnection,
+ NetSuiteCustomList,
+ NetSuiteCustomSegment,
+ PolicyFeatureName,
+ Rate,
+ Tenant,
+} from '@src/types/onyx/Policy';
import type PolicyEmployee from '@src/types/onyx/PolicyEmployee';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import * as Localize from './Localize';
@@ -241,6 +253,7 @@ function getTagListName(policyTagList: OnyxEntry, orderWeight: nu
return Object.values(policyTagList).find((tag) => tag.orderWeight === orderWeight)?.name ?? '';
}
+
/**
* Gets all tag lists of a policy
*/
@@ -607,6 +620,20 @@ function getCustomersOrJobsLabelNetSuite(policy: Policy | undefined, translate:
return importedValueLabel.charAt(0).toUpperCase() + importedValueLabel.slice(1);
}
+function isNetSuiteCustomSegmentRecord(customField: NetSuiteCustomList | NetSuiteCustomSegment): boolean {
+ return 'segmentName' in customField;
+}
+
+function getNameFromNetSuiteCustomField(customField: NetSuiteCustomList | NetSuiteCustomSegment): string {
+ return 'segmentName' in customField ? customField.segmentName : customField.listName;
+}
+
+function isNetSuiteCustomFieldPropertyEditable(customField: NetSuiteCustomList | NetSuiteCustomSegment, fieldName: string) {
+ const fieldsAllowedToEdit = isNetSuiteCustomSegmentRecord(customField) ? [INPUT_IDS.SEGMENT_NAME, INPUT_IDS.INTERNAL_ID, INPUT_IDS.SCRIPT_ID, INPUT_IDS.MAPPING] : [INPUT_IDS.MAPPING];
+ const fieldKey = fieldName as keyof typeof customField;
+ return fieldsAllowedToEdit.includes(fieldKey);
+}
+
function getIntegrationLastSuccessfulDate(connection?: Connections[keyof Connections]) {
if (!connection) {
return undefined;
@@ -617,6 +644,46 @@ function getIntegrationLastSuccessfulDate(connection?: Connections[keyof Connect
return (connection as ConnectionWithLastSyncData)?.lastSync?.successfulDate;
}
+function getSageIntacctBankAccounts(policy?: Policy, selectedBankAccountId?: string): SelectorType[] {
+ const bankAccounts = policy?.connections?.intacct?.data?.bankAccounts ?? [];
+ return (bankAccounts ?? []).map(({id, name}) => ({
+ value: id,
+ text: name,
+ keyForList: id,
+ isSelected: selectedBankAccountId === id,
+ }));
+}
+
+function getSageIntacctVendors(policy?: Policy, selectedVendorId?: string): SelectorType[] {
+ const vendors = policy?.connections?.intacct?.data?.vendors ?? [];
+ return vendors.map(({id, value}) => ({
+ value: id,
+ text: value,
+ keyForList: id,
+ isSelected: selectedVendorId === id,
+ }));
+}
+
+function getSageIntacctNonReimbursableActiveDefaultVendor(policy?: Policy): string | undefined {
+ const {
+ nonReimbursableCreditCardChargeDefaultVendor: creditCardDefaultVendor,
+ nonReimbursableVendor: expenseReportDefaultVendor,
+ nonReimbursable,
+ } = policy?.connections?.intacct?.config.export ?? {};
+
+ return nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE ? creditCardDefaultVendor : expenseReportDefaultVendor;
+}
+
+function getSageIntacctCreditCards(policy?: Policy, selectedAccount?: string): SelectorType[] {
+ const creditCards = policy?.connections?.intacct?.data?.creditCards ?? [];
+ return creditCards.map(({name}) => ({
+ value: name,
+ text: name,
+ keyForList: name,
+ isSelected: name === selectedAccount,
+ }));
+}
+
/**
* Sort the workspaces by their name, while keeping the selected one at the beginning.
* @param workspace1 Details of the first workspace to be compared.
@@ -657,6 +724,13 @@ function getCurrentConnectionName(policy: Policy | undefined): string | undefine
return connectionKey ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionKey] : undefined;
}
+/**
+ * Check if the policy member is deleted from the workspace
+ */
+function isDeletedPolicyEmployee(policyEmployee: PolicyEmployee, isOffline: boolean) {
+ return !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors);
+}
+
export {
canEditTaxRate,
extractPolicyIDFromPath,
@@ -690,6 +764,7 @@ export {
hasPolicyErrorFields,
hasTaxRateError,
isExpensifyTeam,
+ isDeletedPolicyEmployee,
isFreeGroupPolicy,
isInstantSubmitEnabled,
isPaidGroupPolicy,
@@ -719,6 +794,10 @@ export {
getNetSuiteReceivableAccountOptions,
getNetSuiteInvoiceItemOptions,
getNetSuiteTaxAccountOptions,
+ getSageIntacctVendors,
+ getSageIntacctNonReimbursableActiveDefaultVendor,
+ getSageIntacctCreditCards,
+ getSageIntacctBankAccounts,
getCustomUnit,
getCustomUnitRate,
sortWorkspacesBySelected,
@@ -727,6 +806,9 @@ export {
getIntegrationLastSuccessfulDate,
getCurrentConnectionName,
getCustomersOrJobsLabelNetSuite,
+ isNetSuiteCustomSegmentRecord,
+ getNameFromNetSuiteCustomField,
+ isNetSuiteCustomFieldPropertyEditable,
};
export type {MemberEmailsToAccountIDs};
diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts
index 0765f5f0ba75..2dfa81d89a20 100644
--- a/src/libs/ReportActionComposeFocusManager.ts
+++ b/src/libs/ReportActionComposeFocusManager.ts
@@ -1,8 +1,10 @@
import React from 'react';
import type {MutableRefObject} from 'react';
import type {TextInput} from 'react-native';
-import ROUTES from '@src/ROUTES';
-import Navigation from './Navigation/Navigation';
+import SCREENS from '@src/SCREENS';
+import getTopmostRouteName from './Navigation/getTopmostRouteName';
+import isReportOpenInRHP from './Navigation/isReportOpenInRHP';
+import navigationRef from './Navigation/navigationRef';
type FocusCallback = (shouldFocusForNonBlurInputOnTapOutside?: boolean) => void;
@@ -31,8 +33,9 @@ function onComposerFocus(callback: FocusCallback | null, isMainComposer = false)
* Request focus on the ReportActionComposer
*/
function focus(shouldFocusForNonBlurInputOnTapOutside?: boolean) {
- /** Do not trigger the refocusing when the active route is not the report route, */
- if (!Navigation.isActiveRoute(ROUTES.REPORT_WITH_ID.getRoute(Navigation.getTopmostReportId() ?? '-1'))) {
+ /** Do not trigger the refocusing when the active route is not the report screen */
+ const navigationState = navigationRef.getState();
+ if (!navigationState || (!isReportOpenInRHP(navigationState) && getTopmostRouteName(navigationState) !== SCREENS.REPORT)) {
return;
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index bf1d86dad32f..edfb048f9da7 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -550,6 +550,26 @@ Onyx.connect({
},
});
+let allReportMetadata: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT_METADATA,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ if (!value) {
+ return;
+ }
+ allReportMetadata = value;
+ },
+});
+
+let isFirstTimeNewExpensifyUser = false;
+Onyx.connect({
+ key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
+ callback: (value) => {
+ isFirstTimeNewExpensifyUser = value ?? false;
+ },
+});
+
function getCurrentUserAvatar(): AvatarSource | undefined {
return currentUserPersonalDetails?.avatar;
}
@@ -1150,30 +1170,23 @@ function hasExpensifyGuidesEmails(accountIDs: number[]): boolean {
return accountIDs.some((accountID) => Str.extractEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN);
}
-function findLastAccessedReport(
- reports: OnyxCollection,
- ignoreDomainRooms: boolean,
- policies: OnyxCollection,
- isFirstTimeNewExpensifyUser: boolean,
- openOnAdminRoom = false,
- reportMetadata: OnyxCollection = {},
- policyID?: string,
- policyMemberAccountIDs: number[] = [],
- excludeReportID?: string,
-): OnyxEntry {
+function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = false, policyID?: string, excludeReportID?: string): OnyxEntry {
// If it's the user's first time using New Expensify, then they could either have:
// - just a Concierge report, if so we'll return that
// - their Concierge report, and a separate report that must have deeplinked them to the app before they created their account.
// If it's the latter, we'll use the deeplinked report over the Concierge report,
// since the Concierge report would be incorrectly selected over the deep-linked report in the logic below.
- let reportsValues = Object.values(reports ?? {});
+ const policyMemberAccountIDs = PolicyUtils.getPolicyEmployeeListByIdWithoutCurrentUser(allPolicies, policyID, currentUserAccountID);
+
+ const allReports = ReportConnection.getAllReports();
+ let reportsValues = Object.values(allReports ?? {});
if (!!policyID || policyMemberAccountIDs.length > 0) {
reportsValues = filterReportsByPolicyIDAndMemberAccountIDs(reportsValues, policyMemberAccountIDs, policyID);
}
- let sortedReports = sortReportsByLastRead(reportsValues, reportMetadata);
+ let sortedReports = sortReportsByLastRead(reportsValues, allReportMetadata);
let adminReport: OnyxEntry;
if (openOnAdminRoom) {
@@ -1197,7 +1210,7 @@ function findLastAccessedReport(
if (
ignoreDomainRooms &&
isDomainRoom(report) &&
- getPolicyType(report, policies) !== CONST.POLICY.TYPE.FREE &&
+ getPolicyType(report, allPolicies) !== CONST.POLICY.TYPE.FREE &&
!hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number))
) {
return false;
@@ -5143,7 +5156,7 @@ function buildTransactionThread(
participantAccountIDs,
getTransactionReportName(reportAction),
undefined,
- moneyRequestReport?.policyID,
+ moneyRequestReport?.policyID ?? '-1',
CONST.POLICY.OWNER_ACCOUNT_ID_FAKE,
false,
'',
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index cb579e44b95d..91d742f44e62 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -1,4 +1,4 @@
-import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils';
+import type {SearchColumnType, SortOrder} from '@components/Search/types';
import ReportListItem from '@components/SelectionList/Search/ReportListItem';
import TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
import type {ListItem, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
@@ -14,9 +14,6 @@ import type {AuthScreensParamList, RootStackParamList, State} from './Navigation
import * as TransactionUtils from './TransactionUtils';
import * as UserUtils from './UserUtils';
-type SortOrder = ValueOf;
-type SearchColumnType = ValueOf;
-
const columnNamesToSortingProperty = {
[CONST.SEARCH.TABLE_COLUMNS.TO]: 'formattedTo' as const,
[CONST.SEARCH.TABLE_COLUMNS.FROM]: 'formattedFrom' as const,
@@ -317,4 +314,3 @@ export {
isTransactionListItemType,
isSearchResultsEmpty,
};
-export type {SearchColumnType, SortOrder};
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 4f227e04482a..80081c8f89c7 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -126,21 +126,22 @@ function getOrderedReportIDs(
}
});
- // The LHN is split into four distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order:
+ // The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order:
// 1. Pinned/GBR - Always sorted by reportDisplayName
- // 2. Drafts - Always sorted by reportDisplayName
- // 3. Non-archived reports and settled IOUs
+ // 2. Error reports - Always sorted by reportDisplayName
+ // 3. Drafts - Always sorted by reportDisplayName
+ // 4. Non-archived reports and settled IOUs
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
// - Sorted by reportDisplayName in GSD (focus) view mode
- // 4. Archived reports
+ // 5. Archived reports
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
// - Sorted by reportDisplayName in GSD (focus) view mode
const pinnedAndGBRReports: MiniReport[] = [];
+ const errorReports: MiniReport[] = [];
const draftReports: MiniReport[] = [];
const nonArchivedReports: MiniReport[] = [];
const archivedReports: MiniReport[] = [];
- const errorReports: MiniReport[] = [];
if (currentPolicyID || policyMemberAccountIDs.length > 0) {
reportsToDisplay = reportsToDisplay.filter(
@@ -160,12 +161,12 @@ function getOrderedReportIDs(
const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1');
if (isPinned || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) {
pinnedAndGBRReports.push(miniReport);
+ } else if (report?.hasErrorsOtherThanFailedReceipt) {
+ errorReports.push(miniReport);
} else if (hasValidDraftComment(report?.reportID ?? '-1')) {
draftReports.push(miniReport);
} else if (ReportUtils.isArchivedRoom(report)) {
archivedReports.push(miniReport);
- } else if (report?.hasErrorsOtherThanFailedReceipt) {
- errorReports.push(miniReport);
} else {
nonArchivedReports.push(miniReport);
}
diff --git a/src/libs/WorkspaceReportFieldsUtils.ts b/src/libs/WorkspaceReportFieldUtils.ts
similarity index 83%
rename from src/libs/WorkspaceReportFieldsUtils.ts
rename to src/libs/WorkspaceReportFieldUtils.ts
index 0cc3cae24a23..b7d93b8dee3a 100644
--- a/src/libs/WorkspaceReportFieldsUtils.ts
+++ b/src/libs/WorkspaceReportFieldUtils.ts
@@ -2,8 +2,8 @@ import type {FormInputErrors} from '@components/Form/types';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type ONYXKEYS from '@src/ONYXKEYS';
-import type {InputID} from '@src/types/form/WorkspaceReportFieldsForm';
-import type {PolicyReportFieldType} from '@src/types/onyx/Policy';
+import type {InputID} from '@src/types/form/WorkspaceReportFieldForm';
+import type {PolicyReportField, PolicyReportFieldType} from '@src/types/onyx/Policy';
import * as ErrorUtils from './ErrorUtils';
import * as Localize from './Localize';
import * as ValidationUtils from './ValidationUtils';
@@ -67,4 +67,23 @@ function generateFieldID(name: string) {
return `field_id_${name.replace(CONST.REGEX.ANY_SPACE, '_').toUpperCase()}`;
}
-export {getReportFieldTypeTranslationKey, getReportFieldAlternativeTextTranslationKey, validateReportFieldListValueName, generateFieldID};
+/**
+ * Gets the initial value for a report field.
+ */
+function getReportFieldInitialValue(reportField: PolicyReportField | null): string {
+ if (!reportField) {
+ return '';
+ }
+
+ if (reportField.type === CONST.REPORT_FIELD_TYPES.LIST) {
+ return reportField.defaultValue ?? '';
+ }
+
+ if (reportField.type === CONST.REPORT_FIELD_TYPES.DATE) {
+ return Localize.translateLocal('common.currentDate');
+ }
+
+ return reportField.value ?? reportField.defaultValue;
+}
+
+export {getReportFieldTypeTranslationKey, getReportFieldAlternativeTextTranslationKey, validateReportFieldListValueName, generateFieldID, getReportFieldInitialValue};
diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts
index aea952618071..b23493c08e8e 100644
--- a/src/libs/actions/Card.ts
+++ b/src/libs/actions/Card.ts
@@ -5,10 +5,21 @@ import type {ActivatePhysicalExpensifyCardParams, ReportVirtualExpensifyCardFrau
import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ExpensifyCardDetails, IssueNewCardStep} from '@src/types/onyx/Card';
+import type {ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card';
type ReplacementReason = 'damaged' | 'stolen';
+type IssueNewCardFlowData = {
+ /** Step to be set in Onyx */
+ step?: IssueNewCardStep;
+
+ /** Whether the user is editing step */
+ isEditing?: boolean;
+
+ /** Data required to be sent to issue a new card */
+ data?: Partial;
+};
+
function reportVirtualExpensifyCardFraud(cardID: number) {
const optimisticData: OnyxUpdate[] = [
{
@@ -185,9 +196,24 @@ function revealVirtualCardDetails(cardID: number): Promise
});
}
-function setIssueNewCardStep(step: IssueNewCardStep | null) {
- Onyx.merge(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, {currentStep: step});
+function setIssueNewCardStepAndData({data, isEditing, step}: IssueNewCardFlowData) {
+ Onyx.merge(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, {data, isEditing, currentStep: step});
+}
+
+function clearIssueNewCardFlow() {
+ Onyx.set(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, {
+ currentStep: null,
+ data: {},
+ });
}
-export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud, revealVirtualCardDetails, setIssueNewCardStep};
+export {
+ requestReplacementExpensifyCard,
+ activatePhysicalExpensifyCard,
+ clearCardListErrors,
+ reportVirtualExpensifyCardFraud,
+ revealVirtualCardDetails,
+ setIssueNewCardStepAndData,
+ clearIssueNewCardFlow,
+};
export type {ReplacementReason};
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 33d906652af6..e5ebe2281a94 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -534,6 +534,10 @@ function clearNetSuiteAutoSyncErrorField(policyID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuite: {config: {errorFields: {autoSync: null}}}}});
}
+function clearSageIntacctErrorField(policyID: string, fieldName: string) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {intacct: {config: {errorFields: {[fieldName]: null}}}}});
+}
+
function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserEmail: string) {
const policy = getPolicy(policyID);
@@ -3140,6 +3144,7 @@ export {
openPolicyExpensifyCardsPage,
requestExpensifyCardLimitIncrease,
getPoliciesConnectedToSageIntacct,
+ clearSageIntacctErrorField,
};
export type {NewCustomUnit};
diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts
index 4a6c5ed4fad5..bccb08c47c18 100644
--- a/src/libs/actions/Policy/ReportField.ts
+++ b/src/libs/actions/Policy/ReportField.ts
@@ -1,15 +1,23 @@
+import cloneDeep from 'lodash/cloneDeep';
import type {NullishDeep, OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
-import type {CreateWorkspaceReportFieldParams, PolicyReportFieldsReplace} from '@libs/API/parameters';
+import type {
+ CreateWorkspaceReportFieldListValueParams,
+ CreateWorkspaceReportFieldParams,
+ EnableWorkspaceReportFieldListValueParams,
+ PolicyReportFieldsReplace,
+ RemoveWorkspaceReportFieldListValueParams,
+ UpdateWorkspaceReportFieldInitialValueParams,
+} from '@libs/API/parameters';
import {WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as ReportUtils from '@libs/ReportUtils';
-import {generateFieldID} from '@libs/WorkspaceReportFieldsUtils';
+import * as WorkspaceReportFieldUtils from '@libs/WorkspaceReportFieldUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {WorkspaceReportFieldsForm} from '@src/types/form/WorkspaceReportFieldsForm';
-import INPUT_IDS from '@src/types/form/WorkspaceReportFieldsForm';
+import type {WorkspaceReportFieldForm} from '@src/types/form/WorkspaceReportFieldForm';
+import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm';
import type {Policy, PolicyReportField, Report} from '@src/types/onyx';
import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon';
import type {OnyxData} from '@src/types/onyx/Request';
@@ -127,16 +135,16 @@ function deleteReportFieldsListValue(valueIndexes: number[]) {
});
}
-type CreateReportFieldArguments = Pick;
+type CreateReportFieldArguments = Pick;
/**
* Creates a new report field.
*/
function createReportField(policyID: string, {name, type, initialValue}: CreateReportFieldArguments) {
const previousFieldList = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.fieldList ?? {};
- const fieldID = generateFieldID(name);
+ const fieldID = WorkspaceReportFieldUtils.generateFieldID(name);
const fieldKey = ReportUtils.getReportFieldKey(fieldID);
- const newReportField: OnyxValueWithOfflineFeedback = {
+ const newReportField: PolicyReportField = {
name,
type,
defaultValue: initialValue,
@@ -149,7 +157,6 @@ function createReportField(policyID: string, {name, type, initialValue}: CreateR
keys: [],
externalIDs: [],
isTax: false,
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
};
const onyxData: OnyxData = {
optimisticData: [
@@ -158,7 +165,7 @@ function createReportField(policyID: string, {name, type, initialValue}: CreateR
onyxMethod: Onyx.METHOD.MERGE,
value: {
fieldList: {
- [fieldKey]: newReportField,
+ [fieldKey]: {...newReportField, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
},
errorFields: null,
},
@@ -258,17 +265,281 @@ function deleteReportFields(policyID: string, reportFieldsToUpdate: string[]) {
reportFields: JSON.stringify(Object.values(updatedReportFields)),
};
- API.write(WRITE_COMMANDS.POLICY_REPORT_FIELDS_REPLACE, parameters, onyxData);
+ API.write(WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD, parameters, onyxData);
+}
+
+/**
+ * Updates the initial value of a report field.
+ */
+function updateReportFieldInitialValue(policyID: string, reportFieldID: string, newInitialValue: string) {
+ const previousFieldList = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.fieldList ?? {};
+ const fieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const updatedReportField: PolicyReportField = {
+ ...previousFieldList[fieldKey],
+ defaultValue: newInitialValue,
+ };
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [fieldKey]: {...updatedReportField, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [fieldKey]: {pendingAction: null},
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ failureData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [fieldKey]: {...previousFieldList[fieldKey], pendingAction: null},
+ },
+ errorFields: {
+ [fieldKey]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.reportFields.genericFailureMessage'),
+ },
+ },
+ },
+ ],
+ };
+ const parameters: UpdateWorkspaceReportFieldInitialValueParams = {
+ policyID,
+ reportFields: JSON.stringify([updatedReportField]),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE, parameters, onyxData);
+}
+
+function updateReportFieldListValueEnabled(policyID: string, reportFieldID: string, valueIndexes: number[], enabled: boolean) {
+ const previousFieldList = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.fieldList ?? {};
+ const fieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField = previousFieldList[fieldKey];
+
+ const updatedReportField = cloneDeep(reportField);
+
+ valueIndexes.forEach((valueIndex) => {
+ updatedReportField.disabledOptions[valueIndex] = !enabled;
+ const shouldResetDefaultValue = !enabled && reportField.defaultValue === reportField.values[valueIndex];
+
+ if (shouldResetDefaultValue) {
+ updatedReportField.defaultValue = '';
+ }
+ });
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [fieldKey]: {...updatedReportField, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [fieldKey]: {pendingAction: null},
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ failureData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [fieldKey]: {...reportField, pendingAction: null},
+ },
+ errorFields: {
+ [fieldKey]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.reportFields.genericFailureMessage'),
+ },
+ },
+ },
+ ],
+ };
+ const parameters: EnableWorkspaceReportFieldListValueParams = {
+ policyID,
+ reportFields: JSON.stringify([updatedReportField]),
+ };
+
+ API.write(WRITE_COMMANDS.ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE, parameters, onyxData);
+}
+
+/**
+ * Adds a new option to the list type report field on a workspace.
+ */
+function addReportFieldListValue(policyID: string, reportFieldID: string, valueName: string) {
+ const previousFieldList = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.fieldList ?? {};
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField = previousFieldList[reportFieldKey];
+ const updatedReportField = cloneDeep(reportField);
+
+ updatedReportField.values.push(valueName);
+ updatedReportField.disabledOptions.push(false);
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [reportFieldKey]: {
+ ...updatedReportField,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [reportFieldKey]: {pendingAction: null},
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ failureData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [reportFieldKey]: {...reportField, pendingAction: null},
+ },
+ errorFields: {
+ [reportFieldKey]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.reportFields.genericFailureMessage'),
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters: CreateWorkspaceReportFieldListValueParams = {
+ policyID,
+ reportFields: JSON.stringify([updatedReportField]),
+ };
+
+ API.write(WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE, parameters, onyxData);
+}
+
+/**
+ * Removes a list value from the workspace report fields.
+ */
+function removeReportFieldListValue(policyID: string, reportFieldID: string, valueIndexes: number[]) {
+ const previousFieldList = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.fieldList ?? {};
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField = previousFieldList[reportFieldKey];
+ const updatedReportField = cloneDeep(reportField);
+
+ valueIndexes
+ .sort((a, b) => b - a)
+ .forEach((valueIndex) => {
+ const shouldResetDefaultValue = reportField.defaultValue === reportField.values[valueIndex];
+
+ if (shouldResetDefaultValue) {
+ updatedReportField.defaultValue = '';
+ }
+
+ updatedReportField.values.splice(valueIndex, 1);
+ updatedReportField.disabledOptions.splice(valueIndex, 1);
+ });
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [reportFieldKey]: {
+ ...updatedReportField,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [reportFieldKey]: {pendingAction: null},
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ failureData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [reportFieldKey]: {...reportField, pendingAction: null},
+ },
+ errorFields: {
+ [reportFieldKey]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.reportFields.genericFailureMessage'),
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters: RemoveWorkspaceReportFieldListValueParams = {
+ policyID,
+ reportFields: JSON.stringify([updatedReportField]),
+ };
+
+ API.write(WRITE_COMMANDS.REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE, parameters, onyxData);
}
export type {CreateReportFieldArguments};
export {
- deleteReportFields,
setInitialCreateReportFieldsForm,
createReportFieldsListValue,
renameReportFieldsListValue,
setReportFieldsListValueEnabled,
deleteReportFieldsListValue,
createReportField,
+ deleteReportFields,
+ updateReportFieldInitialValue,
+ updateReportFieldListValueEnabled,
+ addReportFieldListValue,
+ removeReportFieldListValue,
};
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 9870b561ad6f..3060f53f12c3 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -1,3 +1,4 @@
+import {findFocusedRoute} from '@react-navigation/native';
import {format as timezoneFormat, utcToZonedTime} from 'date-fns-tz';
import {Str} from 'expensify-common';
import isEmpty from 'lodash/isEmpty';
@@ -55,11 +56,13 @@ import {prepareDraftComment} from '@libs/DraftCommentUtils';
import * as EmojiUtils from '@libs/EmojiUtils';
import * as Environment from '@libs/Environment/Environment';
import * as ErrorUtils from '@libs/ErrorUtils';
+import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import isPublicScreenRoute from '@libs/isPublicScreenRoute';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import {registerPaginationConfig} from '@libs/Middleware/Pagination';
-import Navigation from '@libs/Navigation/Navigation';
+import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
+import {isOnboardingFlowName} from '@libs/NavigationUtils';
import type {NetworkStatus} from '@libs/NetworkConnection';
import LocalNotification from '@libs/Notification/LocalNotification';
import Parser from '@libs/Parser';
@@ -92,7 +95,6 @@ import type {
RecentlyUsedReportFields,
ReportAction,
ReportActionReactions,
- ReportMetadata,
ReportUserIsTyping,
} from '@src/types/onyx';
import type {Decision} from '@src/types/onyx/OriginalMessage';
@@ -222,13 +224,6 @@ Onyx.connect({
},
});
-let reportMetadata: OnyxCollection = {};
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT_METADATA,
- waitForCollectionCallback: true,
- callback: (value) => (reportMetadata = value),
-});
-
const typingWatchTimers: Record = {};
let reportIDDeeplinkedFromOldDot: string | undefined;
@@ -2549,28 +2544,47 @@ function openReportFromDeepLink(url: string) {
// Navigate to the report after sign-in/sign-up.
InteractionManager.runAfterInteractions(() => {
Session.waitForUserSignIn().then(() => {
- Navigation.waitForProtectedRoutes().then(() => {
- if (route && Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(route)) {
- Session.signOutAndRedirectToSignIn(true);
- return;
- }
-
- // We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
- // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
- // which is already called when AuthScreens mounts.
- if (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) {
- return;
- }
-
- if (shouldSkipDeepLinkNavigation(route)) {
- return;
- }
-
- if (isAuthenticated) {
- return;
- }
-
- Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
+ Onyx.connect({
+ key: ONYXKEYS.NVP_ONBOARDING,
+ callback: (onboarding) => {
+ Navigation.waitForProtectedRoutes().then(() => {
+ if (route && Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(route)) {
+ Session.signOutAndRedirectToSignIn(true);
+ return;
+ }
+
+ // We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
+ // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
+ // which is already called when AuthScreens mounts.
+ if (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) {
+ return;
+ }
+
+ if (shouldSkipDeepLinkNavigation(route)) {
+ return;
+ }
+
+ const state = navigationRef.getRootState();
+ const currentFocusedRoute = findFocusedRoute(state);
+ const hasCompletedGuidedSetupFlow = hasCompletedGuidedSetupFlowSelector(onboarding);
+
+ // We need skip deeplinking if the user hasn't completed the guided setup flow.
+ if (!hasCompletedGuidedSetupFlow) {
+ return;
+ }
+
+ if (isOnboardingFlowName(currentFocusedRoute?.name)) {
+ Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
+ return;
+ }
+
+ if (isAuthenticated) {
+ return;
+ }
+
+ Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
+ });
+ },
});
});
});
@@ -2581,17 +2595,7 @@ function getCurrentUserAccountID(): number {
}
function navigateToMostRecentReport(currentReport: OnyxEntry) {
- const lastAccessedReportID = ReportUtils.findLastAccessedReport(
- ReportConnection.getAllReports(),
- false,
- undefined,
- false,
- false,
- reportMetadata,
- undefined,
- [],
- currentReport?.reportID,
- )?.reportID;
+ const lastAccessedReportID = ReportUtils.findLastAccessedReport(false, false, undefined, currentReport?.reportID)?.reportID;
if (lastAccessedReportID) {
const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '-1');
diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts
index 70f7d2d5b7e0..4ce82a027a12 100644
--- a/src/libs/actions/Search.ts
+++ b/src/libs/actions/Search.ts
@@ -63,17 +63,18 @@ function createTransactionThread(hash: number, transactionID: string, reportID:
},
},
};
-
Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, onyxUpdate);
}
function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], comment: string) {
const {optimisticData, finallyData} = getOnyxLoadingData(hash);
+
API.write(WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList, comment}, {optimisticData, finallyData});
}
function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) {
const {optimisticData, finallyData} = getOnyxLoadingData(hash);
+
API.write(WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData});
}
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 0c362f870da4..903ac6b97ec7 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -198,12 +198,12 @@ function hasAuthToken(): boolean {
return !!session.authToken;
}
-function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean) {
+function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, killHybridApp = true) {
Log.info('Redirecting to Sign In because signOut() was called');
hideContextMenu(false);
if (!isAnonymousUser()) {
// In the HybridApp, we want the Old Dot to handle the sign out process
- if (NativeModules.HybridAppModule) {
+ if (NativeModules.HybridAppModule && killHybridApp) {
NativeModules.HybridAppModule.closeReactNativeApp();
return;
}
diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts
index a90c386d02b6..b592424cfcdf 100644
--- a/src/libs/actions/Welcome.ts
+++ b/src/libs/actions/Welcome.ts
@@ -11,7 +11,8 @@ import ROUTES from '@src/ROUTES';
import type Onboarding from '@src/types/onyx/Onboarding';
import type TryNewDot from '@src/types/onyx/TryNewDot';
-let onboarding: Onboarding | [] | undefined;
+type OnboardingData = Onboarding | [] | undefined;
+
let isLoadingReportData = true;
let tryNewDotData: TryNewDot | undefined;
@@ -30,8 +31,8 @@ let isServerDataReadyPromise = new Promise((resolve) => {
resolveIsReadyPromise = resolve;
});
-let resolveOnboardingFlowStatus: (value?: Promise) => void | undefined;
-let isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
+let resolveOnboardingFlowStatus: (value?: OnboardingData) => void;
+let isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
resolveOnboardingFlowStatus = resolve;
});
@@ -45,7 +46,7 @@ function onServerDataReady(): Promise {
}
function isOnboardingFlowCompleted({onCompleted, onNotCompleted}: HasCompletedOnboardingFlowProps) {
- isOnboardingFlowStatusKnownPromise.then(() => {
+ isOnboardingFlowStatusKnownPromise.then((onboarding) => {
if (Array.isArray(onboarding) || onboarding?.hasCompletedGuidedSetupFlow === undefined) {
return;
}
@@ -102,23 +103,7 @@ function handleHybridAppOnboarding() {
}
/**
- * Check that a few requests have completed so that the welcome action can proceed:
- *
- * - Whether we are a first time new expensify user
- * - Whether we have loaded all policies the server knows about
- * - Whether we have loaded all reports the server knows about
- * Check if onboarding data is ready in order to check if the user has completed onboarding or not
- */
-function checkOnboardingDataReady() {
- if (onboarding === undefined) {
- return;
- }
-
- resolveOnboardingFlowStatus?.();
-}
-
-/**
- * Check if user dismissed modal and if report data are loaded
+ * Check if report data are loaded
*/
function checkServerDataReady() {
if (isLoadingReportData) {
@@ -143,6 +128,10 @@ function setOnboardingPurposeSelected(value: OnboardingPurposeType) {
Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, value ?? null);
}
+function setOnboardingErrorMessage(value: string) {
+ Onyx.set(ONYXKEYS.ONBOARDING_ERROR_MESSAGE, value ?? null);
+}
+
function setOnboardingAdminsChatReportID(adminsChatReportID?: string) {
Onyx.set(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, adminsChatReportID ?? null);
}
@@ -186,9 +175,7 @@ Onyx.connect({
return;
}
- onboarding = value;
-
- checkOnboardingDataReady();
+ resolveOnboardingFlowStatus(value);
},
});
@@ -213,10 +200,9 @@ function resetAllChecks() {
isServerDataReadyPromise = new Promise((resolve) => {
resolveIsReadyPromise = resolve;
});
- isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
+ isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
resolveOnboardingFlowStatus = resolve;
});
- onboarding = undefined;
isLoadingReportData = true;
}
@@ -229,4 +215,5 @@ export {
setOnboardingPolicyID,
completeHybridAppOnboarding,
handleHybridAppOnboarding,
+ setOnboardingErrorMessage,
};
diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts
index 66bf482d6ef2..adab82a5da8f 100644
--- a/src/libs/actions/connections/NetSuiteCommands.ts
+++ b/src/libs/actions/connections/NetSuiteCommands.ts
@@ -7,7 +7,7 @@ import {WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Connections, NetSuiteCustomFormID} from '@src/types/onyx/Policy';
+import type {Connections, NetSuiteCustomFormID, NetSuiteCustomList, NetSuiteCustomSegment} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
type SubsidiaryParam = {
@@ -427,6 +427,31 @@ function updateNetSuiteCrossSubsidiaryCustomersConfiguration(policyID: string, i
API.write(WRITE_COMMANDS.UPDATE_NETSUITE_CROSS_SUBSIDIARY_CUSTOMER_CONFIGURATION, params, onyxData);
}
+function updateNetSuiteCustomSegments(policyID: string, records: NetSuiteCustomSegment[], oldRecords: NetSuiteCustomSegment[]) {
+ const onyxData = updateNetSuiteSyncOptionsOnyxData(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS, records, oldRecords);
+
+ API.write(
+ WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_SEGMENTS,
+ {
+ policyID,
+ customSegments: JSON.stringify(records),
+ },
+ onyxData,
+ );
+}
+
+function updateNetSuiteCustomLists(policyID: string, records: NetSuiteCustomList[], oldRecords: NetSuiteCustomList[]) {
+ const onyxData = updateNetSuiteSyncOptionsOnyxData(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS, records, oldRecords);
+ API.write(
+ WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_LISTS,
+ {
+ policyID,
+ customLists: JSON.stringify(records),
+ },
+ onyxData,
+ );
+}
+
function updateNetSuiteExporter(policyID: string, exporter: string, oldExporter: string) {
const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.EXPORTER, exporter, oldExporter);
@@ -849,6 +874,8 @@ export {
updateNetSuiteExportToNextOpenPeriod,
updateNetSuiteImportMapping,
updateNetSuiteCrossSubsidiaryCustomersConfiguration,
+ updateNetSuiteCustomSegments,
+ updateNetSuiteCustomLists,
updateNetSuiteAutoSync,
updateNetSuiteSyncReimbursedReports,
updateNetSuiteSyncPeople,
diff --git a/src/libs/actions/connections/SageIntacct.ts b/src/libs/actions/connections/SageIntacct.ts
index 9f944bd17273..75a8b1e2d555 100644
--- a/src/libs/actions/connections/SageIntacct.ts
+++ b/src/libs/actions/connections/SageIntacct.ts
@@ -1,6 +1,21 @@
+import type {OnyxUpdate} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
import * as API from '@libs/API';
-import type ConnectPolicyToSageIntacctParams from '@libs/API/parameters/ConnectPolicyToSageIntacctParams';
+import type {ConnectPolicyToSageIntacctParams} from '@libs/API/parameters';
import {WRITE_COMMANDS} from '@libs/API/types';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {
+ Connections,
+ SageIntacctConnectionsConfig,
+ SageIntacctDimension,
+ SageIntacctMappingName,
+ SageIntacctMappingType,
+ SageIntacctMappingValue,
+ SageIntacctOfflineStateKeys,
+} from '@src/types/onyx/Policy';
type SageIntacctCredentials = {companyID: string; userID: string; password: string};
@@ -14,4 +29,732 @@ function connectToSageIntacct(policyID: string, credentials: SageIntacctCredenti
API.write(WRITE_COMMANDS.CONNECT_POLICY_TO_SAGE_INTACCT, parameters, {});
}
-export default connectToSageIntacct;
+function prepareOnyxDataForMappingUpdate(policyID: string, mappingName: keyof SageIntacctMappingType, mappingValue: boolean | SageIntacctMappingValue) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ mappings: {
+ [mappingName]: mappingValue,
+ },
+ pendingFields: {
+ [mappingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ errorFields: {
+ [mappingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ [mappingName]: null,
+ },
+ errorFields: {
+ [mappingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ [mappingName]: null,
+ },
+ errorFields: {
+ [mappingName]: undefined,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ return {optimisticData, failureData, successData};
+}
+
+function updateSageIntacctBillable(policyID: string, enabled: boolean) {
+ const parameters = {
+ policyID,
+ enabled,
+ };
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_BILLABLE, parameters, prepareOnyxDataForMappingUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_ITEMS, enabled));
+}
+
+function getCommandForMapping(mappingName: ValueOf) {
+ switch (mappingName) {
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS:
+ return WRITE_COMMANDS.UPDATE_SAGE_INTACCT_DEPARTMENT_MAPPING;
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CLASSES:
+ return WRITE_COMMANDS.UPDATE_SAGE_INTACCT_CLASSES_MAPPING;
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.LOCATIONS:
+ return WRITE_COMMANDS.UPDATE_SAGE_INTACCT_LOCATIONS_MAPPING;
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.CUSTOMERS:
+ return WRITE_COMMANDS.UPDATE_SAGE_INTACCT_CUSTOMERS_MAPPING;
+ case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.PROJECTS:
+ return WRITE_COMMANDS.UPDATE_SAGE_INTACCT_PROJECTS_MAPPING;
+ default:
+ return undefined;
+ }
+}
+
+function updateSageIntacctMappingValue(policyID: string, mappingName: SageIntacctMappingName, mappingValue: SageIntacctMappingValue) {
+ const command = getCommandForMapping(mappingName);
+ if (!command) {
+ return;
+ }
+
+ const onyxData = prepareOnyxDataForMappingUpdate(policyID, mappingName, mappingValue);
+ API.write(
+ command,
+ {
+ policyID,
+ mapping: mappingValue,
+ },
+ onyxData,
+ );
+}
+
+function updateSageIntacctSyncTaxConfiguration(policyID: string, enabled: boolean) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ tax: {
+ syncTax: enabled,
+ },
+ pendingFields: {
+ tax: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ errorFields: {
+ tax: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ tax: null,
+ },
+ errorFields: {
+ tax: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ tax: null,
+ },
+ errorFields: {
+ tax: undefined,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_TAX_CONFIGURATION, {policyID, enabled}, {optimisticData, failureData, successData});
+}
+
+function prepareOnyxDataForUserDimensionUpdate(policyID: string, dimensionName: string, newDimensions: SageIntacctDimension[]) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ mappings: {
+ dimensions: newDimensions,
+ },
+ pendingFields: {[`dimension_${dimensionName}`]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ errorFields: {[`dimension_${dimensionName}`]: null},
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ mappings: {
+ dimensions: newDimensions,
+ },
+ pendingFields: {[`dimension_${dimensionName}`]: null},
+ errorFields: {[`dimension_${dimensionName}`]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')},
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ mappings: {
+ dimensions: newDimensions,
+ },
+ pendingFields: {[`dimension_${dimensionName}`]: null},
+ errorFields: {[`dimension_${dimensionName}`]: null},
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ return {optimisticData, failureData, successData};
+}
+
+function addSageIntacctUserDimensions(
+ policyID: string,
+ dimensionName: string,
+ mapping: typeof CONST.SAGE_INTACCT_MAPPING_VALUE.TAG | typeof CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD,
+ existingUserDimensions: SageIntacctDimension[],
+) {
+ const newDimensions = [...existingUserDimensions, {mapping, dimension: dimensionName}];
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_USER_DIMENSION, {policyID, dimensions: newDimensions}, prepareOnyxDataForUserDimensionUpdate(policyID, dimensionName, newDimensions));
+}
+
+function editSageIntacctUserDimensions(
+ policyID: string,
+ previousName: string,
+ name: string,
+ mapping: typeof CONST.SAGE_INTACCT_MAPPING_VALUE.TAG | typeof CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD,
+ existingUserDimensions: SageIntacctDimension[],
+) {
+ const newDimensions = existingUserDimensions.map((userDimension) => {
+ if (userDimension.dimension === previousName) {
+ return {dimension: name, mapping};
+ }
+ return userDimension;
+ });
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_USER_DIMENSION, {policyID, dimensions: newDimensions}, prepareOnyxDataForUserDimensionUpdate(policyID, name, newDimensions));
+}
+
+function removeSageIntacctUserDimensions(policyID: string, dimensionName: string, existingUserDimensions: SageIntacctDimension[]) {
+ const newDimensions = existingUserDimensions.filter((userDimension) => dimensionName !== userDimension.dimension);
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_USER_DIMENSION, {policyID, dimensions: newDimensions}, prepareOnyxDataForUserDimensionUpdate(policyID, dimensionName, newDimensions));
+}
+
+function prepareOnyxDataForExportUpdate(policyID: string, settingName: keyof Connections['intacct']['config']['export'], settingValue: string | null) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ export: {
+ [settingName]: settingValue,
+ pendingFields: {
+ [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ errorFields: {
+ [settingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ export: {
+ [settingName]: settingValue,
+ pendingFields: {
+ [settingName]: null,
+ },
+ errorFields: {
+ [settingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ export: {
+ [settingName]: settingValue,
+ pendingFields: {
+ [settingName]: null,
+ },
+ errorFields: {
+ [settingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ return {optimisticData, failureData, successData};
+}
+
+function updateSageIntacctExporter(policyID: string, exporter: string) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForExportUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.EXPORTER, exporter);
+ const parameters = {
+ policyID,
+ email: exporter,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_EXPORTER, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctExportDate(policyID: string, date: ValueOf) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForExportUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.EXPORT_DATE, date);
+ const parameters = {
+ policyID,
+ value: date,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_EXPORT_DATE, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctReimbursableExpensesExportDestination(policyID: string, reimbursable: ValueOf) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForExportUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE, reimbursable);
+ const parameters = {
+ policyID,
+ value: reimbursable,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctNonreimbursableExpensesExportDestination(policyID: string, nonReimbursable: ValueOf) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForExportUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE, nonReimbursable);
+ const parameters = {
+ policyID,
+ value: nonReimbursable,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctReimbursableExpensesReportExportDefaultVendor(policyID: string, vendor: string) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForExportUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR, vendor);
+ const parameters = {
+ policyID,
+ vendorID: vendor,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_REIMBURSABLE_EXPENSES_REPORT_EXPORT_DEFAULT_VENDOR, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctNonreimbursableExpensesCreditCardChargeExportDefaultVendor(policyID: string, vendor: string) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForExportUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, vendor);
+ const parameters = {
+ policyID,
+ vendorID: vendor,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_CREDIT_CARD_CHARGE_EXPORT_DEFAULT_VENDOR, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctNonreimbursableExpensesExportAccount(policyID: string, nonReimbursableAccount: string) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForExportUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_ACCOUNT, nonReimbursableAccount);
+ const parameters = {
+ policyID,
+ creditCardAccountID: nonReimbursableAccount,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_ACCOUNT, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctNonreimbursableExpensesExportVendor(policyID: string, vendor: string) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForExportUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR, vendor);
+ const parameters = {
+ policyID,
+ vendorID: vendor,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_VENDOR, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctDefaultVendor(policyID: string, settingName: keyof Connections['intacct']['config']['export'], vendor: string) {
+ if (settingName === CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR) {
+ updateSageIntacctReimbursableExpensesReportExportDefaultVendor(policyID, vendor);
+ } else if (settingName === CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR) {
+ updateSageIntacctNonreimbursableExpensesCreditCardChargeExportDefaultVendor(policyID, vendor);
+ } else if (settingName === CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR) {
+ updateSageIntacctNonreimbursableExpensesExportVendor(policyID, vendor);
+ }
+}
+
+function clearSageIntacctErrorField(policyID: string, key: SageIntacctOfflineStateKeys | keyof SageIntacctConnectionsConfig) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {intacct: {config: {errorFields: {[key]: null}}}}});
+}
+
+function prepareOnyxDataForConfigUpdate(policyID: string, settingName: keyof SageIntacctConnectionsConfig, settingValue: string | boolean | null) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ [settingName]: settingValue,
+ pendingFields: {
+ [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ errorFields: {
+ [settingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ [settingName]: null,
+ },
+ errorFields: {
+ [settingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ [settingName]: null,
+ },
+ errorFields: {
+ [settingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ return {optimisticData, failureData, successData};
+}
+
+function prepareOnyxDataForSyncUpdate(policyID: string, settingName: keyof Connections['intacct']['config']['sync'], settingValue: string | boolean) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ sync: {
+ [settingName]: settingValue,
+ },
+ pendingFields: {
+ [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ errorFields: {
+ [settingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ [settingName]: null,
+ },
+ errorFields: {
+ [settingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ [settingName]: null,
+ },
+ errorFields: {
+ [settingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ return {optimisticData, failureData, successData};
+}
+
+function prepareOnyxDataForAutoSyncUpdate(policyID: string, settingName: keyof Connections['intacct']['config']['autoSync'], settingValue: boolean) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ autoSync: {
+ [settingName]: settingValue,
+ },
+ pendingFields: {
+ [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ errorFields: {
+ [settingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ [settingName]: null,
+ },
+ errorFields: {
+ [settingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ intacct: {
+ config: {
+ pendingFields: {
+ [settingName]: null,
+ },
+ errorFields: {
+ [settingName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ return {optimisticData, failureData, successData};
+}
+
+function updateSageIntacctAutoSync(policyID: string, enabled: boolean) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForAutoSyncUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED, enabled);
+ const parameters = {
+ policyID,
+ enabled,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_AUTO_SYNC, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctImportEmployees(policyID: string, enabled: boolean) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForConfigUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES, enabled);
+ const parameters = {
+ policyID,
+ enabled,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_IMPORT_EMPLOYEES, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctApprovalMode(policyID: string, enabled: boolean) {
+ const approvalModeSettingValue = enabled ? CONST.SAGE_INTACCT.APPROVAL_MODE.APPROVAL_MANUAL : '';
+ const {optimisticData, failureData, successData} = prepareOnyxDataForConfigUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE, approvalModeSettingValue);
+ const parameters = {
+ policyID,
+ value: approvalModeSettingValue,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_APPROVAL_MODE, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctSyncReimbursedReports(policyID: string, enabled: boolean) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForSyncUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS, enabled);
+ const parameters = {
+ policyID,
+ enabled,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_REIMBURSED_REPORTS, parameters, {optimisticData, failureData, successData});
+}
+
+function updateSageIntacctSyncReimbursementAccountID(policyID: string, vendorID: string) {
+ const {optimisticData, failureData, successData} = prepareOnyxDataForSyncUpdate(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID, vendorID);
+ const parameters = {
+ policyID,
+ vendorID,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_REIMBURSEMENT_ACCOUNT_ID, parameters, {optimisticData, failureData, successData});
+}
+
+export {
+ connectToSageIntacct,
+ updateSageIntacctBillable,
+ updateSageIntacctSyncTaxConfiguration,
+ addSageIntacctUserDimensions,
+ updateSageIntacctMappingValue,
+ editSageIntacctUserDimensions,
+ removeSageIntacctUserDimensions,
+ updateSageIntacctExporter,
+ clearSageIntacctErrorField,
+ updateSageIntacctExportDate,
+ updateSageIntacctReimbursableExpensesExportDestination,
+ updateSageIntacctNonreimbursableExpensesExportDestination,
+ updateSageIntacctNonreimbursableExpensesExportAccount,
+ updateSageIntacctDefaultVendor,
+ updateSageIntacctAutoSync,
+ updateSageIntacctImportEmployees,
+ updateSageIntacctApprovalMode,
+ updateSageIntacctSyncReimbursedReports,
+ updateSageIntacctSyncReimbursementAccountID,
+};
diff --git a/src/libs/hasCompletedGuidedSetupFlowSelector.ts b/src/libs/hasCompletedGuidedSetupFlowSelector.ts
new file mode 100644
index 000000000000..83cde0a0be8c
--- /dev/null
+++ b/src/libs/hasCompletedGuidedSetupFlowSelector.ts
@@ -0,0 +1,12 @@
+import type {OnyxValue} from 'react-native-onyx';
+import type ONYXKEYS from '@src/ONYXKEYS';
+
+function hasCompletedGuidedSetupFlowSelector(onboarding: OnyxValue): boolean {
+ // onboarding is an array for old accounts and accounts created from olddot
+ if (Array.isArray(onboarding)) {
+ return true;
+ }
+ return onboarding?.hasCompletedGuidedSetupFlow ?? false;
+}
+
+export default hasCompletedGuidedSetupFlowSelector;
diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx
index 69f959d6545f..d489e58493d3 100644
--- a/src/pages/EditReportFieldPage.tsx
+++ b/src/pages/EditReportFieldPage.tsx
@@ -77,11 +77,11 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps)
const value = form[fieldKey];
if (isReportFieldTitle) {
ReportActions.updateReportName(report.reportID, value, report.reportName ?? '');
+ Navigation.goBack();
} else {
ReportActions.updateReportField(report.reportID, {...reportField, value: value === '' ? null : value}, reportField);
+ Navigation.dismissModal(report?.reportID);
}
-
- Navigation.dismissModal(report?.reportID);
};
const handleReportFieldDelete = () => {
diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx
index 8a0d71ec8d8c..6eb6a6cc7161 100644
--- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx
+++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx
@@ -9,7 +9,6 @@ import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
-import useHybridAppMiddleware from '@hooks/useHybridAppMiddleware';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -35,7 +34,6 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {navigateToExitUrl} = useHybridAppMiddleware();
const {email = '', shortLivedAuthToken = '', shortLivedToken = '', authTokenType, exitTo, error} = route?.params ?? {};
useEffect(() => {
@@ -64,10 +62,10 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA
Session.setAccountError(error);
}
- if (exitTo) {
+ // For HybridApp we have separate logic to handle transitions.
+ if (!NativeModules.HybridAppModule && exitTo) {
Navigation.isNavigationReady().then(() => {
- const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : (exitTo as Route);
- navigateToExitUrl(url);
+ Navigation.navigate(exitTo as Route);
});
}
// The only dependencies of the effect are based on props.route
diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx
index 622a7db6e086..f5b96e2d57c5 100644
--- a/src/pages/LogOutPreviousUserPage.tsx
+++ b/src/pages/LogOutPreviousUserPage.tsx
@@ -5,7 +5,6 @@ import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import {InitialURLContext} from '@components/InitialURLContextProvider';
-import useHybridAppMiddleware from '@hooks/useHybridAppMiddleware';
import * as SessionUtils from '@libs/SessionUtils';
import Navigation from '@navigation/Navigation';
import type {AuthScreensParamList} from '@navigation/types';
@@ -33,7 +32,6 @@ type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreen
// This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate
function LogOutPreviousUserPage({session, route, isAccountLoading}: LogOutPreviousUserPageProps) {
const initialURL = useContext(InitialURLContext);
- const {navigateToExitUrl} = useHybridAppMiddleware();
useEffect(() => {
const sessionEmail = session?.email;
@@ -42,7 +40,8 @@ function LogOutPreviousUserPage({session, route, isAccountLoading}: LogOutPrevio
const isSupportalLogin = route.params.authTokenType === CONST.AUTH_TOKEN_TYPES.SUPPORT;
if (isLoggingInAsNewUser) {
- SessionActions.signOutAndRedirectToSignIn(false, isSupportalLogin);
+ // We don't want to close react-native app in this particular case.
+ SessionActions.signOutAndRedirectToSignIn(false, isSupportalLogin, false);
return;
}
@@ -78,12 +77,12 @@ function LogOutPreviousUserPage({session, route, isAccountLoading}: LogOutPrevio
// We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
// because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
// which is already called when AuthScreens mounts.
- if (exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !isAccountLoading && !isLoggingInAsNewUser) {
+ // For HybridApp we have separate logic to handle transitions.
+ if (!NativeModules.HybridAppModule && exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !isAccountLoading && !isLoggingInAsNewUser) {
Navigation.isNavigationReady().then(() => {
// remove this screen and navigate to exit route
- const exitUrl = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo;
Navigation.goBack();
- navigateToExitUrl(exitUrl);
+ Navigation.navigate(exitTo);
});
}
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx
index 2cfe5dbde75e..17baddd39251 100755
--- a/src/pages/NewChatPage.tsx
+++ b/src/pages/NewChatPage.tsx
@@ -1,7 +1,6 @@
import isEmpty from 'lodash/isEmpty';
import reject from 'lodash/reject';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
@@ -32,7 +31,6 @@ import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {Beta} from '@src/types/onyx';
import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
type NewChatPageProps = {
@@ -52,12 +50,12 @@ function useOptions({isGroupChat}: NewChatPageProps) {
shouldInitialize: didScreenTransitionEnd,
});
- const options = useMemo(() => {
+ const defaultOptions = useMemo(() => {
const filteredOptions = OptionsListUtils.getFilteredOptions(
listOptions.reports ?? [],
listOptions.personalDetails ?? [],
- (betas ?? []) as OnyxEntry,
- debouncedSearchTerm,
+ betas ?? [],
+ '',
selectedOptions,
isGroupChat ? excludedGroupEmails : [],
false,
@@ -71,19 +69,33 @@ function useOptions({isGroupChat}: NewChatPageProps) {
true,
undefined,
undefined,
- CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
+ 0,
undefined,
true,
);
+ return filteredOptions;
+ }, [betas, isGroupChat, listOptions.personalDetails, listOptions.reports, selectedOptions]);
- const headerMessage = OptionsListUtils.getHeaderMessage(
- filteredOptions.personalDetails.length + filteredOptions.recentReports.length !== 0,
- !!filteredOptions.userToInvite,
- debouncedSearchTerm.trim(),
- selectedOptions.some((participant) => participant?.searchText?.toLowerCase?.().includes(debouncedSearchTerm.trim().toLowerCase())),
- );
- return {...filteredOptions, headerMessage};
- }, [betas, debouncedSearchTerm, isGroupChat, listOptions.personalDetails, listOptions.reports, selectedOptions]);
+ const options = useMemo(() => {
+ const filteredOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, {
+ selectedOptions,
+ excludeLogins: isGroupChat ? excludedGroupEmails : [],
+ maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
+ });
+
+ return filteredOptions;
+ }, [debouncedSearchTerm, defaultOptions, isGroupChat, selectedOptions]);
+
+ const headerMessage = useMemo(
+ () =>
+ OptionsListUtils.getHeaderMessage(
+ options.personalDetails.length + options.recentReports.length !== 0,
+ !!options.userToInvite,
+ debouncedSearchTerm.trim(),
+ selectedOptions.some((participant) => participant?.searchText?.toLowerCase?.().includes(debouncedSearchTerm.trim().toLowerCase())),
+ ),
+ [debouncedSearchTerm, options.personalDetails.length, options.recentReports.length, options.userToInvite, selectedOptions],
+ );
useEffect(() => {
if (!debouncedSearchTerm.length) {
@@ -120,7 +132,16 @@ function useOptions({isGroupChat}: NewChatPageProps) {
setSelectedOptions(newSelectedOptions);
}, [newGroupDraft?.participants, listOptions.personalDetails, betas, personalData.accountID]);
- return {...options, searchTerm, debouncedSearchTerm, setSearchTerm, areOptionsInitialized: areOptionsInitialized && didScreenTransitionEnd, selectedOptions, setSelectedOptions};
+ return {
+ ...options,
+ searchTerm,
+ debouncedSearchTerm,
+ setSearchTerm,
+ areOptionsInitialized: areOptionsInitialized && didScreenTransitionEnd,
+ selectedOptions,
+ setSelectedOptions,
+ headerMessage,
+ };
}
function NewChatPage({isGroupChat}: NewChatPageProps) {
diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
index f5bd14ed7aa1..52e2d817e6db 100644
--- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
+++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useState} from 'react';
+import React, {useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
@@ -12,7 +12,6 @@ import Text from '@components/Text';
import TextInput from '@components/TextInput';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
-import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape';
import useLocalize from '@hooks/useLocalize';
import useOnboardingLayout from '@hooks/useOnboardingLayout';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -46,7 +45,9 @@ function BaseOnboardingPersonalDetails({
const [shouldValidateOnChange, setShouldValidateOnChange] = useState(false);
const {accountID} = useSession();
- useDisableModalDismissOnEscape();
+ useEffect(() => {
+ Welcome.setOnboardingErrorMessage('');
+ }, []);
const completeEngagement = useCallback(
(values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => {
diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
index 03a4b790bc5f..7304c1822ae9 100644
--- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
+++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
@@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {ScrollView} from 'react-native-gesture-handler';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
@@ -13,7 +13,6 @@ import MenuItemList from '@components/MenuItemList';
import OfflineIndicator from '@components/OfflineIndicator';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import Text from '@components/Text';
-import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape';
import useLocalize from '@hooks/useLocalize';
import useOnboardingLayout from '@hooks/useOnboardingLayout';
import useTheme from '@hooks/useTheme';
@@ -28,7 +27,8 @@ import type {OnboardingPurposeType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps} from './types';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+import type {BaseOnboardingPurposeProps} from './types';
const menuIcons = {
[CONST.ONBOARDING_CHOICES.EMPLOYER]: Illustrations.ReceiptUpload,
@@ -38,15 +38,15 @@ const menuIcons = {
[CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: Illustrations.Binoculars,
};
-function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, onboardingPurposeSelected}: BaseOnboardingPurposeProps) {
+function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight}: BaseOnboardingPurposeProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useOnboardingLayout();
const [selectedPurpose, setSelectedPurpose] = useState(undefined);
const {isSmallScreenWidth, windowHeight} = useWindowDimensions();
const theme = useTheme();
-
- useDisableModalDismissOnEscape();
+ const [onboardingPurposeSelected, onboardingPurposeSelectedResult] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED);
+ const [onboardingErrorMessage, onboardingErrorMessageResult] = useOnyx(ONYXKEYS.ONBOARDING_ERROR_MESSAGE);
const PurposeFooterInstance = ;
@@ -83,8 +83,6 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on
Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS);
}, [selectedPurpose]);
- const [errorMessage, setErrorMessage] = useState('');
-
const menuItems: MenuItemProps[] = Object.values(CONST.ONBOARDING_CHOICES).map((choice) => {
const translationKey = `onboarding.purpose.${choice}` as const;
const isSelected = selectedPurpose === choice;
@@ -103,7 +101,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on
numberOfLinesTitle: 0,
onPress: () => {
Welcome.setOnboardingPurposeSelected(choice);
- setErrorMessage('');
+ Welcome.setOnboardingErrorMessage('');
},
};
});
@@ -111,15 +109,18 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on
const handleOuterClick = useCallback(() => {
if (!selectedPurpose) {
- setErrorMessage(translate('onboarding.purpose.errorSelection'));
+ Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorSelection'));
} else {
- setErrorMessage(translate('onboarding.purpose.errorContinue'));
+ Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorContinue'));
}
- }, [selectedPurpose, setErrorMessage, translate]);
+ }, [selectedPurpose, translate]);
const onboardingLocalRef = useRef(null);
useImperativeHandle(isFocused ? OnboardingRefManager.ref : onboardingLocalRef, () => ({handleOuterClick}), [handleOuterClick]);
+ if (isLoadingOnyxValue(onboardingPurposeSelectedResult, onboardingErrorMessageResult)) {
+ return null;
+ }
return (
{({safeAreaPaddingBottomStyle}) => (
@@ -148,14 +149,14 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on
buttonText={translate('common.continue')}
onSubmit={() => {
if (!selectedPurpose) {
- setErrorMessage(translate('onboarding.purpose.errorSelection'));
+ Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorSelection'));
return;
}
- setErrorMessage('');
+ Welcome.setOnboardingErrorMessage('');
saveAndNavigate();
}}
- message={errorMessage}
- isAlertVisible={!!errorMessage}
+ message={onboardingErrorMessage}
+ isAlertVisible={!!onboardingErrorMessage}
containerStyles={[styles.w100, styles.mb5, styles.mh0, paddingHorizontal]}
/>
@@ -166,10 +167,6 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on
BaseOnboardingPurpose.displayName = 'BaseOnboardingPurpose';
-export default withOnyx({
- onboardingPurposeSelected: {
- key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED,
- },
-})(BaseOnboardingPurpose);
+export default BaseOnboardingPurpose;
export type {BaseOnboardingPurposeProps};
diff --git a/src/pages/OnboardingPurpose/types.ts b/src/pages/OnboardingPurpose/types.ts
index 8c8f11503f1a..17970dbab9a6 100644
--- a/src/pages/OnboardingPurpose/types.ts
+++ b/src/pages/OnboardingPurpose/types.ts
@@ -1,20 +1,11 @@
-import type {OnyxEntry} from 'react-native-onyx';
-import type {OnboardingPurposeType} from '@src/CONST';
-
type OnboardingPurposeProps = Record;
-type BaseOnboardingPurposeOnyxProps = {
- /** Saved onboarding purpose selected by the user */
- onboardingPurposeSelected: OnyxEntry;
-};
-
-type BaseOnboardingPurposeProps = OnboardingPurposeProps &
- BaseOnboardingPurposeOnyxProps & {
- /* Whether to use native styles tailored for native devices */
- shouldUseNativeStyles: boolean;
+type BaseOnboardingPurposeProps = OnboardingPurposeProps & {
+ /* Whether to use native styles tailored for native devices */
+ shouldUseNativeStyles: boolean;
- /** Whether to use the maxHeight (true) or use the 100% of the height (false) */
- shouldEnableMaxHeight: boolean;
- };
+ /** Whether to use the maxHeight (true) or use the 100% of the height (false) */
+ shouldEnableMaxHeight: boolean;
+};
-export type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps, OnboardingPurposeProps};
+export type {BaseOnboardingPurposeProps, OnboardingPurposeProps};
diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx
index 9b8824300d30..14f9223f6c67 100644
--- a/src/pages/OnboardingWork/BaseOnboardingWork.tsx
+++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx
@@ -9,7 +9,6 @@ import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
import OfflineIndicator from '@components/OfflineIndicator';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape';
import useLocalize from '@hooks/useLocalize';
import useOnboardingLayout from '@hooks/useOnboardingLayout';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -33,8 +32,6 @@ function BaseOnboardingWork({shouldUseNativeStyles, onboardingPurposeSelected, o
const {isSmallScreenWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useOnboardingLayout();
- useDisableModalDismissOnEscape();
-
const completeEngagement = useCallback(
(values: FormOnyxValues<'onboardingWorkForm'>) => {
if (!onboardingPurposeSelected) {
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index 530df5fa0532..c9440ee548af 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -200,9 +200,9 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
const shouldShowDeleteButton = shouldShowTaskDeleteButton || canDeleteRequest;
const canUnapproveRequest =
- ReportUtils.isMoneyRequestReport(moneyRequestReport) &&
- (ReportUtils.isReportManager(moneyRequestReport) || isPolicyAdmin) &&
- (ReportUtils.isReportApproved(moneyRequestReport) || ReportUtils.isReportManuallyReimbursed(moneyRequestReport));
+ ReportUtils.isExpenseReport(report) &&
+ (ReportUtils.isReportManager(report) || isPolicyAdmin) &&
+ (ReportUtils.isReportApproved(report) || ReportUtils.isReportManuallyReimbursed(report));
useEffect(() => {
if (canDeleteRequest) {
diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx
new file mode 100644
index 000000000000..2506f6bf2453
--- /dev/null
+++ b/src/pages/Search/SearchHoldReasonPage.tsx
@@ -0,0 +1,69 @@
+import type {RouteProp} from '@react-navigation/native';
+import React, {useCallback, useEffect} from 'react';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import {useSearchContext} from '@components/Search/SearchContext';
+import useLocalize from '@hooks/useLocalize';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import HoldReasonFormView from '@pages/iou/HoldReasonFormView';
+import * as FormActions from '@userActions/FormActions';
+import * as SearchActions from '@src/libs/actions/Search';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm';
+
+type SearchHoldReasonPageRouteParams = {
+ /** ID of the transaction the page was opened for */
+ transactionID: string;
+
+ /** Link to previous page */
+ backTo: Route;
+};
+
+type SearchHoldReasonPageProps = {
+ /** Navigation route context info provided by react navigation */
+ route: RouteProp<{params: SearchHoldReasonPageRouteParams}>;
+};
+
+function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) {
+ const {translate} = useLocalize();
+
+ const {currentSearchHash} = useSearchContext();
+ const {transactionID, backTo} = route.params;
+
+ const onSubmit = (values: FormOnyxValues) => {
+ SearchActions.holdMoneyRequestOnSearch(currentSearchHash, [transactionID], values.comment);
+
+ Navigation.goBack();
+ };
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMMENT]);
+
+ if (!values.comment) {
+ errors.comment = translate('common.error.fieldRequired');
+ }
+
+ return errors;
+ },
+ [translate],
+ );
+
+ useEffect(() => {
+ FormActions.clearErrors(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM);
+ FormActions.clearErrorFields(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM);
+ }, []);
+
+ return (
+
+ );
+}
+
+SearchHoldReasonPage.displayName = 'SearchHoldReasonPage';
+
+export default SearchHoldReasonPage;
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index d15378431d72..662e92658e9d 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -20,12 +20,13 @@ import ScreenWrapper from '@components/ScreenWrapper';
import TaskHeaderActionButton from '@components/TaskHeaderActionButton';
import type {CurrentReportIDContextValue} from '@components/withCurrentReportID';
import withCurrentReportID from '@components/withCurrentReportID';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useAppFocusEvent from '@hooks/useAppFocusEvent';
import useDeepCompareRef from '@hooks/useDeepCompareRef';
import useIsReportOpenInRHP from '@hooks/useIsReportOpenInRHP';
-import useLastAccessedReportID from '@hooks/useLastAccessedReportID';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
@@ -146,10 +147,12 @@ function ReportScreen({
const prevIsFocused = usePrevious(isFocused);
const firstRenderRef = useRef(true);
const flatListRef = useRef(null);
+ const {canUseDefaultRooms} = usePermissions();
const reactionListRef = useRef(null);
const {isOffline} = useNetwork();
const isReportOpenInRHP = useIsReportOpenInRHP();
const {isSmallScreenWidth} = useWindowDimensions();
+ const {activeWorkspaceID} = useActiveWorkspace();
const shouldUseNarrowLayout = isSmallScreenWidth || isReportOpenInRHP;
const [modal] = useOnyx(ONYXKEYS.MODAL);
@@ -169,8 +172,6 @@ function ReportScreen({
const isLoadingReportOnyx = isLoadingOnyxValue(reportResult);
const permissions = useDeepCompareRef(reportOnyx?.permissions);
- // Check if there's a reportID in the route. If not, set it to the last accessed reportID
- const lastAccessedReportID = useLastAccessedReportID(!!route.params.openOnAdminRoom);
useEffect(() => {
// Don't update if there is a reportID in the params already
if (route.params.reportID) {
@@ -182,6 +183,8 @@ function ReportScreen({
return;
}
+ const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID;
+
// It's possible that reports aren't fully loaded yet
// in that case the reportID is undefined
if (!lastAccessedReportID) {
@@ -190,7 +193,7 @@ function ReportScreen({
Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`);
navigation.setParams({reportID: lastAccessedReportID});
- }, [lastAccessedReportID, navigation, route]);
+ }, [activeWorkspaceID, canUseDefaultRooms, navigation, route]);
/**
* Create a lightweight Report so as to keep the re-rendering as light as possible by
@@ -583,7 +586,7 @@ function ReportScreen({
}
Navigation.dismissModal();
if (Navigation.getTopmostReportId() === prevOnyxReportID) {
- Navigation.setShouldPopAllStateOnUP();
+ Navigation.setShouldPopAllStateOnUP(true);
Navigation.goBack(undefined, false, true);
}
if (prevReport.parentReportID) {
diff --git a/src/pages/iou/HoldReasonFormView.tsx b/src/pages/iou/HoldReasonFormView.tsx
new file mode 100644
index 000000000000..197d00b6b0ed
--- /dev/null
+++ b/src/pages/iou/HoldReasonFormView.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm';
+
+type HoldReasonFormViewProps = {
+ /** Submit function for submitting form */
+ onSubmit: (values: FormOnyxValues) => void;
+
+ /** Submit function for validating form */
+ validate: (values: FormOnyxValues) => Partial>;
+
+ /** Link to previous page */
+ backTo: Route;
+};
+
+function HoldReasonFormView({backTo, validate, onSubmit}: HoldReasonFormViewProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ return (
+
+ Navigation.goBack(backTo)}
+ />
+
+ {translate('iou.explainHold')}
+
+
+
+
+
+ );
+}
+
+HoldReasonFormView.displayName = 'HoldReasonFormViewProps';
+
+export default HoldReasonFormView;
diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx
index 2fc789f77c6c..82f29acf7d10 100644
--- a/src/pages/iou/HoldReasonPage.tsx
+++ b/src/pages/iou/HoldReasonPage.tsx
@@ -1,17 +1,8 @@
import type {RouteProp} from '@react-navigation/native';
import React, {useCallback, useEffect} from 'react';
-import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import ScreenWrapper from '@components/ScreenWrapper';
-import Text from '@components/Text';
-import TextInput from '@components/TextInput';
-import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -22,6 +13,7 @@ import * as IOU from '@userActions/IOU';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm';
+import HoldReasonFormView from './HoldReasonFormView';
type HoldReasonPageRouteParams = {
/** ID of the transaction the page was opened for */
@@ -40,9 +32,7 @@ type HoldReasonPageProps = {
};
function HoldReasonPage({route}: HoldReasonPageProps) {
- const styles = useThemeStyles();
const {translate} = useLocalize();
- const {inputCallbackRef} = useAutoFocusInput();
const {transactionID, reportID, backTo} = route.params;
@@ -53,10 +43,6 @@ function HoldReasonPage({route}: HoldReasonPageProps) {
const isWorkspaceRequest = ReportUtils.isReportInGroupPolicy(report);
const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1');
- const navigateBack = () => {
- Navigation.navigate(backTo);
- };
-
const onSubmit = (values: FormOnyxValues) => {
// We have extra isWorkspaceRequest condition since, for 1:1 requests, canEditMoneyRequest will rightly return false
// as we do not allow requestee to edit fields like description and amount.
@@ -66,7 +52,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) {
}
IOU.putOnHold(transactionID, values.comment, reportID);
- navigateBack();
+ Navigation.navigate(backTo);
};
const validate = useCallback(
@@ -96,38 +82,11 @@ function HoldReasonPage({route}: HoldReasonPageProps) {
}, []);
return (
-
- Navigation.goBack(backTo)}
- />
-
- {translate('iou.explainHold')}
-
-
-
-
-
+
);
}
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
index 671cc6d06706..5fef11fa4b15 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
@@ -551,7 +551,9 @@ function IOURequestStepConfirmation({
{
if (NativeModules.HybridAppModule) {
Navigation.resetToHome();
- showSplashScreenOnNextStart();
NativeModules.HybridAppModule.closeReactNativeApp();
return;
}
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 936c43b56ee5..adbf5a664c82 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -43,7 +43,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session} from '@src/types/onyx';
+import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployeeList, Session} from '@src/types/onyx';
import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
@@ -304,14 +304,6 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft,
[route.params.policyID],
);
- /**
- * Check if the policy member is deleted from the workspace
- */
- const isDeletedPolicyEmployee = useCallback(
- (policyEmployee: PolicyEmployee): boolean => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors),
- [isOffline],
- );
-
const policyOwner = policy?.owner;
const currentUserLogin = currentUserPersonalDetails.login;
const invitedPrimaryToSecondaryLogins = invertObject(policy?.primaryLoginsInvited ?? {});
@@ -320,7 +312,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft,
Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => {
const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? '');
- if (isDeletedPolicyEmployee(policyEmployee)) {
+ if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) {
return;
}
@@ -375,13 +367,13 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft,
invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '',
});
});
- result = result.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase()));
+ result = OptionsListUtils.sortItemsAlphabetically(result);
return result;
}, [
+ isOffline,
currentUserLogin,
formatPhoneNumber,
invitedPrimaryToSecondaryLogins,
- isDeletedPolicyEmployee,
isPolicyAdmin,
personalDetails,
policy?.owner,
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index ca4c990aea2a..400d54d0e005 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -128,9 +128,9 @@ function accountingIntegrationData(
integrationToDisconnect={integrationToDisconnect}
/>
),
- onImportPagePress: () => {},
- onExportPagePress: () => {},
- onAdvancedPagePress: () => {},
+ onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT.getRoute(policyID)),
+ onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)),
+ onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.getRoute(policyID)),
};
default:
return undefined;
@@ -152,7 +152,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting
const lastSyncProgressDate = parseISO(connectionSyncProgress?.timestamp ?? '');
const isSyncInProgress =
!!connectionSyncProgress?.stageInProgress &&
- connectionSyncProgress.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE &&
+ (connectionSyncProgress.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE || !policy.connections?.[connectionSyncProgress.connectionName]) &&
isValid(lastSyncProgressDate) &&
differenceInMinutes(new Date(), lastSyncProgressDate) < CONST.POLICY.CONNECTIONS.SYNC_STAGE_TIMEOUT_MINUTES;
diff --git a/src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx b/src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx
index 75f4cab3783d..fa7fdbfa1fc1 100644
--- a/src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx
+++ b/src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx
@@ -10,7 +10,7 @@ import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import connectToSageIntacct from '@libs/actions/connections/SageIntacct';
+import {connectToSageIntacct} from '@libs/actions/connections/SageIntacct';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
@@ -20,9 +20,9 @@ import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/SageIntactCredentialsForm';
-type IntacctPrerequisitesPageProps = StackScreenProps;
+type SageIntacctPrerequisitesPageProps = StackScreenProps;
-function EnterSageIntacctCredentialsPage({route}: IntacctPrerequisitesPageProps) {
+function EnterSageIntacctCredentialsPage({route}: SageIntacctPrerequisitesPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const policyID: string = route.params.policyID;
@@ -93,6 +93,6 @@ function EnterSageIntacctCredentialsPage({route}: IntacctPrerequisitesPageProps)
);
}
-EnterSageIntacctCredentialsPage.displayName = 'PolicyEnterSageIntacctCredentialsPage';
+EnterSageIntacctCredentialsPage.displayName = 'EnterSageIntacctCredentialsPage';
export default EnterSageIntacctCredentialsPage;
diff --git a/src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx b/src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx
index 7e867d88285d..eaa63bbcaadb 100644
--- a/src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx
+++ b/src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx
@@ -59,6 +59,6 @@ function ExistingConnectionsPage({route}: ExistingConnectionsPageProps) {
);
}
-ExistingConnectionsPage.displayName = 'PolicyExistingConnectionsPage';
+ExistingConnectionsPage.displayName = 'ExistingConnectionsPage';
export default ExistingConnectionsPage;
diff --git a/src/pages/workspace/accounting/intacct/IntacctPrerequisitesPage.tsx b/src/pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage.tsx
similarity index 91%
rename from src/pages/workspace/accounting/intacct/IntacctPrerequisitesPage.tsx
rename to src/pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage.tsx
index eaf46392fb04..8e921ca934b2 100644
--- a/src/pages/workspace/accounting/intacct/IntacctPrerequisitesPage.tsx
+++ b/src/pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage.tsx
@@ -23,9 +23,9 @@ import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-type IntacctPrerequisitesPageProps = StackScreenProps;
+type SageIntacctPrerequisitesPageProps = StackScreenProps;
-function IntacctPrerequisitesPage({route}: IntacctPrerequisitesPageProps) {
+function SageIntacctPrerequisitesPage({route}: SageIntacctPrerequisitesPageProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const popoverAnchor = useRef(null);
@@ -67,7 +67,7 @@ function IntacctPrerequisitesPage({route}: IntacctPrerequisitesPageProps) {
bankAccount.id === reimbursementAccountID)?.name ?? reimbursementAccountID;
+}
+
+function SageIntacctAdvancedPage({policy}: WithPolicyProps) {
+ const {translate} = useLocalize();
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+
+ const {importEmployees, autoSync, sync, pendingFields, errorFields, credentials} = policy?.connections?.intacct?.config ?? {};
+ const {data, config} = policy?.connections?.intacct ?? {};
+
+ const currentSageIntacctOrganizationName = credentials?.companyID;
+
+ const toggleSections = useMemo(
+ () => [
+ {
+ label: translate('workspace.sageIntacct.autoSync'),
+ description: translate('workspace.sageIntacct.autoSyncDescription'),
+ isActive: !!autoSync?.enabled,
+ onToggle: (enabled: boolean) => updateSageIntacctAutoSync(policyID, enabled),
+ pendingAction: pendingFields?.enabled,
+ error: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED),
+ onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED),
+ },
+ {
+ label: translate('workspace.sageIntacct.inviteEmployees'),
+ description: translate('workspace.sageIntacct.inviteEmployeesDescription'),
+ isActive: !!importEmployees,
+ onToggle: (enabled: boolean) => {
+ updateSageIntacctImportEmployees(policyID, enabled);
+ updateSageIntacctApprovalMode(policyID, enabled);
+ },
+ pendingAction: pendingFields?.importEmployees,
+ error:
+ ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES) ??
+ ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE),
+ onCloseError: () => {
+ Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES);
+ Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE);
+ },
+ },
+ {
+ label: translate('workspace.sageIntacct.syncReimbursedReports'),
+ description: translate('workspace.sageIntacct.syncReimbursedReportsDescription'),
+ isActive: !!sync?.syncReimbursedReports,
+ onToggle: (enabled: boolean) => {
+ updateSageIntacctSyncReimbursedReports(policyID, enabled);
+
+ if (enabled && !sync?.reimbursementAccountID) {
+ const reimbursementAccountID = data?.bankAccounts[0]?.id ?? '';
+ updateSageIntacctSyncReimbursementAccountID(policyID, reimbursementAccountID);
+ }
+ },
+ pendingAction: pendingFields?.syncReimbursedReports,
+ error: ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS),
+ onCloseError: () => {
+ Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS);
+ },
+ },
+ ],
+ [
+ translate,
+ autoSync?.enabled,
+ pendingFields?.enabled,
+ pendingFields?.importEmployees,
+ pendingFields?.syncReimbursedReports,
+ config,
+ importEmployees,
+ sync?.syncReimbursedReports,
+ sync?.reimbursementAccountID,
+ policyID,
+ data?.bankAccounts,
+ ],
+ );
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID))}
+ >
+ {toggleSections.map((section) => (
+
+ ))}
+
+ {!!sync?.syncReimbursedReports && (
+
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.getRoute(policyID))}
+ brickRoadIndicator={errorFields?.reimbursementAccountID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ />
+
+ )}
+
+ );
+}
+
+SageIntacctAdvancedPage.displayName = 'SageIntacctAdvancedPage';
+
+export default withPolicy(SageIntacctAdvancedPage);
diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx
new file mode 100644
index 000000000000..dadd1dc0af2c
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx
@@ -0,0 +1,76 @@
+import React, {useCallback, useMemo} from 'react';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import * as Illustrations from '@components/Icon/Illustrations';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {SelectorType} from '@components/SelectionScreen';
+import SelectionScreen from '@components/SelectionScreen';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import {getSageIntacctBankAccounts} from '@libs/PolicyUtils';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import variables from '@styles/variables';
+import {updateSageIntacctSyncReimbursementAccountID} from '@userActions/connections/SageIntacct';
+import * as Policy from '@userActions/Policy/Policy';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+function SageIntacctPaymentAccountPage({policy}: WithPolicyConnectionsProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const policyID = policy?.id ?? '-1';
+
+ const {config} = policy?.connections?.intacct ?? {};
+
+ const vendorSelectorOptions = useMemo(() => getSageIntacctBankAccounts(policy, config?.sync?.reimbursementAccountID), [policy, config?.sync?.reimbursementAccountID]);
+
+ const updateDefaultVendor = useCallback(
+ ({value}: SelectorType) => {
+ if (value !== config?.sync?.reimbursementAccountID) {
+ updateSageIntacctSyncReimbursementAccountID(policyID, value);
+ }
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.getRoute(policyID));
+ },
+ [policyID, config?.sync?.reimbursementAccountID],
+ );
+
+ const listEmptyContent = useMemo(
+ () => (
+
+ ),
+ [translate],
+ );
+
+ return (
+ mode.isSelected)?.keyForList}
+ onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.getRoute(policyID))}
+ title="workspace.sageIntacct.paymentAccount"
+ listEmptyContent={listEmptyContent}
+ connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}
+ pendingAction={config?.pendingFields?.reimbursementAccountID}
+ errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID)}
+ errorRowStyles={[styles.ph5, styles.mv2]}
+ onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID)}
+ />
+ );
+}
+
+SageIntacctPaymentAccountPage.displayName = 'SageIntacctPaymentAccountPage';
+
+export default withPolicyConnections(SageIntacctPaymentAccountPage);
diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx
new file mode 100644
index 000000000000..51d08423e530
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx
@@ -0,0 +1,74 @@
+import React, {useCallback, useMemo} from 'react';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {ListItem} from '@components/SelectionList/types';
+import SelectionScreen from '@components/SelectionScreen';
+import type {SelectorType} from '@components/SelectionScreen';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import {updateSageIntacctExportDate} from '@userActions/connections/SageIntacct';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+type MenuListItem = ListItem & {
+ value: ValueOf;
+};
+
+function SageIntacctDatePage({policy}: WithPolicyProps) {
+ const {translate} = useLocalize();
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const {export: exportConfig} = policy?.connections?.intacct?.config ?? {};
+ const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_EXPORT_DATE).map((dateType) => ({
+ value: dateType,
+ text: translate(`workspace.sageIntacct.exportDate.values.${dateType}.label`),
+ alternateText: translate(`workspace.sageIntacct.exportDate.values.${dateType}.description`),
+ keyForList: dateType,
+ isSelected: exportConfig?.exportDate === dateType,
+ }));
+
+ const headerContent = useMemo(
+ () => (
+
+ {translate('workspace.sageIntacct.exportDate.description')}
+
+ ),
+ [translate, styles.pb5, styles.ph5],
+ );
+
+ const selectExportDate = useCallback(
+ (row: MenuListItem) => {
+ if (row.value !== exportConfig?.exportDate) {
+ updateSageIntacctExportDate(policyID, row.value);
+ }
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID));
+ },
+ [exportConfig?.exportDate, policyID],
+ );
+
+ return (
+ selectExportDate(selection as MenuListItem)}
+ initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList}
+ policyID={policyID}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
+ featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
+ onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))}
+ connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}
+ />
+ );
+}
+
+SageIntacctDatePage.displayName = 'SageIntacctDatePage';
+
+export default withPolicyConnections(SageIntacctDatePage);
diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx
new file mode 100644
index 000000000000..4782cfba9e97
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx
@@ -0,0 +1,112 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useMemo} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import * as Illustrations from '@components/Icon/Illustrations';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {SelectorType} from '@components/SelectionScreen';
+import SelectionScreen from '@components/SelectionScreen';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import {getSageIntacctNonReimbursableActiveDefaultVendor, getSageIntacctVendors} from '@libs/PolicyUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import variables from '@styles/variables';
+import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {Connections} from '@src/types/onyx/Policy';
+
+type SageIntacctDefaultVendorPageProps = StackScreenProps;
+
+function SageIntacctDefaultVendorPage({route}: SageIntacctDefaultVendorPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const policyID = route.params.policyID ?? '-1';
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
+ const isReimbursable = route.params.reimbursable === CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE;
+
+ let defaultVendor;
+ let settingName: keyof Connections['intacct']['config']['export'];
+ if (!isReimbursable) {
+ const {nonReimbursable} = policy?.connections?.intacct?.config.export ?? {};
+ defaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy);
+ settingName =
+ nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE
+ ? CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR
+ : CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR;
+ } else {
+ const {reimbursableExpenseReportDefaultVendor} = policy?.connections?.intacct?.config.export ?? {};
+ defaultVendor = reimbursableExpenseReportDefaultVendor;
+ settingName = CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR;
+ }
+
+ const vendorSelectorOptions = useMemo(() => getSageIntacctVendors(policy, defaultVendor), [defaultVendor, policy]);
+
+ const listHeaderComponent = useMemo(
+ () => (
+
+ {translate('workspace.sageIntacct.defaultVendorDescription', isReimbursable)}
+
+ ),
+ [translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal, isReimbursable],
+ );
+
+ const updateDefaultVendor = useCallback(
+ ({value}: SelectorType) => {
+ if (value !== defaultVendor) {
+ updateSageIntacctDefaultVendor(policyID, settingName, value);
+ }
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID));
+ },
+ [defaultVendor, policyID, settingName],
+ );
+
+ const listEmptyContent = useMemo(
+ () => (
+
+ ),
+ [translate, styles.pb10],
+ );
+
+ return (
+ mode.isSelected)?.keyForList}
+ headerContent={listHeaderComponent}
+ onBackButtonPress={() =>
+ Navigation.goBack(
+ isReimbursable
+ ? ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES.getRoute(policyID)
+ : ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID),
+ )
+ }
+ title="workspace.sageIntacct.defaultVendor"
+ listEmptyContent={listEmptyContent}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
+ connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}
+ />
+ );
+}
+
+SageIntacctDefaultVendorPage.displayName = 'SageIntacctDefaultVendorPage';
+
+export default SageIntacctDefaultVendorPage;
diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx
new file mode 100644
index 000000000000..b6aceb56b6ce
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx
@@ -0,0 +1,91 @@
+import React, {useMemo} from 'react';
+import ConnectionLayout from '@components/ConnectionLayout';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/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';
+
+function SageIntacctExportPage({policy}: WithPolicyProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const policyID = policy?.id ?? '-1';
+
+ const {export: exportConfig, credentials} = policy?.connections?.intacct?.config ?? {};
+
+ const sections = useMemo(
+ () => [
+ {
+ description: translate('workspace.sageIntacct.preferredExporter'),
+ action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREFERRED_EXPORTER.getRoute(policyID)),
+ title: exportConfig?.exporter ?? translate('workspace.sageIntacct.notConfigured'),
+ hasError: !!exportConfig?.errorFields?.exporter,
+ pendingAction: exportConfig?.pendingFields?.exporter,
+ },
+ {
+ description: translate('workspace.sageIntacct.exportDate.label'),
+ action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT_DATE.getRoute(policyID)),
+ title: exportConfig?.exportDate ? translate(`workspace.sageIntacct.exportDate.values.${exportConfig.exportDate}.label`) : translate(`workspace.sageIntacct.notConfigured`),
+ hasError: !!exportConfig?.errorFields?.exportDate,
+ pendingAction: exportConfig?.pendingFields?.exportDate,
+ },
+ {
+ description: translate('workspace.sageIntacct.reimbursableExpenses.label'),
+ action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES.getRoute(policyID)),
+ title: exportConfig?.reimbursable
+ ? translate(`workspace.sageIntacct.reimbursableExpenses.values.${exportConfig.reimbursable}`)
+ : translate('workspace.sageIntacct.notConfigured'),
+ hasError: !!exportConfig?.errorFields?.reimbursable,
+ pendingAction: exportConfig?.pendingFields?.reimbursable,
+ },
+ {
+ description: translate('workspace.sageIntacct.nonReimbursableExpenses.label'),
+ action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID)),
+ title: exportConfig?.nonReimbursable
+ ? translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${exportConfig.nonReimbursable}`)
+ : translate('workspace.sageIntacct.notConfigured'),
+ hasError: !!exportConfig?.errorFields?.nonReimbursable,
+ pendingAction: exportConfig?.pendingFields?.nonReimbursable,
+ },
+ ],
+ [exportConfig, policyID, translate],
+ );
+
+ return (
+
+ {sections.map((section) => (
+
+
+
+ ))}
+
+ );
+}
+
+SageIntacctExportPage.displayName = 'SageIntacctExportPage';
+
+export default withPolicyConnections(SageIntacctExportPage);
diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx
new file mode 100644
index 000000000000..f22b653b26f9
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx
@@ -0,0 +1,72 @@
+import React, {useCallback, useMemo} from 'react';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import * as Illustrations from '@components/Icon/Illustrations';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {SelectorType} from '@components/SelectionScreen';
+import SelectionScreen from '@components/SelectionScreen';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import {getSageIntacctCreditCards} from '@libs/PolicyUtils';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import variables from '@styles/variables';
+import {updateSageIntacctNonreimbursableExpensesExportAccount} from '@userActions/connections/SageIntacct';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+function SageIntacctNonReimbursableCreditCardAccountPage({policy}: WithPolicyConnectionsProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const policyID = policy?.id ?? '-1';
+
+ const {export: exportConfig} = policy?.connections?.intacct?.config ?? {};
+
+ const creditCardSelectorOptions = useMemo(() => getSageIntacctCreditCards(policy, exportConfig?.nonReimbursableAccount), [exportConfig?.nonReimbursableAccount, policy]);
+
+ const updateCreditCardAccount = useCallback(
+ ({value}: SelectorType) => {
+ if (value !== exportConfig?.nonReimbursableAccount) {
+ updateSageIntacctNonreimbursableExpensesExportAccount(policyID, value);
+ }
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID));
+ },
+ [policyID, exportConfig?.nonReimbursableAccount],
+ );
+
+ const listEmptyContent = useMemo(
+ () => (
+
+ ),
+ [translate, styles.pb10],
+ );
+
+ return (
+ mode.isSelected)?.keyForList}
+ onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID))}
+ title="workspace.sageIntacct.creditCardAccount"
+ listEmptyContent={listEmptyContent}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
+ connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}
+ />
+ );
+}
+
+SageIntacctNonReimbursableCreditCardAccountPage.displayName = 'SageIntacctNonReimbursableCreditCardAccountPage';
+
+export default withPolicyConnections(SageIntacctNonReimbursableCreditCardAccountPage);
diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx
new file mode 100644
index 000000000000..ba977cbba238
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx
@@ -0,0 +1,174 @@
+import React, {useCallback, useMemo} from 'react';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import ConnectionLayout from '@components/ConnectionLayout';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {ListItem} from '@components/SelectionList/types';
+import type {SelectorType} from '@components/SelectionScreen';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getSageIntacctNonReimbursableActiveDefaultVendor} from '@libs/PolicyUtils';
+import Navigation from '@navigation/Navigation';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
+import {updateSageIntacctDefaultVendor, updateSageIntacctNonreimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type {SageIntacctDataElementWithValue} from '@src/types/onyx/Policy';
+
+type MenuListItem = ListItem & {
+ value: ValueOf;
+};
+
+function getDefaultVendorName(defaultVendor?: string, vendors?: SageIntacctDataElementWithValue[]): string | undefined {
+ return (vendors ?? []).find((vendor) => vendor.id === defaultVendor)?.value ?? defaultVendor;
+}
+
+function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyProps) {
+ const {translate} = useLocalize();
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const {data: intacctData, config} = policy?.connections?.intacct ?? {};
+
+ const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE).map((expenseType) => ({
+ value: expenseType,
+ text: translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${expenseType}`),
+ keyForList: expenseType,
+ isSelected: config?.export.nonReimbursable === expenseType,
+ }));
+
+ const selectNonReimbursableExpense = useCallback(
+ (row: MenuListItem) => {
+ if (row.value === config?.export.nonReimbursable) {
+ return;
+ }
+ updateSageIntacctNonreimbursableExpensesExportDestination(policyID, row.value);
+ },
+ [config?.export.nonReimbursable, policyID],
+ );
+
+ const defaultVendor = useMemo(() => {
+ const activeDefaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy);
+ const defaultVendorName = getDefaultVendorName(activeDefaultVendor, intacctData?.vendors);
+
+ const defaultVendorSection = {
+ description: translate('workspace.sageIntacct.defaultVendor'),
+ action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE.toLowerCase())),
+ title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'),
+ hasError:
+ config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL
+ ? !!config?.export?.errorFields?.nonReimbursableVendor
+ : !!config?.export?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor,
+ pendingAction:
+ config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL
+ ? config?.export?.pendingFields?.nonReimbursableVendor
+ : config?.export?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor,
+ };
+
+ return (
+
+
+
+ );
+ }, [
+ config?.export?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor,
+ config?.export?.errorFields?.nonReimbursableVendor,
+ config?.export.nonReimbursable,
+ config?.export?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor,
+ config?.export?.pendingFields?.nonReimbursableVendor,
+ intacctData?.vendors,
+ policy,
+ policyID,
+ translate,
+ ]);
+
+ const creditCardAccount = useMemo(() => {
+ const creditCardAccountSection = {
+ description: translate('workspace.sageIntacct.creditCardAccount'),
+ action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT.getRoute(policyID)),
+ title: config?.export.nonReimbursableAccount ? config.export.nonReimbursableAccount : translate('workspace.sageIntacct.notConfigured'),
+ hasError: !!config?.export?.errorFields?.nonReimbursableAccount,
+ pendingAction: config?.export?.pendingFields?.nonReimbursableAccount,
+ };
+
+ return (
+
+
+
+ );
+ }, [config?.export?.errorFields?.nonReimbursableAccount, config?.export.nonReimbursableAccount, config?.export?.pendingFields?.nonReimbursableAccount, policyID, translate]);
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
+ featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
+ displayName={SageIntacctNonReimbursableExpensesPage.displayName}
+ policyID={policyID}
+ connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}
+ contentContainerStyle={[styles.flex1]}
+ shouldUseScrollView={false}
+ >
+ selectNonReimbursableExpense(selection as MenuListItem)}
+ sections={[{data}]}
+ ListItem={RadioListItem}
+ showScrollIndicator
+ shouldShowTooltips={false}
+ listFooterContent={
+
+ {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL && defaultVendor}
+ {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && (
+
+ {creditCardAccount}
+ {
+ const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : '';
+ updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, vendor);
+ }}
+ wrapperStyle={[styles.ph5, styles.pv3]}
+ pendingAction={config?.export?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor}
+ />
+ {!!config?.export.nonReimbursableCreditCardChargeDefaultVendor && defaultVendor}
+
+ )}
+
+ }
+ />
+
+ );
+}
+
+SageIntacctNonReimbursableExpensesPage.displayName = 'SageIntacctNonReimbursableExpensesPage';
+
+export default withPolicyConnections(SageIntacctNonReimbursableExpensesPage);
diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx
new file mode 100644
index 000000000000..abbb87982a5e
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx
@@ -0,0 +1,104 @@
+import {isEmpty} from 'lodash';
+import React, {useCallback, useMemo} from 'react';
+import {View} from 'react-native';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {ListItem} from '@components/SelectionList/types';
+import SelectionScreen from '@components/SelectionScreen';
+import Text from '@components/Text';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getAdminEmployees, isExpensifyTeam} from '@libs/PolicyUtils';
+import Navigation from '@navigation/Navigation';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import {updateSageIntacctExporter} from '@userActions/connections/SageIntacct';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+type CardListItem = ListItem & {
+ value: string;
+};
+
+function SageIntacctPreferredExporterPage({policy}: WithPolicyProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const policyOwner = policy?.owner ?? '';
+ const {export: exportConfiguration} = policy?.connections?.intacct?.config ?? {};
+ const exporters = getAdminEmployees(policy);
+ const {login: currentUserLogin} = useCurrentUserPersonalDetails();
+
+ const policyID = policy?.id ?? '-1';
+ const data: CardListItem[] = useMemo(() => {
+ if (!isEmpty(policyOwner) && isEmpty(exporters)) {
+ return [
+ {
+ value: policyOwner,
+ text: policyOwner,
+ keyForList: policyOwner,
+ isSelected: exportConfiguration?.exporter === policyOwner,
+ },
+ ];
+ }
+
+ return exporters?.reduce((options, exporter) => {
+ if (!exporter.email) {
+ return options;
+ }
+
+ // Don't show guides if the current user is not a guide themselves or an Expensify employee
+ if (isExpensifyTeam(exporter.email) && !isExpensifyTeam(policyOwner) && !isExpensifyTeam(currentUserLogin)) {
+ return options;
+ }
+
+ options.push({
+ value: exporter.email,
+ text: exporter.email,
+ keyForList: exporter.email,
+ isSelected: exportConfiguration?.exporter === exporter.email,
+ });
+ return options;
+ }, []);
+ }, [exportConfiguration, exporters, policyOwner, currentUserLogin]);
+
+ const selectExporter = useCallback(
+ (row: CardListItem) => {
+ if (row.value !== exportConfiguration?.exporter) {
+ updateSageIntacctExporter(policyID, row.value);
+ }
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID));
+ },
+ [policyID, exportConfiguration],
+ );
+
+ const headerContent = useMemo(
+ () => (
+
+ {translate('workspace.sageIntacct.exportPreferredExporterNote')}
+ {translate('workspace.sageIntacct.exportPreferredExporterSubNote')}
+
+ ),
+ [translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal],
+ );
+
+ return (
+ mode.isSelected)?.keyForList}
+ onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))}
+ title="workspace.sageIntacct.preferredExporter"
+ connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}
+ />
+ );
+}
+
+SageIntacctPreferredExporterPage.displayName = 'SageIntacctPreferredExporterPage';
+
+export default withPolicyConnections(SageIntacctPreferredExporterPage);
diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx
new file mode 100644
index 000000000000..b3a4a04a5582
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx
@@ -0,0 +1,136 @@
+import React, {useCallback, useMemo} from 'react';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import ConnectionLayout from '@components/ConnectionLayout';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {ListItem} from '@components/SelectionList/types';
+import type {SelectorType} from '@components/SelectionScreen';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
+import {updateSageIntacctDefaultVendor, updateSageIntacctReimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type {SageIntacctDataElementWithValue} from '@src/types/onyx/Policy';
+
+type MenuListItem = ListItem & {
+ value: ValueOf;
+};
+
+function getDefaultVendorName(defaultVendor?: string, vendors?: SageIntacctDataElementWithValue[]): string | undefined {
+ return (vendors ?? []).find((vendor) => vendor.id === defaultVendor)?.value ?? defaultVendor;
+}
+
+function SageIntacctReimbursableExpensesPage({policy}: WithPolicyProps) {
+ const {translate} = useLocalize();
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const {data: intacctData, config} = policy?.connections?.intacct ?? {};
+ const {reimbursable, reimbursableExpenseReportDefaultVendor} = policy?.connections?.intacct?.config?.export ?? {};
+
+ const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE).map((expenseType) => ({
+ value: expenseType,
+ text: translate(`workspace.sageIntacct.reimbursableExpenses.values.${expenseType}`),
+ keyForList: expenseType,
+ isSelected: reimbursable === expenseType,
+ }));
+
+ const selectReimbursableDestination = useCallback(
+ (row: MenuListItem) => {
+ if (row.value !== reimbursable) {
+ updateSageIntacctReimbursableExpensesExportDestination(policyID, row.value);
+ }
+ if (row.value === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL) {
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID));
+ }
+ },
+ [reimbursable, policyID],
+ );
+
+ const defaultVendor = useMemo(() => {
+ const defaultVendorName = getDefaultVendorName(reimbursableExpenseReportDefaultVendor, intacctData?.vendors);
+ const defaultVendorSection = {
+ description: translate('workspace.sageIntacct.defaultVendor'),
+ action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)),
+ title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'),
+ hasError: !!config?.export?.errorFields?.reimbursableExpenseReportDefaultVendor,
+ pendingAction: config?.export?.pendingFields?.reimbursableExpenseReportDefaultVendor,
+ };
+
+ return (
+
+
+
+ );
+ }, [
+ config?.export?.errorFields?.reimbursableExpenseReportDefaultVendor,
+ config?.export?.pendingFields?.reimbursableExpenseReportDefaultVendor,
+ intacctData?.vendors,
+ policyID,
+ reimbursableExpenseReportDefaultVendor,
+ translate,
+ ]);
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
+ featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
+ displayName={SageIntacctReimbursableExpensesPage.displayName}
+ policyID={policyID}
+ connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}
+ contentContainerStyle={[styles.flex1]}
+ shouldUseScrollView={false}
+ >
+ selectReimbursableDestination(selection as MenuListItem)}
+ sections={[{data}]}
+ ListItem={RadioListItem}
+ showScrollIndicator
+ shouldShowTooltips={false}
+ listFooterContent={
+ reimbursable === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT ? (
+
+ {
+ const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : '';
+ updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR, vendor);
+ }}
+ wrapperStyle={[styles.ph5, styles.pv3]}
+ pendingAction={config?.export?.pendingFields?.reimbursableExpenseReportDefaultVendor}
+ />
+ {!!reimbursableExpenseReportDefaultVendor && defaultVendor}
+
+ ) : undefined
+ }
+ />
+
+ );
+}
+
+SageIntacctReimbursableExpensesPage.displayName = 'SageIntacctReimbursableExpensesPage';
+
+export default withPolicyConnections(SageIntacctReimbursableExpensesPage);
diff --git a/src/pages/workspace/accounting/intacct/import/DimensionTypeSelector.tsx b/src/pages/workspace/accounting/intacct/import/DimensionTypeSelector.tsx
new file mode 100644
index 000000000000..05d0c0438c8f
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/import/DimensionTypeSelector.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import {View} from 'react-native';
+import FormHelpMessage from '@components/FormHelpMessage';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {SelectorType} from '@components/SelectionScreen';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+type DimensionTypeSelectorProps = {
+ /** Error text to display */
+ errorText?: string;
+
+ /** Business type to display */
+ value?: string;
+
+ /** Callback to call when the input changes */
+ onInputChange?: (value: string) => void;
+};
+
+function DimensionTypeSelector({errorText = '', value = '', onInputChange}: DimensionTypeSelectorProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const selectionOptions = [
+ {
+ value: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG,
+ text: translate('common.tag'),
+ alternateText: translate('workspace.common.lineItemLevel'),
+ keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG,
+ isSelected: value === CONST.SAGE_INTACCT_MAPPING_VALUE.TAG,
+ },
+ {
+ value: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD,
+ text: translate('workspace.common.reportField'),
+ alternateText: translate('workspace.common.reportLevel'),
+ keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD,
+ isSelected: value === CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD,
+ },
+ ];
+
+ const onDimensionTypeSelected = (dimensionType: SelectorType) => {
+ if (!onInputChange || dimensionType.value === value) {
+ return;
+ }
+ onInputChange(dimensionType.value);
+ };
+
+ return (
+
+ {translate('workspace.common.displayedAs')}
+
+ {selectionOptions.map((option) => (
+
+ ))}
+ {!!errorText && (
+
+ )}
+
+
+ );
+}
+
+export default DimensionTypeSelector;
diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage.tsx
new file mode 100644
index 000000000000..3dc7a325650b
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage.tsx
@@ -0,0 +1,101 @@
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import ConnectionLayout from '@components/ConnectionLayout';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {addSageIntacctUserDimensions} from '@libs/actions/connections/SageIntacct';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import withPolicy from '@pages/workspace/withPolicy';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/SageIntacctDimensionsForm';
+import DimensionTypeSelector from './DimensionTypeSelector';
+
+function SageIntacctAddUserDimensionPage({policy}: WithPolicyProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const policyID = policy?.id ?? '-1';
+ const userDimensions = policy?.connections?.intacct?.config?.mappings?.dimensions;
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+
+ if (!values[INPUT_IDS.INTEGRATION_NAME]) {
+ ErrorUtils.addErrorMessage(errors, INPUT_IDS.INTEGRATION_NAME, translate('common.error.fieldRequired'));
+ }
+
+ if (userDimensions?.some((userDimension) => userDimension.dimension === values[INPUT_IDS.INTEGRATION_NAME])) {
+ ErrorUtils.addErrorMessage(errors, INPUT_IDS.INTEGRATION_NAME, translate('workspace.intacct.dimensionExists'));
+ }
+
+ if (!values[INPUT_IDS.DIMENSION_TYPE]) {
+ ErrorUtils.addErrorMessage(errors, INPUT_IDS.DIMENSION_TYPE, translate('common.error.fieldRequired'));
+ }
+ return errors;
+ },
+ [translate, userDimensions],
+ );
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID))}
+ >
+ {
+ addSageIntacctUserDimensions(policyID, value[INPUT_IDS.INTEGRATION_NAME], value[INPUT_IDS.DIMENSION_TYPE], userDimensions ?? []);
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID));
+ }}
+ submitButtonText={translate('common.confirm')}
+ enabledWhenOffline
+ shouldValidateOnBlur
+ shouldValidateOnChange
+ >
+
+
+
+
+
+
+
+
+ );
+}
+
+SageIntacctAddUserDimensionPage.displayName = 'SageIntacctAddUserDimensionPage';
+
+export default withPolicy(SageIntacctAddUserDimensionPage);
diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx
new file mode 100644
index 000000000000..d0d9578a72b2
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx
@@ -0,0 +1,141 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import ConfirmModal from '@components/ConfirmModal';
+import ConnectionLayout from '@components/ConnectionLayout';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {clearSageIntacctErrorField, editSageIntacctUserDimensions, removeSageIntacctUserDimensions} from '@libs/actions/connections/SageIntacct';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/SageIntacctDimensionsForm';
+import DimensionTypeSelector from './DimensionTypeSelector';
+
+type SageIntacctEditUserDimensionsPageProps = StackScreenProps;
+
+function SageIntacctEditUserDimensionsPage({route}: SageIntacctEditUserDimensionsPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const editedUserDimensionName: string = route.params.dimensionName;
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`);
+ const policyID: string = policy?.id ?? '-1';
+ const config = policy?.connections?.intacct?.config;
+ const userDimensions = policy?.connections?.intacct?.config?.mappings?.dimensions;
+ const editedUserDimension = userDimensions?.find((userDimension) => userDimension.dimension === editedUserDimensionName);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+
+ if (!values[INPUT_IDS.INTEGRATION_NAME]) {
+ ErrorUtils.addErrorMessage(errors, INPUT_IDS.INTEGRATION_NAME, translate('common.error.fieldRequired'));
+ }
+
+ if (userDimensions?.some((userDimension) => userDimension.dimension === values[INPUT_IDS.INTEGRATION_NAME] && editedUserDimensionName !== values[INPUT_IDS.INTEGRATION_NAME])) {
+ ErrorUtils.addErrorMessage(errors, INPUT_IDS.INTEGRATION_NAME, translate('workspace.intacct.dimensionExists'));
+ }
+
+ if (!values[INPUT_IDS.DIMENSION_TYPE]) {
+ ErrorUtils.addErrorMessage(errors, INPUT_IDS.DIMENSION_TYPE, translate('common.error.fieldRequired'));
+ }
+ return errors;
+ },
+ [editedUserDimensionName, translate, userDimensions],
+ );
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID))}
+ >
+ {
+ editSageIntacctUserDimensions(policyID, editedUserDimensionName, value[INPUT_IDS.INTEGRATION_NAME], value[INPUT_IDS.DIMENSION_TYPE], userDimensions ?? []);
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID));
+ }}
+ submitButtonText={translate('common.save')}
+ enabledWhenOffline
+ shouldValidateOnBlur
+ shouldValidateOnChange
+ >
+ clearSageIntacctErrorField(policyID, `dimension_${editedUserDimensionName}`)}
+ >
+
+
+
+
+
+
+
+
+
+ {
+ setIsDeleteModalOpen(false);
+ removeSageIntacctUserDimensions(policyID, editedUserDimensionName, userDimensions ?? []);
+ Navigation.goBack();
+ }}
+ onCancel={() => setIsDeleteModalOpen(false)}
+ prompt={translate('workspace.intacct.removeDimensionPrompt')}
+ confirmText={translate('common.remove')}
+ cancelText={translate('common.cancel')}
+ danger
+ shouldEnableNewFocusManagement
+ />
+
+
+ );
+}
+
+SageIntacctEditUserDimensionsPage.displayName = 'SageIntacctEditUserDimensionsPage';
+
+export default SageIntacctEditUserDimensionsPage;
diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx
new file mode 100644
index 000000000000..22dfc25c7dda
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx
@@ -0,0 +1,144 @@
+import {Str} from 'expensify-common';
+import React, {useMemo} from 'react';
+import ConnectionLayout from '@components/ConnectionLayout';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {clearSageIntacctErrorField, updateSageIntacctBillable, updateSageIntacctSyncTaxConfiguration} from '@libs/actions/connections/SageIntacct';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import withPolicy from '@pages/workspace/withPolicy';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ROUTES from '@src/ROUTES';
+import type {SageIntacctConnectionsConfig, SageIntacctMappingValue} from '@src/types/onyx/Policy';
+
+function getDisplayTypeTranslationKey(displayType?: SageIntacctMappingValue): TranslationPaths | undefined {
+ switch (displayType) {
+ case CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT: {
+ return 'workspace.intacct.employeeDefault';
+ }
+ case CONST.SAGE_INTACCT_MAPPING_VALUE.TAG: {
+ return 'workspace.accounting.importTypes.TAG';
+ }
+ case CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD: {
+ return 'workspace.accounting.importTypes.REPORT_FIELD';
+ }
+ default: {
+ return 'workspace.accounting.notImported';
+ }
+ }
+}
+
+const checkForUserDimensionWithError = (config?: SageIntacctConnectionsConfig) =>
+ config?.mappings?.dimensions?.some((dimension) => !!config?.errorFields?.[`dimension_${dimension.dimension}`]);
+
+const checkForUserDimensionWithPendingAction = (config?: SageIntacctConnectionsConfig) =>
+ config?.mappings?.dimensions?.some((dimension) => !!config?.pendingFields?.[`dimension_${dimension.dimension}`]);
+
+function SageIntacctImportPage({policy}: WithPolicyProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const policyID: string = policy?.id ?? '-1';
+ const sageIntacctConfig = policy?.connections?.intacct?.config;
+
+ const mapingItems = useMemo(
+ () =>
+ Object.values(CONST.SAGE_INTACCT_CONFIG.MAPPINGS).map((mapping) => {
+ const menuItemTitleKey = getDisplayTypeTranslationKey(sageIntacctConfig?.mappings?.[mapping]);
+ return {
+ description: Str.recapitalize(translate('workspace.intacct.mappingTitle', mapping)),
+ action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS.getRoute(policyID, mapping)),
+ title: menuItemTitleKey ? translate(menuItemTitleKey) : undefined,
+ hasError: !!sageIntacctConfig?.errorFields?.[mapping],
+ pendingAction: sageIntacctConfig?.pendingFields?.[mapping],
+ };
+ }),
+ [policyID, sageIntacctConfig?.errorFields, sageIntacctConfig?.mappings, sageIntacctConfig?.pendingFields, translate],
+ );
+
+ return (
+
+ {}}
+ disabled
+ />
+ updateSageIntacctBillable(policyID, !sageIntacctConfig?.mappings?.syncItems)}
+ pendingAction={sageIntacctConfig?.pendingFields?.syncItems}
+ errors={ErrorUtils.getLatestErrorField(sageIntacctConfig ?? {}, CONST.SAGE_INTACCT_CONFIG.SYNC_ITEMS)}
+ onCloseError={() => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_ITEMS)}
+ />
+
+ {mapingItems.map((section) => (
+
+
+
+ ))}
+
+ updateSageIntacctSyncTaxConfiguration(policyID, !sageIntacctConfig?.tax?.syncTax)}
+ pendingAction={sageIntacctConfig?.pendingFields?.tax}
+ errors={ErrorUtils.getLatestErrorField(sageIntacctConfig ?? {}, CONST.SAGE_INTACCT_CONFIG.TAX)}
+ onCloseError={() => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.TAX)}
+ />
+
+
+ 0
+ ? translate('workspace.intacct.userDimensionsAdded', sageIntacctConfig?.mappings?.dimensions?.length)
+ : undefined
+ }
+ description={translate('workspace.intacct.userDefinedDimensions')}
+ shouldShowRightIcon
+ onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID))}
+ brickRoadIndicator={checkForUserDimensionWithError(sageIntacctConfig) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ />
+
+
+ );
+}
+
+SageIntacctImportPage.displayName = 'SageIntacctImportPage';
+
+export default withPolicy(SageIntacctImportPage);
diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx
new file mode 100644
index 000000000000..57ebac617393
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx
@@ -0,0 +1,81 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useMemo} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import SelectionScreen from '@components/SelectionScreen';
+import type {SelectorType} from '@components/SelectionScreen';
+import useLocalize from '@hooks/useLocalize';
+import {updateSageIntacctMappingValue} from '@libs/actions/connections/SageIntacct';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {SageIntacctMappingName, SageIntacctMappingValue} from '@src/types/onyx/Policy';
+
+type SageIntacctMappingsTypePageProps = StackScreenProps;
+
+function SageIntacctMappingsTypePage({route}: SageIntacctMappingsTypePageProps) {
+ const {translate} = useLocalize();
+
+ const mappingName: SageIntacctMappingName = route.params.mapping;
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`);
+ const policyID = policy?.id ?? '-1';
+ const mappings = policy?.connections?.intacct?.config?.mappings;
+
+ const selectionOptions = useMemo(
+ () => [
+ {
+ value: CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT,
+ text: translate('workspace.intacct.employeeDefault'),
+ alternateText: translate('workspace.common.appliedOnExport'),
+ keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT,
+ isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT,
+ },
+ {
+ value: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG,
+ text: translate('workspace.common.tags'),
+ alternateText: translate('workspace.common.lineItemLevel'),
+ keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG,
+ isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.TAG,
+ },
+ {
+ value: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD,
+ text: translate('workspace.common.reportFields'),
+ alternateText: translate('workspace.common.reportLevel'),
+ keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD,
+ isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD,
+ },
+ ],
+ [mappingName, mappings, translate],
+ );
+
+ const updateMapping = useCallback(
+ ({value}: SelectorType) => {
+ updateSageIntacctMappingValue(policyID, mappingName, value as SageIntacctMappingValue);
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS.getRoute(policyID, mappingName));
+ },
+ [mappingName, policyID],
+ );
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS.getRoute(policyID, mappingName))}
+ title="workspace.common.displayedAs"
+ />
+ );
+}
+
+SageIntacctMappingsTypePage.displayName = 'SageIntacctMappingsTypePage';
+
+export default SageIntacctMappingsTypePage;
diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx
new file mode 100644
index 000000000000..c76a9f0e26bc
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx
@@ -0,0 +1,122 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import {Str} from 'expensify-common';
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import ConnectionLayout from '@components/ConnectionLayout';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {clearSageIntacctErrorField, updateSageIntacctMappingValue} from '@libs/actions/connections/SageIntacct';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {SageIntacctMappingName, SageIntacctMappingValue} from '@src/types/onyx/Policy';
+
+type SageIntacctToggleMappingsPageProps = StackScreenProps;
+
+type DisplayTypeTranslationKeys = {
+ titleKey: TranslationPaths;
+ descriptionKey: TranslationPaths;
+};
+
+function getDisplayTypeTranslationKeys(displayType?: SageIntacctMappingValue): DisplayTypeTranslationKeys | undefined {
+ switch (displayType) {
+ case CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT: {
+ return {titleKey: 'workspace.intacct.employeeDefault', descriptionKey: 'workspace.intacct.employeeDefaultDescription'};
+ }
+ case CONST.SAGE_INTACCT_MAPPING_VALUE.TAG: {
+ return {titleKey: 'workspace.common.tags', descriptionKey: 'workspace.intacct.displayedAsTagDescription'};
+ }
+ case CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD: {
+ return {titleKey: 'workspace.common.reportFields', descriptionKey: 'workspace.intacct.displayedAsReportFieldDescription'};
+ }
+ default: {
+ return undefined;
+ }
+ }
+}
+
+function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPageProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`);
+ const mappingName: SageIntacctMappingName = route.params.mapping;
+ const policyID: string = policy?.id ?? '-1';
+
+ const config = policy?.connections?.intacct?.config;
+ const translationKeys = getDisplayTypeTranslationKeys(config?.mappings?.[mappingName]);
+ const [importMapping, setImportMapping] = useState(config?.mappings?.[mappingName] && config?.mappings?.[mappingName] !== CONST.SAGE_INTACCT_MAPPING_VALUE.NONE);
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT.getRoute(policyID))}
+ >
+
+ {translate('workspace.intacct.toggleImportTitleFirstPart')}
+ {translate('workspace.intacct.mappingTitle', mappingName)}
+ {translate('workspace.intacct.toggleImportTitleSecondPart')}
+
+ clearSageIntacctErrorField(policyID, mappingName)}
+ >
+ {
+ if (importMapping) {
+ setImportMapping(false);
+ updateSageIntacctMappingValue(policyID, mappingName, CONST.SAGE_INTACCT_MAPPING_VALUE.NONE);
+ } else {
+ setImportMapping(true);
+ updateSageIntacctMappingValue(policyID, mappingName, CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT);
+ }
+ }}
+ />
+ {importMapping && (
+
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE.getRoute(policyID, mappingName))}
+ />
+
+ {translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined}
+
+
+ )}
+
+
+ );
+}
+
+SageIntacctToggleMappingsPage.displayName = 'SageIntacctToggleMappingsPage';
+
+export default SageIntacctToggleMappingsPage;
diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctUserDimensionsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctUserDimensionsPage.tsx
new file mode 100644
index 000000000000..4a6d5b7bd21c
--- /dev/null
+++ b/src/pages/workspace/accounting/intacct/import/SageIntacctUserDimensionsPage.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import {View} from 'react-native';
+import Button from '@components/Button';
+import ConnectionLayout from '@components/ConnectionLayout';
+import FixedFooter from '@components/FixedFooter';
+import Icon from '@components/Icon';
+import * as Illustrations from '@components/Icon/Illustrations';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import withPolicy from '@pages/workspace/withPolicy';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import * as Link from '@userActions/Link';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+function SageIntacctUserDimensionsPage({policy}: WithPolicyProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const policyID = policy?.id ?? '-1';
+ const config = policy?.connections?.intacct?.config;
+ const userDimensions = policy?.connections?.intacct?.config?.mappings?.dimensions ?? [];
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT.getRoute(policyID))}
+ >
+ {userDimensions?.length === 0 ? (
+
+
+
+
+
+ {translate('workspace.intacct.addAUserDefinedDimension')}
+
+
+
+
+ {
+ Link.openExternalLink(CONST.SAGE_INTACCT_INSTRUCTIONS);
+ }}
+ >
+ {translate('workspace.intacct.detailedInstructionsLink')}
+
+ {translate('workspace.intacct.detailedInstructionsRestOfSentence')}
+
+
+
+
+ ) : (
+ <>
+
+
+ {
+ Link.openExternalLink(CONST.SAGE_INTACCT_INSTRUCTIONS);
+ }}
+ >
+ {translate('workspace.intacct.detailedInstructionsLink')}
+
+ {translate('workspace.intacct.detailedInstructionsRestOfSentence')}
+
+
+
+ {userDimensions.map((userDimension) => (
+
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EDIT_USER_DIMENSION.getRoute(policyID, userDimension.dimension))}
+ brickRoadIndicator={config?.errorFields?.[`dimension_${userDimension.dimension}`] ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ />
+
+ ))}
+
+ >
+ )}
+
+
+
+ );
+}
+
+SageIntacctUserDimensionsPage.displayName = 'SageIntacctUserDimensionsPage';
+
+export default withPolicy(SageIntacctUserDimensionsPage);
diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteTokenInputPage.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteTokenInputPage.tsx
index 41d428257163..8bfcc42a81da 100644
--- a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteTokenInputPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteTokenInputPage.tsx
@@ -60,8 +60,8 @@ function NetSuiteTokenInputPage({policy}: WithPolicyConnectionsProps) {
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={[styles.flex1]}
titleStyle={styles.ph5}
- reverseConnectionEmptyCheck
- connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO}
+ shouldLoadForEmptyConnection
+ connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
onBackButtonPress={handleBackButtonPress}
shouldIncludeSafeAreaPaddingBottom
>
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit.tsx
new file mode 100644
index 000000000000..9fe11b8eb00d
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit.tsx
@@ -0,0 +1,170 @@
+import React, {useCallback, useMemo} from 'react';
+import type {ValueOf} from 'type-fest';
+import ConnectionLayout from '@components/ConnectionLayout';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {updateNetSuiteCustomLists, updateNetSuiteCustomSegments} from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {NetSuiteCustomList, NetSuiteCustomSegment} from '@src/types/onyx/Policy';
+import NetSuiteCustomFieldMappingPicker from './NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker';
+
+type CustomField = NetSuiteCustomList | NetSuiteCustomSegment;
+type ImportCustomFieldsKeys = ValueOf;
+
+type NetSuiteImportCustomFieldViewProps = WithPolicyConnectionsProps & {
+ route: {
+ params: {
+ /** Whether the record is of type custom segment or list */
+ importCustomField: ImportCustomFieldsKeys;
+
+ /** Index of the current record */
+ valueIndex: number;
+
+ /** Selected field of the current record */
+ fieldName: string;
+ };
+ };
+};
+
+function NetSuiteImportCustomFieldEdit({
+ policy,
+ route: {
+ params: {importCustomField, valueIndex, fieldName},
+ },
+}: NetSuiteImportCustomFieldViewProps) {
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const config = policy?.connections?.netsuite?.options?.config;
+ const allRecords = useMemo(() => config?.syncOptions?.[importCustomField] ?? [], [config?.syncOptions, importCustomField]);
+
+ const customField: CustomField | undefined = allRecords[valueIndex];
+ const fieldValue = customField?.[fieldName as keyof CustomField] ?? '';
+
+ const updateRecord = useCallback(
+ (formValues: Partial>) => {
+ const newValue = formValues[fieldName as keyof typeof formValues];
+
+ if (customField) {
+ const updatedRecords = allRecords.map((record, index) => {
+ if (index === Number(valueIndex)) {
+ return {
+ ...record,
+ [fieldName]: newValue,
+ };
+ }
+ return record;
+ });
+
+ if (PolicyUtils.isNetSuiteCustomSegmentRecord(customField)) {
+ updateNetSuiteCustomSegments(policyID, updatedRecords as NetSuiteCustomSegment[], allRecords as NetSuiteCustomSegment[]);
+ } else {
+ updateNetSuiteCustomLists(policyID, updatedRecords as NetSuiteCustomList[], allRecords as NetSuiteCustomList[]);
+ }
+ }
+
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_VIEW.getRoute(policyID, importCustomField, valueIndex));
+ },
+ [allRecords, customField, fieldName, importCustomField, policyID, valueIndex],
+ );
+
+ const validate = useCallback(
+ (formValues: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+
+ const key = fieldName as keyof typeof formValues;
+ const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.${importCustomField}.fields.${fieldName}` as TranslationPaths);
+ if (!formValues[key]) {
+ ErrorUtils.addErrorMessage(errors, fieldName, translate('workspace.netsuite.import.importCustomFields.requiredFieldError', fieldLabel));
+ } else if (
+ policy?.connections?.netsuite?.options?.config?.syncOptions?.customSegments?.find(
+ (customSegment) => customSegment?.[fieldName as keyof typeof customSegment]?.toLowerCase() === formValues[key].toLowerCase(),
+ )
+ ) {
+ ErrorUtils.addErrorMessage(errors, fieldName, translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', fieldLabel));
+ }
+
+ return errors;
+ },
+ [fieldName, importCustomField, policy?.connections?.netsuite?.options?.config?.syncOptions?.customSegments, translate],
+ );
+
+ const renderForm = useMemo(
+ () =>
+ customField && (
+
+
+
+ ),
+ [config?.syncOptions?.pendingFields, customField, fieldName, fieldValue, importCustomField, styles.flexGrow1, styles.ph5, translate, updateRecord, validate],
+ );
+
+ const renderSelection = useMemo(
+ () =>
+ customField && (
+ {
+ updateRecord({
+ [fieldName]: value,
+ });
+ }}
+ value={fieldValue}
+ />
+ ),
+ [customField, fieldName, fieldValue, updateRecord],
+ );
+
+ const renderMap: Record = {
+ mapping: renderSelection,
+ };
+
+ return (
+
+ {renderMap[fieldName] || renderForm}
+
+ );
+}
+
+NetSuiteImportCustomFieldEdit.displayName = 'NetSuiteImportCustomFieldEdit';
+export default withPolicyConnections(NetSuiteImportCustomFieldEdit);
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker.tsx
new file mode 100644
index 000000000000..bf4cd65bd981
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import useLocalize from '@hooks/useLocalize';
+import CONST from '@src/CONST';
+
+type NetSuiteCustomListPickerProps = {
+ /** Selected mapping value */
+ value?: string;
+
+ /** Callback to fire when mapping is selected */
+ onInputChange?: (value: string) => void;
+};
+
+function NetSuiteCustomFieldMappingPicker({value, onInputChange}: NetSuiteCustomListPickerProps) {
+ const {translate} = useLocalize();
+
+ const options = [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD];
+
+ const selectionData =
+ options.map((option) => ({
+ text: translate(`workspace.netsuite.import.importTypes.${option}.label`),
+ keyForList: option,
+ isSelected: value === option,
+ value: option,
+ alternateText: translate(`workspace.netsuite.import.importTypes.${option}.description`),
+ })) ?? [];
+
+ return (
+ {
+ onInputChange?.(selected.value);
+ }}
+ ListItem={RadioListItem}
+ initiallyFocusedOptionKey={value ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG}
+ />
+ );
+}
+
+NetSuiteCustomFieldMappingPicker.displayName = 'NetSuiteCustomFieldMappingPicker';
+export default NetSuiteCustomFieldMappingPicker;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx
new file mode 100644
index 000000000000..18a4a8318316
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx
@@ -0,0 +1,67 @@
+import React, {useState} from 'react';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import useLocalize from '@hooks/useLocalize';
+import Navigation from '@libs/Navigation/Navigation';
+import type {CustomListSelectorType} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import type {Policy} from '@src/types/onyx';
+import NetSuiteCustomListSelectorModal from './NetSuiteCustomListSelectorModal';
+
+type NetSuiteCustomListPickerProps = {
+ /** Current value of the selected item */
+ value?: string;
+
+ /** Current connected policy */
+ policy?: Policy;
+
+ /** Callback when the list item is selected */
+ onInputChange?: (value: string, key?: string) => void;
+
+ /** Id of the internalID input to be updated on input change */
+ internalIDInputID?: string;
+
+ /** Form Error description */
+ errorText?: string;
+};
+
+function NetSuiteCustomListPicker({value, policy, internalIDInputID, errorText, onInputChange = () => {}}: NetSuiteCustomListPickerProps) {
+ const {translate} = useLocalize();
+ const [isPickerVisible, setIsPickerVisible] = useState(false);
+
+ const hidePickerModal = () => {
+ setIsPickerVisible(false);
+ };
+
+ const updateInput = (item: CustomListSelectorType) => {
+ onInputChange?.(item.value);
+ if (internalIDInputID) {
+ onInputChange(item.id, internalIDInputID);
+ }
+ hidePickerModal();
+ };
+
+ return (
+ <>
+ setIsPickerVisible(true)}
+ brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ errorText={errorText}
+ />
+
+ >
+ );
+}
+
+NetSuiteCustomListPicker.displayName = 'NetSuiteCustomListPicker';
+export default NetSuiteCustomListPicker;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx
new file mode 100644
index 000000000000..3eeebbafc8a3
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx
@@ -0,0 +1,112 @@
+import {Str} from 'expensify-common';
+import React, {useMemo} from 'react';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import Modal from '@components/Modal';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import useDebouncedState from '@hooks/useDebouncedState';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {CustomListSelectorType} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import type {Policy} from '@src/types/onyx';
+
+type NetSuiteCustomListSelectorModalProps = {
+ /** Whether the modal is visible */
+ isVisible: boolean;
+
+ /** Function to call when the user closes the business type selector modal */
+ onClose: () => void;
+
+ /** Label to display on field */
+ label: string;
+
+ /** Custom List value selected */
+ currentCustomListValue: string;
+
+ policy?: Policy;
+
+ /** Function to call when the user selects a custom list */
+ onCustomListSelected: (value: CustomListSelectorType) => void;
+
+ /** Function to call when the user presses on the modal backdrop */
+ onBackdropPress?: () => void;
+};
+
+function NetSuiteCustomListSelectorModal({isVisible, currentCustomListValue, onCustomListSelected, onClose, label, policy, onBackdropPress}: NetSuiteCustomListSelectorModalProps) {
+ const {translate} = useLocalize();
+ const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
+
+ const {sections, headerMessage, showTextInput} = useMemo(() => {
+ const customLists = policy?.connections?.netsuite?.options?.data?.customLists ?? [];
+ const customListData = customLists.map((customListRecord) => ({
+ text: customListRecord.name,
+ value: customListRecord.name,
+ isSelected: customListRecord.name === currentCustomListValue,
+ keyForList: customListRecord.name,
+ id: customListRecord.id,
+ }));
+
+ const searchRegex = new RegExp(Str.escapeForRegExp(debouncedSearchValue.trim()), 'i');
+ const filteredCustomLists = customListData.filter((customListRecord) => searchRegex.test(customListRecord.text ?? ''));
+ const isEmpty = debouncedSearchValue.trim() && !filteredCustomLists.length;
+
+ return {
+ sections: isEmpty
+ ? []
+ : [
+ {
+ data: filteredCustomLists,
+ },
+ ],
+ headerMessage: isEmpty ? translate('common.noResultsFound') : '',
+ showTextInput: customListData.length > CONST.NETSUITE_CONFIG.NETSUITE_CUSTOM_LIST_LIMIT,
+ };
+ }, [debouncedSearchValue, policy?.connections?.netsuite?.options?.data?.customLists, translate, currentCustomListValue]);
+
+ const styles = useThemeStyles();
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+NetSuiteCustomListSelectorModal.displayName = 'NetSuiteCustomListSelectorModal';
+
+export default NetSuiteCustomListSelectorModal;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx
new file mode 100644
index 000000000000..ae0b205e7373
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx
@@ -0,0 +1,176 @@
+import React, {useCallback, useMemo, useRef} from 'react';
+import type {ForwardedRef} from 'react';
+import {View} from 'react-native';
+import ConnectionLayout from '@components/ConnectionLayout';
+import FormProvider from '@components/Form/FormProvider';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
+import type {InteractiveStepSubHeaderHandle} from '@components/InteractiveStepSubHeader';
+import useLocalize from '@hooks/useLocalize';
+import useSubStep from '@hooks/useSubStep';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+import ChooseCustomListStep from './substeps/ChooseCustomListStep';
+import ConfirmCustomListStep from './substeps/ConfirmCustomListStep';
+import MappingStep from './substeps/MappingStep';
+import TransactionFieldIDStep from './substeps/TransactionFieldIDStep';
+
+const formSteps = [ChooseCustomListStep, TransactionFieldIDStep, MappingStep, ConfirmCustomListStep];
+
+function NetSuiteImportAddCustomListPage({policy}: WithPolicyConnectionsProps) {
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const ref: ForwardedRef = useRef(null);
+
+ const config = policy?.connections?.netsuite?.options?.config;
+ const customLists = useMemo(() => config?.syncOptions?.customLists ?? [], [config?.syncOptions]);
+
+ const handleFinishStep = useCallback(() => {
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS));
+ }, [policyID]);
+
+ const {
+ componentToRender: SubStep,
+ isEditing,
+ nextScreen,
+ prevScreen,
+ screenIndex,
+ moveTo,
+ goToTheLastStep,
+ } = useSubStep({
+ bodyContent: formSteps,
+ startFrom: CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER,
+ onFinished: handleFinishStep,
+ });
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+
+ // Clicking back on the first screen should go back to listing
+ if (screenIndex === CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER) {
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS));
+ return;
+ }
+ ref.current?.movePrevious();
+ prevScreen();
+ };
+
+ const handleNextScreen = useCallback(() => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+ ref.current?.moveNext();
+ nextScreen();
+ }, [goToTheLastStep, isEditing, nextScreen]);
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+ switch (screenIndex) {
+ case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER:
+ return ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.LIST_NAME]);
+ case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.TRANSACTION_FIELD_ID:
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.TRANSACTION_FIELD_ID])) {
+ const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customLists.fields.transactionFieldID`);
+ errors[INPUT_IDS.TRANSACTION_FIELD_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', fieldLabel);
+ } else if (customLists.find((customList) => customList.transactionFieldID.toLowerCase() === values[INPUT_IDS.TRANSACTION_FIELD_ID].toLowerCase())) {
+ errors[INPUT_IDS.TRANSACTION_FIELD_ID] = translate('workspace.netsuite.import.importCustomFields.customLists.errors.uniqueTransactionFieldIDError');
+ }
+ return errors;
+ case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.MAPPING:
+ return ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.MAPPING]);
+ default:
+ return errors;
+ }
+ },
+ [customLists, screenIndex, translate],
+ );
+
+ const updateNetSuiteCustomLists = useCallback(
+ (formValues: FormOnyxValues) => {
+ const updatedCustomLists = customLists.concat([
+ {
+ listName: formValues[INPUT_IDS.LIST_NAME],
+ internalID: formValues[INPUT_IDS.INTERNAL_ID],
+ transactionFieldID: formValues[INPUT_IDS.TRANSACTION_FIELD_ID],
+ mapping: formValues[INPUT_IDS.MAPPING] ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG,
+ },
+ ]);
+ Connections.updateNetSuiteCustomLists(policyID, updatedCustomLists, customLists);
+ nextScreen();
+ },
+ [customLists, nextScreen, policyID],
+ );
+
+ const selectionListForm = [CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.MAPPING as number].includes(screenIndex);
+ const submitFlexAllowed = [
+ CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER as number,
+ CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.TRANSACTION_FIELD_ID as number,
+ ].includes(screenIndex);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+NetSuiteImportAddCustomListPage.displayName = 'NetSuiteImportAddCustomListPage';
+
+export default withPolicyConnections(NetSuiteImportAddCustomListPage);
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage.tsx
new file mode 100644
index 000000000000..f6832edcae97
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage.tsx
@@ -0,0 +1,220 @@
+import React, {useCallback, useMemo, useRef, useState} from 'react';
+import type {ForwardedRef} from 'react';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import ConnectionLayout from '@components/ConnectionLayout';
+import FormProvider from '@components/Form/FormProvider';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
+import type {InteractiveStepSubHeaderHandle} from '@components/InteractiveStepSubHeader';
+import useLocalize from '@hooks/useLocalize';
+import useSubStep from '@hooks/useSubStep';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+import ChooseSegmentTypeStep from './substeps/ChooseSegmentTypeStep';
+import ConfirmCustomSegmentList from './substeps/ConfirmCustomSegmentList';
+import CustomSegmentInternalIdStep from './substeps/CustomSegmentInternalIdStep';
+import CustomSegmentNameStep from './substeps/CustomSegmentNameStep';
+import CustomSegmentScriptIdStep from './substeps/CustomSegmentScriptIdStep';
+import MappingStep from './substeps/MappingStep';
+
+const formSteps = [ChooseSegmentTypeStep, CustomSegmentNameStep, CustomSegmentInternalIdStep, CustomSegmentScriptIdStep, MappingStep, ConfirmCustomSegmentList];
+
+function NetSuiteImportAddCustomSegmentPage({policy}: WithPolicyConnectionsProps) {
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const ref: ForwardedRef = useRef(null);
+
+ const config = policy?.connections?.netsuite?.options?.config;
+ const customSegments = useMemo(() => config?.syncOptions?.customSegments ?? [], [config?.syncOptions]);
+
+ const handleFinishStep = useCallback(() => {
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS));
+ }, [policyID]);
+
+ const {
+ componentToRender: SubStep,
+ isEditing,
+ nextScreen,
+ prevScreen,
+ screenIndex,
+ moveTo,
+ goToTheLastStep,
+ } = useSubStep({bodyContent: formSteps, startFrom: CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_TYPE, onFinished: handleFinishStep});
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+
+ // Clicking back on the first screen should go back to listing
+ if (screenIndex === CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_TYPE) {
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS));
+ return;
+ }
+ ref.current?.movePrevious();
+ prevScreen();
+ };
+
+ const handleNextScreen = useCallback(() => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+ ref.current?.moveNext();
+ nextScreen();
+ }, [goToTheLastStep, isEditing, nextScreen]);
+
+ const [customSegmentType, setCustomSegmentType] = useState | undefined>();
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+ const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
+ switch (screenIndex) {
+ case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_NAME:
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.SEGMENT_NAME])) {
+ errors[INPUT_IDS.SEGMENT_NAME] = translate(
+ 'workspace.netsuite.import.importCustomFields.requiredFieldError',
+ translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}Name`),
+ );
+ } else if (customSegments.find((customSegment) => customSegment.segmentName.toLowerCase() === values[INPUT_IDS.SEGMENT_NAME].toLowerCase())) {
+ const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.segmentName`);
+ errors[INPUT_IDS.SEGMENT_NAME] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', fieldLabel);
+ }
+ return errors;
+ case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.INTERNAL_ID:
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.INTERNAL_ID])) {
+ const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.internalID`);
+ errors[INPUT_IDS.INTERNAL_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', fieldLabel);
+ } else if (customSegments.find((customSegment) => customSegment.internalID.toLowerCase() === values[INPUT_IDS.INTERNAL_ID].toLowerCase())) {
+ const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.internalID`);
+ errors[INPUT_IDS.INTERNAL_ID] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', fieldLabel);
+ }
+ return errors;
+ case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SCRIPT_ID:
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.SCRIPT_ID])) {
+ const fieldLabel = translate(
+ `workspace.netsuite.import.importCustomFields.customSegments.fields.${
+ customSegmentRecordType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT ? 'scriptID' : 'customRecordScriptID'
+ }`,
+ );
+ errors[INPUT_IDS.SCRIPT_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', fieldLabel);
+ } else if (customSegments.find((customSegment) => customSegment.scriptID.toLowerCase() === values[INPUT_IDS.SCRIPT_ID].toLowerCase())) {
+ const fieldLabel = translate(
+ `workspace.netsuite.import.importCustomFields.customSegments.fields.${
+ customSegmentRecordType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT ? 'scriptID' : 'customRecordScriptID'
+ }`,
+ );
+ errors[INPUT_IDS.SCRIPT_ID] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', fieldLabel);
+ }
+ return errors;
+ case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.MAPPING:
+ return ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.MAPPING]);
+ default:
+ return errors;
+ }
+ },
+ [customSegmentType, customSegments, screenIndex, translate],
+ );
+
+ const updateNetSuiteCustomSegments = useCallback(
+ (formValues: FormOnyxValues) => {
+ const updatedCustomSegments = customSegments.concat([
+ {
+ segmentName: formValues[INPUT_IDS.SEGMENT_NAME],
+ internalID: formValues[INPUT_IDS.INTERNAL_ID],
+ scriptID: formValues[INPUT_IDS.SCRIPT_ID],
+ mapping: formValues[INPUT_IDS.MAPPING] ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG,
+ },
+ ]);
+ Connections.updateNetSuiteCustomSegments(policyID, updatedCustomSegments, customSegments);
+ nextScreen();
+ },
+ [customSegments, nextScreen, policyID],
+ );
+
+ const renderSubStepContent = useMemo(
+ () => (
+
+ ),
+ [SubStep, handleNextScreen, isEditing, moveTo, policy, policyID, screenIndex, customSegmentType],
+ );
+
+ const selectionListForm = [CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.MAPPING as number].includes(screenIndex);
+ const submitFlexAllowed = [
+ CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_NAME as number,
+ CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.INTERNAL_ID as number,
+ CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SCRIPT_ID as number,
+ ].includes(screenIndex);
+
+ return (
+
+
+
+
+
+ {screenIndex === CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_TYPE ? (
+ renderSubStepContent
+ ) : (
+
+ {renderSubStepContent}
+
+ )}
+
+
+ );
+}
+
+NetSuiteImportAddCustomSegmentPage.displayName = 'NetSuiteImportAddCustomSegmentPage';
+
+export default withPolicyConnections(NetSuiteImportAddCustomSegmentPage);
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm.tsx
new file mode 100644
index 000000000000..684059a5b846
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import type {MenuItemProps} from '@components/MenuItem';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+
+type NetSuiteMenuWithTopDescriptionFormProps = MenuItemProps & {
+ /** The value of the menu item */
+ value?: string;
+
+ /** Callback to format the value */
+ valueRenderer?: (value?: string) => string | undefined;
+};
+
+function NetSuiteMenuWithTopDescriptionForm({value, valueRenderer, ...props}: NetSuiteMenuWithTopDescriptionFormProps) {
+ return (
+
+ );
+}
+
+NetSuiteMenuWithTopDescriptionForm.displayName = 'NetSuiteMenuWithTopDescriptionForm';
+export default NetSuiteMenuWithTopDescriptionForm;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseCustomListStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseCustomListStep.tsx
new file mode 100644
index 000000000000..473a01d5e7ce
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseCustomListStep.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import InputWrapper from '@components/Form/InputWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function ChooseCustomListStep({policy}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ return (
+ <>
+ {translate(`workspace.netsuite.import.importCustomFields.customLists.addForm.listNameTitle`)}
+
+ >
+ );
+}
+
+ChooseCustomListStep.displayName = 'ChooseCustomListStep';
+export default ChooseCustomListStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseSegmentTypeStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseSegmentTypeStep.tsx
new file mode 100644
index 000000000000..93b0ed183b18
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseSegmentTypeStep.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+
+function ChooseSegmentTypeStep({onNext, customSegmentType, setCustomSegmentType}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const selectionData = [
+ {
+ text: translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.segmentTitle`),
+ keyForList: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
+ isSelected: customSegmentType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
+ value: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
+ },
+ {
+ text: translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.recordTitle`),
+ keyForList: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
+ isSelected: customSegmentType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
+ value: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
+ },
+ ];
+
+ return (
+ <>
+
+ {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.segmentRecordType`)}
+
+ {translate(`workspace.netsuite.import.importCustomFields.chooseOptionBelow`)}
+ {
+ setCustomSegmentType?.(selected.value);
+ onNext();
+ }}
+ />
+ >
+ );
+}
+
+ChooseSegmentTypeStep.displayName = 'ChooseSegmentTypeStep';
+export default ChooseSegmentTypeStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomListStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomListStep.tsx
new file mode 100644
index 000000000000..e72aa8710753
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomListStep.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import {View} from 'react-native';
+import InputWrapper from '@components/Form/InputWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import type {TranslationPaths} from '@src/languages/types';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function ConfirmCustomListStep({onMove}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const fieldNames = [INPUT_IDS.LIST_NAME, INPUT_IDS.TRANSACTION_FIELD_ID, INPUT_IDS.MAPPING];
+
+ return (
+
+ {translate('workspace.common.letsDoubleCheck')}
+ {fieldNames.map((fieldName, index) => (
+ {
+ onMove(index);
+ }}
+ valueRenderer={(value) => (fieldName === INPUT_IDS.MAPPING && value ? translate(`workspace.netsuite.import.importTypes.${value}.label` as TranslationPaths) : value)}
+ />
+ ))}
+
+ );
+}
+
+ConfirmCustomListStep.displayName = 'ConfirmCustomListStep';
+export default ConfirmCustomListStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomSegmentList.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomSegmentList.tsx
new file mode 100644
index 000000000000..bf1314491c03
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomSegmentList.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import {View} from 'react-native';
+import InputWrapper from '@components/Form/InputWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function ConfirmCustomListStep({onMove, customSegmentType}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const fieldNames = [INPUT_IDS.SEGMENT_NAME, INPUT_IDS.INTERNAL_ID, INPUT_IDS.SCRIPT_ID, INPUT_IDS.MAPPING];
+
+ return (
+
+ {translate('workspace.common.letsDoubleCheck')}
+ {fieldNames.map((fieldName, index) => (
+ {
+ onMove(index + 1);
+ }}
+ valueRenderer={(value) => (fieldName === INPUT_IDS.MAPPING && value ? translate(`workspace.netsuite.import.importTypes.${value}.label` as TranslationPaths) : value)}
+ />
+ ))}
+
+ );
+}
+
+ConfirmCustomListStep.displayName = 'ConfirmCustomListStep';
+export default ConfirmCustomListStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentInternalIdStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentInternalIdStep.tsx
new file mode 100644
index 000000000000..366936138d42
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentInternalIdStep.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import {View} from 'react-native';
+import InputWrapper from '@components/Form/InputWrapper';
+import RenderHTML from '@components/RenderHTML';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Parser from '@libs/Parser';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function CustomSegmentInternalIdStep({customSegmentType}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
+
+ const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.internalID`);
+
+ return (
+
+
+ {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.customSegmentInternalIDTitle`)}
+
+
+
+ ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}InternalIDFooter`))}`}
+ />
+
+
+ );
+}
+
+CustomSegmentInternalIdStep.displayName = 'CustomSegmentInternalIdStep';
+export default CustomSegmentInternalIdStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentNameStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentNameStep.tsx
new file mode 100644
index 000000000000..52021ceedc67
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentNameStep.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import {View} from 'react-native';
+import InputWrapper from '@components/Form/InputWrapper';
+import RenderHTML from '@components/RenderHTML';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Parser from '@libs/Parser';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function CustomSegmentNameStep({customSegmentType}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.segmentName`);
+
+ const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
+
+ return (
+
+
+ {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}NameTitle`)}
+
+
+
+ ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}NameFooter`))}`}
+ />
+
+
+ );
+}
+
+CustomSegmentNameStep.displayName = 'CustomSegmentNameStep';
+export default CustomSegmentNameStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentScriptIdStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentScriptIdStep.tsx
new file mode 100644
index 000000000000..20327db7776f
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentScriptIdStep.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import {View} from 'react-native';
+import InputWrapper from '@components/Form/InputWrapper';
+import RenderHTML from '@components/RenderHTML';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Parser from '@libs/Parser';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function CustomSegmentScriptIdStep({customSegmentType}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
+
+ const fieldLabel = translate(
+ `workspace.netsuite.import.importCustomFields.customSegments.fields.${
+ customSegmentRecordType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT ? 'scriptID' : 'customRecordScriptID'
+ }`,
+ );
+
+ return (
+
+
+ {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}ScriptIDTitle`)}
+
+
+
+ ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}ScriptIDFooter`))}`}
+ />
+
+
+ );
+}
+
+CustomSegmentScriptIdStep.displayName = 'CustomSegmentScriptIdStep';
+export default CustomSegmentScriptIdStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/MappingStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/MappingStep.tsx
new file mode 100644
index 000000000000..f41269e4954f
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/MappingStep.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import InputWrapper from '@components/Form/InputWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function MappingStep({importCustomField, customSegmentType}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ let titleKey;
+ if (importCustomField === CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS) {
+ titleKey = 'workspace.netsuite.import.importCustomFields.customLists.addForm.mappingTitle';
+ } else {
+ const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
+ titleKey = `workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}MappingTitle`;
+ }
+
+ return (
+ <>
+ {translate(titleKey as TranslationPaths)}
+ {translate(`workspace.netsuite.import.importCustomFields.chooseOptionBelow`)}
+
+ >
+ );
+}
+
+MappingStep.displayName = 'MappingStep';
+export default MappingStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/TransactionFieldIDStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/TransactionFieldIDStep.tsx
new file mode 100644
index 000000000000..a7f7132143f1
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/TransactionFieldIDStep.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import {View} from 'react-native';
+import InputWrapper from '@components/Form/InputWrapper';
+import RenderHTML from '@components/RenderHTML';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Parser from '@libs/Parser';
+import CONST from '@src/CONST';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function TransactionFieldIDStep() {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customLists.fields.transactionFieldID`);
+
+ return (
+
+ {translate(`workspace.netsuite.import.importCustomFields.customLists.addForm.transactionFieldIDTitle`)}
+
+
+ ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customLists.addForm.transactionFieldIDFooter`))}`} />
+
+
+ );
+}
+
+TransactionFieldIDStep.displayName = 'TransactionFieldIDStep';
+export default TransactionFieldIDStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldPage.tsx
new file mode 100644
index 000000000000..f438a13e134a
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldPage.tsx
@@ -0,0 +1,164 @@
+import React, {useMemo} from 'react';
+import type {StyleProp, TextStyle} from 'react-native';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import Button from '@components/Button';
+import ConnectionLayout from '@components/ConnectionLayout';
+import FixedFooter from '@components/FixedFooter';
+import * as Illustrations from '@components/Icon/Illustrations';
+import type {LocaleContextProps} from '@components/LocaleContextProvider';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
+import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import type {ThemeStyles} from '@styles/index';
+import * as Policy from '@userActions/Policy/Policy';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+type ImportCustomFieldsKeys = ValueOf;
+
+type NetSuiteImportCustomFieldPageProps = WithPolicyConnectionsProps & {
+ route: {
+ params: {
+ /** Whether the record is of type custom segment or list */
+ importCustomField: ImportCustomFieldsKeys;
+ };
+ };
+};
+
+type HelpLinkComponentProps = {
+ /** Whether the record is of type custom segment or list */
+ importCustomField: ImportCustomFieldsKeys;
+
+ /** Callback to localize content */
+ translate: LocaleContextProps['translate'];
+
+ /** Theme styles to apply to the component */
+ styles: ThemeStyles;
+
+ /** Text alignment style for the Text component */
+ alignmentStyle: StyleProp;
+};
+
+function HelpLinkComponent({importCustomField, styles, translate, alignmentStyle}: HelpLinkComponentProps) {
+ return (
+
+
+ {translate(`workspace.netsuite.import.importCustomFields.${importCustomField}.helpLinkText`)}
+
+ {translate(`workspace.netsuite.import.importCustomFields.${importCustomField}.helpText`)}
+
+ );
+}
+
+function NetSuiteImportCustomFieldPage({
+ policy,
+ route: {
+ params: {importCustomField},
+ },
+}: NetSuiteImportCustomFieldPageProps) {
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const config = policy?.connections?.netsuite?.options?.config;
+ const data = config?.syncOptions?.[importCustomField] ?? [];
+
+ const listEmptyComponent = useMemo(
+ () => (
+
+ }
+ containerStyle={[styles.flex1, styles.justifyContentCenter]}
+ />
+ ),
+ [importCustomField, styles, translate],
+ );
+
+ const listHeaderComponent = useMemo(
+ () => (
+
+
+
+ ),
+ [styles, importCustomField, translate],
+ );
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT.getRoute(policyID))}
+ >
+ {data.length === 0 ? listEmptyComponent : listHeaderComponent}
+ Policy.clearNetSuiteErrorField(policyID, importCustomField)}
+ >
+ {data.map((record, index) => (
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_VIEW.getRoute(policyID, importCustomField, index))}
+ />
+ ))}
+
+
+
+
+
+ );
+}
+
+NetSuiteImportCustomFieldPage.displayName = 'NetSuiteImportCustomFieldPage';
+export default withPolicyConnections(NetSuiteImportCustomFieldPage);
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView.tsx
new file mode 100644
index 000000000000..674d9fb39078
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView.tsx
@@ -0,0 +1,137 @@
+import React, {useCallback, useMemo, useState} from 'react';
+import type {ValueOf} from 'type-fest';
+import ConfirmModal from '@components/ConfirmModal';
+import ConnectionLayout from '@components/ConnectionLayout';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {updateNetSuiteCustomLists, updateNetSuiteCustomSegments} from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import * as Policy from '@userActions/Policy/Policy';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+import type {NetSuiteCustomList, NetSuiteCustomSegment} from '@src/types/onyx/Policy';
+
+type CustomField = NetSuiteCustomList | NetSuiteCustomSegment;
+type ImportCustomFieldsKeys = ValueOf;
+
+type NetSuiteImportCustomFieldViewProps = WithPolicyConnectionsProps & {
+ route: {
+ params: {
+ /** Whether the record is of type custom segment or list */
+ importCustomField: ImportCustomFieldsKeys;
+
+ /** Index of the current record */
+ valueIndex: number;
+ };
+ };
+};
+
+function NetSuiteImportCustomFieldView({
+ policy,
+ route: {
+ params: {importCustomField, valueIndex},
+ },
+}: NetSuiteImportCustomFieldViewProps) {
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false);
+
+ const config = policy?.connections?.netsuite?.options?.config;
+ const allRecords = useMemo(() => config?.syncOptions?.[importCustomField] ?? [], [config?.syncOptions, importCustomField]);
+
+ const customField: CustomField | undefined = allRecords[valueIndex];
+ const fieldList =
+ customField && PolicyUtils.isNetSuiteCustomSegmentRecord(customField)
+ ? CONST.NETSUITE_CONFIG.CUSTOM_SEGMENT_FIELDS
+ : [INPUT_IDS.LIST_NAME, INPUT_IDS.TRANSACTION_FIELD_ID, INPUT_IDS.MAPPING];
+
+ const removeRecord = useCallback(() => {
+ if (customField) {
+ // We allow multiple custom list records with the same internalID. Hence it is safe to remove by index.
+ const filteredRecords = allRecords.filter((_, index) => index !== Number(valueIndex));
+
+ if (PolicyUtils.isNetSuiteCustomSegmentRecord(customField)) {
+ updateNetSuiteCustomSegments(policyID, filteredRecords as NetSuiteCustomSegment[], allRecords as NetSuiteCustomSegment[]);
+ } else {
+ updateNetSuiteCustomLists(policyID, filteredRecords as NetSuiteCustomList[], allRecords as NetSuiteCustomList[]);
+ }
+ }
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, importCustomField));
+ }, [allRecords, customField, importCustomField, policyID, valueIndex]);
+
+ return (
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, importCustomField))}
+ >
+ {customField && (
+ Policy.clearNetSuiteErrorField(policyID, importCustomField)}
+ >
+ {fieldList.map((fieldName) => {
+ const isEditable = !config?.syncOptions?.pendingFields?.[importCustomField] && PolicyUtils.isNetSuiteCustomFieldPropertyEditable(customField, fieldName);
+ return (
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_EDIT.getRoute(policyID, importCustomField, valueIndex, fieldName))
+ : undefined
+ }
+ />
+ );
+ })}
+
+ )}
+
+ setIsRemoveModalOpen(false)}
+ prompt={translate(`workspace.netsuite.import.importCustomFields.${importCustomField}.removePrompt`)}
+ confirmText={translate('common.remove')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
+
+ );
+}
+
+NetSuiteImportCustomFieldView.displayName = 'NetSuiteImportCustomFieldView';
+export default withPolicyConnections(NetSuiteImportCustomFieldView);
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx
index ff633158af3b..c13b8cd7fa7c 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx
@@ -22,6 +22,7 @@ type ImportFieldsKeys = TupleToUnion
type NetSuiteImportMappingPageProps = WithPolicyConnectionsProps & {
route: {
params: {
+ /** Whether the record is custom segment or custom list */
importField: ImportFieldsKeys;
};
};
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx
index 17256defc0a8..4b2dc4809a84 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx
@@ -111,7 +111,7 @@ function NetSuiteImportPage({policy}: WithPolicyConnectionsProps) {
/>
)}
- {CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.map((importField) => (
+ {Object.values(CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS).map((importField) => (
Policy.clearNetSuiteErrorField(policyID, importField)}
>
{
- // TODO: Navigation will be handled in future PRs
- }}
+ onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, importField))}
brickRoadIndicator={config?.errorFields?.[importField] ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
diff --git a/src/pages/workspace/accounting/netsuite/types.ts b/src/pages/workspace/accounting/netsuite/types.ts
index 3d01d3a25efd..fa092c856585 100644
--- a/src/pages/workspace/accounting/netsuite/types.ts
+++ b/src/pages/workspace/accounting/netsuite/types.ts
@@ -1,8 +1,11 @@
import type {ValueOf} from 'type-fest';
import type {MenuItemProps} from '@components/MenuItem';
import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback';
+import type {SelectorType} from '@components/SelectionScreen';
+import type {SubStepProps} from '@hooks/useSubStep/types';
import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/ToggleSettingsOptionRow';
import type CONST from '@src/CONST';
+import type {Policy} from '@src/types/onyx';
type MenuItem = MenuItemProps & {
/** Type of the item */
@@ -44,4 +47,26 @@ type ExpenseRouteParams = {
expenseType: ValueOf;
};
-export type {MenuItem, DividerLineItem, ToggleItem, ExpenseRouteParams};
+type CustomFieldSubStepWithPolicy = SubStepProps & {
+ /** Policy ID of the current policy */
+ policyID: string;
+
+ /** Currenct policy in the form steps */
+ policy: Policy | undefined;
+
+ /** Whether the page is a custom segment or custom list */
+ importCustomField: ValueOf;
+
+ /** Whether the record is custom segment or custom record */
+ customSegmentType?: ValueOf;
+
+ /** Callback to update the current segment type of the record */
+ setCustomSegmentType?: (segmentType: ValueOf) => void;
+};
+
+type CustomListSelectorType = SelectorType & {
+ /** ID of the list item */
+ id: string;
+};
+
+export type {MenuItem, DividerLineItem, ToggleItem, ExpenseRouteParams, CustomFieldSubStepWithPolicy, CustomListSelectorType};
diff --git a/src/pages/workspace/card/issueNew/AssigneeStep.tsx b/src/pages/workspace/card/issueNew/AssigneeStep.tsx
index 5012ba294518..23acb3d4a24a 100644
--- a/src/pages/workspace/card/issueNew/AssigneeStep.tsx
+++ b/src/pages/workspace/card/issueNew/AssigneeStep.tsx
@@ -1,30 +1,124 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import {View} from 'react-native';
-import FormProvider from '@components/Form/FormProvider';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import type {ListItem} from '@components/SelectionList/types';
+import UserListItem from '@components/SelectionList/UserListItem';
import Text from '@components/Text';
+import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
+import {formatPhoneNumber} from '@libs/LocalePhoneNumber';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import * as Card from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type * as OnyxTypes from '@src/types/onyx';
-function AssigneeStep() {
+const MINIMUM_MEMBER_TO_SHOW_SEARCH = 8;
+
+type AssigneeStepProps = {
+ // The policy that the card will be issued under
+ policy: OnyxEntry;
+};
+
+function AssigneeStep({policy}: AssigneeStepProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
+ const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD);
+
+ const isEditing = issueNewCard?.isEditing;
+
+ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
- const submit = () => {
- // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.CARD_TYPE);
+ const submit = (assignee: ListItem) => {
+ Card.setIssueNewCardStepAndData({
+ step: isEditing ? CONST.EXPENSIFY_CARD.STEP.CONFIRMATION : CONST.EXPENSIFY_CARD.STEP.CARD_TYPE,
+ data: {
+ assigneeEmail: assignee?.login ?? '',
+ },
+ isEditing: false,
+ });
};
const handleBackButtonPress = () => {
+ if (isEditing) {
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false});
+ return;
+ }
Navigation.goBack();
+ Card.clearIssueNewCardFlow();
};
+ const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH;
+ const textInputLabel = shouldShowSearchInput ? translate('workspace.card.issueNewCard.findMember') : undefined;
+
+ const membersDetails = useMemo(() => {
+ let membersList: ListItem[] = [];
+ if (!policy?.employeeList) {
+ return membersList;
+ }
+
+ Object.entries(policy.employeeList ?? {}).forEach(([email, policyEmployee]) => {
+ if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) {
+ return;
+ }
+
+ const personalDetail = PersonalDetailsUtils.getPersonalDetailByEmail(email);
+ membersList.push({
+ keyForList: email,
+ text: personalDetail?.displayName,
+ alternateText: email,
+ login: email,
+ accountID: personalDetail?.accountID,
+ icons: [
+ {
+ source: personalDetail?.avatar ?? Expensicons.FallbackAvatar,
+ name: formatPhoneNumber(email),
+ type: CONST.ICON_TYPE_AVATAR,
+ id: personalDetail?.accountID,
+ },
+ ],
+ });
+ });
+
+ membersList = OptionsListUtils.sortItemsAlphabetically(membersList);
+
+ return membersList;
+ }, [isOffline, policy?.employeeList]);
+
+ const sections = useMemo(() => {
+ if (!debouncedSearchTerm) {
+ return [
+ {
+ data: membersDetails,
+ shouldShow: true,
+ },
+ ];
+ }
+
+ const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase();
+ const filteredOptions = membersDetails.filter((option) => !!option.text?.toLowerCase().includes(searchValue) || !!option.alternateText?.toLowerCase().includes(searchValue));
+
+ return [
+ {
+ title: undefined,
+ data: filteredOptions,
+ shouldShow: true,
+ },
+ ];
+ }, [membersDetails, debouncedSearchTerm]);
+
return (
{translate('workspace.card.issueNewCard.whoNeedsCard')}
-
- {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */}
-
-
+
);
}
diff --git a/src/pages/workspace/card/issueNew/CardNameStep.tsx b/src/pages/workspace/card/issueNew/CardNameStep.tsx
index 9b48d6417732..58b0748e438a 100644
--- a/src/pages/workspace/card/issueNew/CardNameStep.tsx
+++ b/src/pages/workspace/card/issueNew/CardNameStep.tsx
@@ -1,28 +1,59 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as ValidationUtils from '@libs/ValidationUtils';
import * as Card from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/IssueNewExpensifyCardForm';
function CardNameStep() {
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const {inputCallbackRef} = useAutoFocusInput();
+ const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD);
- const submit = () => {
- // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.CONFIRMATION);
- };
+ const isEditing = issueNewCard?.isEditing;
- const handleBackButtonPress = () => {
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.LIMIT);
- };
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.CARD_TITLE]);
+ if (!values.cardTitle) {
+ errors.cardTitle = translate('common.error.fieldRequired');
+ }
+ return errors;
+ },
+ [translate],
+ );
+
+ const submit = useCallback((values: FormOnyxValues) => {
+ Card.setIssueNewCardStepAndData({
+ step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION,
+ data: {
+ cardTitle: values.cardTitle,
+ },
+ isEditing: false,
+ });
+ }, []);
+
+ const handleBackButtonPress = useCallback(() => {
+ if (isEditing) {
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false});
+ return;
+ }
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.LIMIT});
+ }, [isEditing]);
return (
{translate('workspace.card.issueNewCard.giveItName')}
- {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */}
-
+
);
diff --git a/src/pages/workspace/card/issueNew/CardTypeStep.tsx b/src/pages/workspace/card/issueNew/CardTypeStep.tsx
index 93b99f51d239..31b5585b91ad 100644
--- a/src/pages/workspace/card/issueNew/CardTypeStep.tsx
+++ b/src/pages/workspace/card/issueNew/CardTypeStep.tsx
@@ -1,12 +1,16 @@
import React from 'react';
import {View} from 'react-native';
-import FormProvider from '@components/Form/FormProvider';
+import {useOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Illustrations from '@components/Icon/Illustrations';
import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
+import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
import * as Card from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -14,14 +18,26 @@ import ONYXKEYS from '@src/ONYXKEYS';
function CardTypeStep() {
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD);
- const submit = () => {
- // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE);
+ const isEditing = issueNewCard?.isEditing;
+
+ const submit = (value: ValueOf) => {
+ Card.setIssueNewCardStepAndData({
+ step: isEditing ? CONST.EXPENSIFY_CARD.STEP.CONFIRMATION : CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE,
+ data: {
+ cardType: value,
+ },
+ isEditing: false,
+ });
};
const handleBackButtonPress = () => {
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.ASSIGNEE);
+ if (isEditing) {
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false});
+ return;
+ }
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.ASSIGNEE});
};
return (
@@ -42,15 +58,32 @@ function CardTypeStep() {
/>
{translate('workspace.card.issueNewCard.chooseCardType')}
-
- {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */}
-
-
+
+
);
}
diff --git a/src/pages/workspace/card/issueNew/ConfirmationStep.tsx b/src/pages/workspace/card/issueNew/ConfirmationStep.tsx
index a64d6f463531..35f9fab6598f 100644
--- a/src/pages/workspace/card/issueNew/ConfirmationStep.tsx
+++ b/src/pages/workspace/card/issueNew/ConfirmationStep.tsx
@@ -1,32 +1,62 @@
import React from 'react';
import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as CurrencyUtils from '@libs/CurrencyUtils';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import Navigation from '@navigation/Navigation';
import * as Card from '@userActions/Card';
import CONST from '@src/CONST';
-import ROUTES from '@src/ROUTES';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {IssueNewCardStep} from '@src/types/onyx/Card';
+
+function getTranslationKeyForLimitType(limitType: string | undefined) {
+ switch (limitType) {
+ case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART:
+ return 'workspace.card.issueNewCard.smartLimit';
+ case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED:
+ return 'workspace.card.issueNewCard.fixedAmount';
+ case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY:
+ return 'workspace.card.issueNewCard.monthly';
+ default:
+ return '';
+ }
+}
function ConfirmationStep() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
+ const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD);
+
+ const data = issueNewCard?.data;
+
const submit = () => {
- // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309
- Navigation.navigate(ROUTES.SETTINGS);
+ // TODO: the logic will be created when CreateExpensifyCard is ready
+ Navigation.goBack();
+ Card.clearIssueNewCardFlow();
+ };
+
+ const editStep = (step: IssueNewCardStep) => {
+ Card.setIssueNewCardStepAndData({step, isEditing: true});
};
const handleBackButtonPress = () => {
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.CARD_NAME);
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CARD_NAME});
};
+ const translationForLimitType = getTranslationKeyForLimitType(data?.limitType);
+
return (
- {translate('workspace.card.issueNewCard.letsDoubleCheck')}
-
- {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */}
-
+ editStep(CONST.EXPENSIFY_CARD.STEP.CARD_TYPE)}
+ />
+ editStep(CONST.EXPENSIFY_CARD.STEP.LIMIT)}
+ />
+ editStep(CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE)}
+ />
+ editStep(CONST.EXPENSIFY_CARD.STEP.CARD_NAME)}
+ />
+
+
+
+
);
}
diff --git a/src/pages/workspace/card/issueNew/IssueNewCardPage.tsx b/src/pages/workspace/card/issueNew/IssueNewCardPage.tsx
index d63bbd56b4d0..e12835a4a1e0 100644
--- a/src/pages/workspace/card/issueNew/IssueNewCardPage.tsx
+++ b/src/pages/workspace/card/issueNew/IssueNewCardPage.tsx
@@ -1,5 +1,7 @@
import React from 'react';
import {useOnyx} from 'react-native-onyx';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import AssigneeStep from './AssigneeStep';
@@ -9,18 +11,21 @@ import ConfirmationStep from './ConfirmationStep';
import LimitStep from './LimitStep';
import LimitTypeStep from './LimitTypeStep';
-function IssueNewCardPage() {
+function IssueNewCardPage({policy}: WithPolicyAndFullscreenLoadingProps) {
const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD);
const {currentStep} = issueNewCard ?? {};
+ // TODO: add logic to skip Assignee step when the flow is started from the member's profile page
+ // TODO: StartIssueNewCardFlow call to API
+
switch (currentStep) {
case CONST.EXPENSIFY_CARD.STEP.ASSIGNEE:
- return ;
+ return ;
case CONST.EXPENSIFY_CARD.STEP.CARD_TYPE:
return ;
case CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE:
- return ;
+ return ;
case CONST.EXPENSIFY_CARD.STEP.LIMIT:
return ;
case CONST.EXPENSIFY_CARD.STEP.CARD_NAME:
@@ -28,9 +33,9 @@ function IssueNewCardPage() {
case CONST.EXPENSIFY_CARD.STEP.CONFIRMATION:
return ;
default:
- return ;
+ return ;
}
}
IssueNewCardPage.displayName = 'IssueNewCardPage';
-export default IssueNewCardPage;
+export default withPolicyAndFullscreenLoading(IssueNewCardPage);
diff --git a/src/pages/workspace/card/issueNew/LimitStep.tsx b/src/pages/workspace/card/issueNew/LimitStep.tsx
index dd2e80a6612a..cc65a987bd62 100644
--- a/src/pages/workspace/card/issueNew/LimitStep.tsx
+++ b/src/pages/workspace/card/issueNew/LimitStep.tsx
@@ -1,28 +1,63 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import AmountForm from '@components/AmountForm';
import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as CurrencyUtils from '@libs/CurrencyUtils';
+import * as ValidationUtils from '@libs/ValidationUtils';
import * as Card from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/IssueNewExpensifyCardForm';
function LimitStep() {
const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
const styles = useThemeStyles();
+ const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD);
+ const isEditing = issueNewCard?.isEditing;
- const submit = () => {
- // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.CARD_NAME);
- };
+ const submit = useCallback(
+ (values: FormOnyxValues) => {
+ const limit = CurrencyUtils.convertToBackendAmount(Number(values?.limit));
+ Card.setIssueNewCardStepAndData({
+ step: isEditing ? CONST.EXPENSIFY_CARD.STEP.CONFIRMATION : CONST.EXPENSIFY_CARD.STEP.CARD_NAME,
+ data: {limit},
+ isEditing: false,
+ });
+ },
+ [isEditing],
+ );
+
+ const handleBackButtonPress = useCallback(() => {
+ if (isEditing) {
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false});
+ return;
+ }
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE});
+ }, [isEditing]);
- const handleBackButtonPress = () => {
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE);
- };
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.LIMIT]);
+
+ // We only want integers to be sent as the limit
+ if (!Number(values.limit) || !Number.isInteger(Number(values.limit))) {
+ errors.limit = translate('iou.error.invalidAmount');
+ }
+ return errors;
+ },
+ [translate],
+ );
return (
{translate('workspace.card.issueNewCard.setLimit')}
- {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */}
-
+
);
diff --git a/src/pages/workspace/card/issueNew/LimitTypeStep.tsx b/src/pages/workspace/card/issueNew/LimitTypeStep.tsx
index b1249e33e3c4..79b9c40ef4ae 100644
--- a/src/pages/workspace/card/issueNew/LimitTypeStep.tsx
+++ b/src/pages/workspace/card/issueNew/LimitTypeStep.tsx
@@ -1,28 +1,87 @@
-import React from 'react';
+import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
-import FormProvider from '@components/Form/FormProvider';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Card from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type * as OnyxTypes from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-function LimitTypeStep() {
+type LimitTypeStepProps = {
+ // The policy that the card will be issued under
+ policy: OnyxEntry;
+};
+
+function LimitTypeStep({policy}: LimitTypeStepProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD);
+
+ const areApprovalsConfigured = !isEmptyObject(policy?.approver) && policy?.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL;
+ const defaultType = areApprovalsConfigured ? CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART : CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY;
+
+ const [typeSelected, setTypeSelected] = useState(issueNewCard?.data?.limitType ?? defaultType);
+
+ const isEditing = issueNewCard?.isEditing;
+
+ const submit = useCallback(() => {
+ Card.setIssueNewCardStepAndData({
+ step: isEditing ? CONST.EXPENSIFY_CARD.STEP.CONFIRMATION : CONST.EXPENSIFY_CARD.STEP.LIMIT,
+ data: {limitType: typeSelected},
+ isEditing: false,
+ });
+ }, [isEditing, typeSelected]);
- const submit = () => {
- // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.LIMIT);
- };
+ const handleBackButtonPress = useCallback(() => {
+ if (isEditing) {
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false});
+ return;
+ }
+ Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CARD_TYPE});
+ }, [isEditing]);
- const handleBackButtonPress = () => {
- Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.CARD_TYPE);
- };
+ const data = useMemo(() => {
+ const options = [];
+
+ if (areApprovalsConfigured) {
+ options.push({
+ value: CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART,
+ text: translate('workspace.card.issueNewCard.smartLimit'),
+ alternateText: translate('workspace.card.issueNewCard.smartLimitDescription'),
+ keyForList: CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART,
+ isSelected: typeSelected === CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART,
+ });
+ }
+
+ options.push(
+ {
+ value: CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY,
+ text: translate('workspace.card.issueNewCard.monthly'),
+ alternateText: translate('workspace.card.issueNewCard.monthlyDescription'),
+ keyForList: CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY,
+ isSelected: typeSelected === CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY,
+ },
+ {
+ value: CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED,
+ text: translate('workspace.card.issueNewCard.fixedAmount'),
+ alternateText: translate('workspace.card.issueNewCard.fixedAmountDescription'),
+ keyForList: CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED,
+ isSelected: typeSelected === CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED,
+ },
+ );
+
+ return options;
+ }, [areApprovalsConfigured, translate, typeSelected]);
return (
{translate('workspace.card.issueNewCard.chooseLimitType')}
-
- {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */}
-
-
+ setTypeSelected(value)}
+ sections={[{data}]}
+ shouldDebounceRowSelect
+ />
+
);
}
diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx
index 46b073b2bd48..91a5f3b8f3f1 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx
@@ -21,6 +21,7 @@ import Navigation from '@navigation/Navigation';
import type {FullScreenNavigatorParamList} from '@navigation/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {Card, WorkspaceCardsList} from '@src/types/onyx';
import WorkspaceCardListHeader from './WorkspaceCardListHeader';
@@ -100,7 +101,7 @@ function WorkspaceExpensifyCardListPage({route}: WorkspaceExpensifyCardListPageP
{isSmallScreenWidth && {getHeaderButtons()}}
- {(!isSmallScreenWidth || reportFieldsList.length === 0 || isLoading) && getHeaderText()}
+ setDeleteReportFieldsConfirmModalVisible(false)}
+ title={translate(selectedReportFields.length === 1 ? 'workspace.reportFields.delete' : 'workspace.reportFields.deleteFields')}
+ prompt={translate(selectedReportFields.length === 1 ? 'workspace.reportFields.deleteConfirmation' : 'workspace.reportFields.deleteFieldsConfirmation')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
+ {(!isSmallScreenWidth || reportFieldsSections[0].data.length === 0 || isLoading) && getHeaderText()}
{isLoading && (
{}}
+ onSelectAll={toggleAllReportFields}
ListItem={TableListItem}
customListHeader={getCustomListHeader()}
listHeaderContent={isSmallScreenWidth ? getHeaderText() : null}
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
index 62c0bd104bcb..92b016766742 100644
--- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -34,8 +34,9 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
+import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
-import type {TagListItem} from './types';
+import type {PolicyTag, PolicyTagList, TagListItem} from './types';
type WorkspaceTagsPageProps = StackScreenProps;
@@ -71,6 +72,15 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
setSelectedTags({});
}, [isFocused]);
+ const getPendingAction = (policyTagList: PolicyTagList): PendingAction | undefined => {
+ if (!policyTagList) {
+ return undefined;
+ }
+ return (policyTagList.pendingAction as PendingAction) ?? Object.values(policyTagList.tags).some((tag: PolicyTag) => tag.pendingAction)
+ ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE
+ : undefined;
+ };
+
const tagList = useMemo(() => {
if (isMultiLevelTags) {
return policyTagLists.map((policyTagList) => ({
@@ -79,6 +89,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
text: PolicyUtils.getCleanedTagName(policyTagList.name),
keyForList: String(policyTagList.orderWeight),
isSelected: selectedTags[policyTagList.name],
+ pendingAction: getPendingAction(policyTagList),
enabled: true,
required: policyTagList.required,
rightElement: (
diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx
index 5aec0b7c3ca0..9ac1fc7583ae 100644
--- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx
@@ -252,7 +252,9 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
)}
Tag.clearPolicyTagListErrors(policyID, currentPolicyTag.orderWeight)}
pendingAction={currentPolicyTag.pendingAction}
+ errorRowStyles={styles.mh5}
>
;
+
+type PolicyTagList = {
+ name: string;
+ orderWeight: number;
+ required: boolean;
+ tags: PolicyTags;
+ pendingAction?: PendingAction | null;
+};
+
// eslint-disable-next-line import/prefer-default-export
-export type {TagListItem};
+export type {TagListItem, PolicyTag, PolicyTagList};
diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx
index 14b070d3d781..5dea1664589d 100644
--- a/src/pages/workspace/withPolicy.tsx
+++ b/src/pages/workspace/withPolicy.tsx
@@ -39,6 +39,8 @@ type PolicyRoute = RouteProp<
| typeof SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT
| typeof SCREENS.WORKSPACE.REPORT_FIELDS_CREATE
| typeof SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES
+ | typeof SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE
+ | typeof SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS
>;
function getPolicyIDFromRoute(route: PolicyRoute): string {
diff --git a/src/types/form/IssueNewExpensifyCardForm.ts b/src/types/form/IssueNewExpensifyCardForm.ts
index 06ff2c421968..9d662a6f47b3 100644
--- a/src/types/form/IssueNewExpensifyCardForm.ts
+++ b/src/types/form/IssueNewExpensifyCardForm.ts
@@ -2,7 +2,8 @@ import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
- CARD_NAME: 'cardName',
+ CARD_TITLE: 'cardTitle',
+ LIMIT: 'limit',
} as const;
type InputID = ValueOf;
@@ -10,7 +11,8 @@ type InputID = ValueOf;
type IssueNewExpensifyCardForm = Form<
InputID,
{
- [INPUT_IDS.CARD_NAME]: string;
+ [INPUT_IDS.CARD_TITLE]: string;
+ [INPUT_IDS.LIMIT]: string;
}
>;
diff --git a/src/types/form/NetSuiteCustomFieldForm.ts b/src/types/form/NetSuiteCustomFieldForm.ts
new file mode 100644
index 000000000000..efbf844bd89f
--- /dev/null
+++ b/src/types/form/NetSuiteCustomFieldForm.ts
@@ -0,0 +1,29 @@
+import type {ValueOf} from 'type-fest';
+import type {NetSuiteCustomFieldMapping} from '@src/types/onyx/Policy';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ INTERNAL_ID: 'internalID',
+ MAPPING: 'mapping',
+ LIST_NAME: 'listName',
+ SEGMENT_NAME: 'segmentName',
+ TRANSACTION_FIELD_ID: 'transactionFieldID',
+ SCRIPT_ID: 'scriptID',
+} as const;
+
+type InputID = ValueOf;
+
+type NetSuiteCustomFieldForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.INTERNAL_ID]: string;
+ [INPUT_IDS.MAPPING]: NetSuiteCustomFieldMapping;
+ [INPUT_IDS.LIST_NAME]: string;
+ [INPUT_IDS.SEGMENT_NAME]: string;
+ [INPUT_IDS.TRANSACTION_FIELD_ID]: string;
+ [INPUT_IDS.SCRIPT_ID]: string;
+ }
+>;
+
+export type {NetSuiteCustomFieldForm};
+export default INPUT_IDS;
diff --git a/src/types/form/SageIntacctDimensionsForm.ts b/src/types/form/SageIntacctDimensionsForm.ts
new file mode 100644
index 000000000000..b2b074801190
--- /dev/null
+++ b/src/types/form/SageIntacctDimensionsForm.ts
@@ -0,0 +1,21 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ INTEGRATION_NAME: 'integrationName',
+ DIMENSION_TYPE: 'dimensionType',
+} as const;
+
+type InputID = ValueOf;
+
+type SageIntacctDimensionForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.INTEGRATION_NAME]: string;
+ [INPUT_IDS.DIMENSION_TYPE]: typeof CONST.SAGE_INTACCT_MAPPING_VALUE.TAG | typeof CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD;
+ }
+>;
+
+export type {SageIntacctDimensionForm};
+export default INPUT_IDS;
diff --git a/src/types/form/WorkspaceReportFieldsForm.ts b/src/types/form/WorkspaceReportFieldForm.ts
similarity index 89%
rename from src/types/form/WorkspaceReportFieldsForm.ts
rename to src/types/form/WorkspaceReportFieldForm.ts
index a33437c0b070..69651cf7de20 100644
--- a/src/types/form/WorkspaceReportFieldsForm.ts
+++ b/src/types/form/WorkspaceReportFieldForm.ts
@@ -14,7 +14,7 @@ const INPUT_IDS = {
type InputID = ValueOf;
-type WorkspaceReportFieldsForm = Form<
+type WorkspaceReportFieldForm = Form<
InputID,
{
[INPUT_IDS.NAME]: string;
@@ -27,5 +27,5 @@ type WorkspaceReportFieldsForm = Form<
}
>;
-export type {WorkspaceReportFieldsForm, InputID};
+export type {WorkspaceReportFieldForm, InputID};
export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 44c0cb81cb91..dbfc6e5095f6 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -53,9 +53,11 @@ export type {PolicyDistanceRateEditForm} from './PolicyDistanceRateEditForm';
export type {WalletAdditionalDetailsForm} from './WalletAdditionalDetailsForm';
export type {NewChatNameForm} from './NewChatNameForm';
export type {WorkForm} from './WorkForm';
+export type {SageIntacctDimensionForm} from './SageIntacctDimensionsForm';
export type {SubscriptionSizeForm} from './SubscriptionSizeForm';
-export type {WorkspaceReportFieldsForm} from './WorkspaceReportFieldsForm';
+export type {WorkspaceReportFieldForm} from './WorkspaceReportFieldForm';
export type {SageIntactCredentialsForm} from './SageIntactCredentialsForm';
+export type {NetSuiteCustomFieldForm} from './NetSuiteCustomFieldForm';
export type {NetSuiteTokenInputForm} from './NetSuiteTokenInputForm';
export type {NetSuiteCustomFormIDForm} from './NetSuiteCustomFormIDForm';
export type {default as Form} from './Form';
diff --git a/src/types/onyx/BillingGraceEndPeriod.ts b/src/types/onyx/BillingGraceEndPeriod.ts
index 44235694adb9..01b440dcab4e 100644
--- a/src/types/onyx/BillingGraceEndPeriod.ts
+++ b/src/types/onyx/BillingGraceEndPeriod.ts
@@ -1,11 +1,5 @@
/** Model of BillingGraceEndPeriod's Shared NVP record */
type BillingGraceEndPeriod = {
- /** The name of the NVP key. */
- name: string;
-
- /** The permission associated with the NVP key. */
- permissions: string;
-
/** The grace period end date (epoch timestamp) of the workspace's owner where the user is a member of. */
value: number;
};
diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts
index 138d89ae6617..cdaaddd7dce2 100644
--- a/src/types/onyx/Card.ts
+++ b/src/types/onyx/Card.ts
@@ -89,14 +89,38 @@ type CardList = Record;
/** Issue new card flow steps */
type IssueNewCardStep = ValueOf;
+/** Data required to be sent to issue a new card */
+type IssueNewCardData = {
+ /** The email address of the cardholder */
+ assigneeEmail: string;
+
+ /** Card type */
+ cardType: ValueOf;
+
+ /** Card spending limit type */
+ limitType: ValueOf;
+
+ /** Card spending limit */
+ limit: number;
+
+ /** Name of the card */
+ cardTitle: string;
+};
+
/** Model of Issue new card flow */
type IssueNewCard = {
/** The current step of the flow */
currentStep: IssueNewCardStep;
+
+ /** Data required to be sent to issue a new card */
+ data: IssueNewCardData;
+
+ /** Whether the user is editing step */
+ isEditing: boolean;
};
/** List of Expensify cards */
type WorkspaceCardsList = Record;
export default Card;
-export type {ExpensifyCardDetails, CardList, IssueNewCard, IssueNewCardStep, WorkspaceCardsList};
+export type {ExpensifyCardDetails, CardList, IssueNewCard, IssueNewCardStep, IssueNewCardData, WorkspaceCardsList};
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index fb95d6bea69e..e3b7ea301e26 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -661,8 +661,22 @@ type InvoiceItem = {
name: string;
};
+/**
+ * NetSuite Custom List data modal
+ */
+type NetSuiteCustomListSource = {
+ /** Internal ID of the custom list in NetSuite */
+ id: string;
+
+ /** Name of the custom list */
+ name: string;
+};
+
/** Data from the NetSuite accounting integration. */
type NetSuiteConnectionData = {
+ /** Collection of the custom lists in the NetSuite account */
+ customLists: NetSuiteCustomListSource[];
+
/** Collection of the subsidiaries present in the NetSuite account */
subsidiaryList: NetSuiteSubsidiary[];
@@ -706,6 +720,9 @@ type NetSuiteExportDateOptions = 'SUBMITTED' | 'EXPORTED' | 'LAST_EXPENSE';
/** NetSuite journal posting preference values */
type NetSuiteJournalPostingPreferences = 'JOURNALS_POSTING_TOTAL_LINE' | 'JOURNALS_POSTING_INDIVIDUAL_LINE';
+/** NetSuite custom segment/records and custom lists mapping values */
+type NetSuiteCustomFieldMapping = 'TAG' | 'REPORT_FIELD';
+
/** The custom form selection options for transactions (any one will be used at most) */
type NetSuiteCustomFormIDOptions = {
/** If the option is expense report */
@@ -718,6 +735,36 @@ type NetSuiteCustomFormIDOptions = {
journalEntry?: string;
};
+/** NetSuite custom list */
+type NetSuiteCustomList = {
+ /** The name of the custom list in NetSuite */
+ listName: string;
+
+ /** The internalID of the custom list in NetSuite */
+ internalID: string;
+
+ /** The ID of the transaction form field we'll code the list option onto during Export */
+ transactionFieldID: string;
+
+ /** Whether we import this list as a report field or tag */
+ mapping: NetSuiteCustomFieldMapping;
+};
+
+/** NetSuite custom segments/records */
+type NetSuiteCustomSegment = {
+ /** The name of the custom segment */
+ segmentName: string;
+
+ /** The ID of the custom segment in NetSuite */
+ internalID: string;
+
+ /** The ID of the transaction form field we'll code this segment onto during Export */
+ scriptID: string;
+
+ /** Whether we import this segment as a report field or tag */
+ mapping: NetSuiteCustomFieldMapping;
+};
+
/** The custom form ID object */
type NetSuiteCustomFormID = {
/** The custom form selections for reimbursable transactions */
@@ -795,19 +842,7 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{
syncReimbursedReports: boolean;
/** The relevant details of the custom segments we import into Expensify and code onto expenses */
- customSegments?: Array<{
- /** The name of the custom segment */
- segmentName: string;
-
- /** The ID of the custom segment in NetSuite */
- internalID: string;
-
- /** The ID of the transaction form field we'll code this segment onto during Export */
- scriptID: string;
-
- /** Whether we import this segment as a report field or tag */
- mapping: 'tag' | 'reportField';
- }>;
+ customSegments?: NetSuiteCustomSegment[];
/** Whether to import Employees from NetSuite into Expensify */
syncPeople: boolean;
@@ -828,19 +863,7 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{
syncCustomSegments?: boolean;
/** The relevant details of the custom lists we import into Expensify and code onto expenses */
- customLists?: Array<{
- /** The name of the custom list in NetSuite */
- listName: string;
-
- /** The internalID of the custom list in NetSuite */
- internalID: string;
-
- /** The ID of the transaction form field we'll code the list option onto during Export */
- transactionFieldID: string;
-
- /** Whether we import this list as a report field or tag */
- mapping: 'tag' | 'reportField';
- }>;
+ customLists?: NetSuiteCustomList[];
/** Whether we'll import Expense Categories into Expensify as categories */
syncCategories: boolean;
@@ -958,34 +981,187 @@ type NetSuiteConnection = {
tokenSecret: string;
};
+/** One of the SageIntacctConnectionData object elements */
+type SageIntacctDataElement = {
+ /** Element ID */
+ id: string;
+
+ /** Element name */
+ name: string;
+};
+
+/** One of the SageIntacctConnectionData object elements with value */
+type SageIntacctDataElementWithValue = SageIntacctDataElement & {
+ /** Element value */
+ value: string;
+};
+
/**
* Connection data for Sage Intacct
*/
-// eslint-disable-next-line @typescript-eslint/ban-types
-type SageIntacctConnectionData = {};
+type SageIntacctConnectionData = {
+ /** Collection of credit cards */
+ creditCards: SageIntacctDataElement[];
+
+ /** Collection of entities */
+ entities: SageIntacctDataElementWithValue[];
+
+ /** Collection of bank accounts */
+ bankAccounts: SageIntacctDataElement[];
+
+ /** Collection of vendors */
+ vendors: SageIntacctDataElementWithValue[];
+
+ /** Collection of journals */
+ journals: SageIntacctDataElementWithValue[];
+
+ /** Collection of items */
+ items: SageIntacctDataElement[];
+
+ /** Collection of tax solutions IDs */
+ taxSolutionIDs: string[];
+};
+
+/** Mapping value for Sage Intacct */
+type SageIntacctMappingValue = ValueOf;
+
+/** Mapping names for Sage Intacct */
+type SageIntacctMappingName = ValueOf;
+
+/**
+ * Sage Intacct dimension type
+ */
+type SageIntacctDimension = {
+ /** Name of user defined dimention */
+ dimension: string;
+
+ /** Mapping value for user defined dimention */
+ mapping: typeof CONST.SAGE_INTACCT_MAPPING_VALUE.TAG | typeof CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD;
+};
+
+/** Mapping type for Sage Intacct */
+type SageIntacctMappingType = {
+ /** Whether should sync items for Sage Intacct */
+ syncItems: boolean;
+
+ /** Mapping type for Sage Intacct */
+ departments: SageIntacctMappingValue;
+
+ /** Mapping type for Sage Intacct */
+ classes: SageIntacctMappingValue;
+
+ /** Mapping type for Sage Intacct */
+ locations: SageIntacctMappingValue;
+
+ /** Mapping type for Sage Intacct */
+ customers: SageIntacctMappingValue;
+
+ /** Mapping type for Sage Intacct */
+ projects: SageIntacctMappingValue;
+
+ /** User defined dimention type for Sage Intacct */
+ dimensions: SageIntacctDimension[];
+};
+
+/** Configuration of automatic synchronization from Sage Intacct to the app */
+type SageIntacctAutoSyncConfig = {
+ /** Whether changes made in Sage Intacct should be reflected into the app automatically */
+ enabled: boolean;
+};
+
+/** Sage Intacct sync */
+type SageIntacctSyncConfig = {
+ /** ID of the bank account for Sage Intacct bill payment account */
+ reimbursementAccountID?: string;
+
+ /** Whether the reimbursed reports should be synced */
+ syncReimbursedReports: boolean | string;
+};
/**
* Connection config for Sage Intacct
*/
-type SageIntacctConnectiosConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{
- /** Sage Intacct credentials */
- credentials: {
- /** Sage Intacct companyID */
- companyID: string;
+type SageIntacctOfflineStateKeys = keyof SageIntacctMappingType | `dimension_${string}`;
- /** Sage Intacct password */
- password: string;
+/**
+ * Connection config for Sage Intacct
+ */
+type SageIntacctConnectionsConfig = OnyxCommon.OnyxValueWithOfflineFeedback<
+ {
+ /** Sage Intacct credentials */
+ credentials: {
+ /** Sage Intacct companyID */
+ companyID: string;
- /** Sage Intacct userID */
- userID: string;
- };
+ /** Sage Intacct password */
+ password: string;
- /** Collection of Sage Intacct config errors */
- errors?: OnyxCommon.Errors;
+ /** Sage Intacct userID */
+ userID: string;
+ };
- /** Collection of form field errors */
- errorFields?: OnyxCommon.ErrorFields;
-}>;
+ /** Sage Intacct mappings */
+ mappings: SageIntacctMappingType;
+
+ /** Sage Intacct tax */
+ tax: {
+ /** Sage Intacct tax solution ID */
+ taxSolutionID: string;
+
+ /** Whether should sync tax with Sage Intacct */
+ syncTax: boolean;
+ };
+
+ /** Sage Intacct export configs */
+ export: OnyxCommon.OnyxValueWithOfflineFeedback<{
+ /** Export date type */
+ exportDate: ValueOf;
+
+ /** The e-mail of the exporter */
+ exporter: string;
+
+ /** Defines how non-reimbursable expenses are exported */
+ nonReimbursable: ValueOf;
+
+ /** Account that receives the non-reimbursable expenses */
+ nonReimbursableAccount: string;
+
+ /** Default vendor used for credit card transactions of non-reimbursable bill */
+ nonReimbursableCreditCardChargeDefaultVendor: string;
+
+ /** Default vendor of non-reimbursable bill */
+ nonReimbursableVendor: string;
+
+ /** Defines how reimbursable expenses are exported */
+ reimbursable: ValueOf;
+
+ /** Default vendor of reimbursable bill */
+ reimbursableExpenseReportDefaultVendor: string;
+
+ /** Collection of mapping field errors, which will be triggered when update action fails */
+ errorFields?: OnyxCommon.ErrorFields;
+ }>;
+
+ /** Whether employees should be imported from Sage Intacct */
+ importEmployees: boolean;
+
+ /** Sage Intacct approval mode */
+ approvalMode: ValueOf | null;
+
+ /** Configuration of automatic synchronization from Sage Intacct to the app */
+ autoSync: SageIntacctAutoSyncConfig;
+
+ /** Sage Intacct sync */
+ sync: SageIntacctSyncConfig;
+
+ /** Collection of Sage Intacct config errors */
+ errors?: OnyxCommon.Errors;
+
+ /** Collection of form field errors */
+ errorFields?: OnyxCommon.ErrorFields;
+ },
+ SageIntacctOfflineStateKeys | keyof SageIntacctSyncConfig | keyof SageIntacctAutoSyncConfig
+>;
/** State of integration connection */
type Connection = {
@@ -1011,7 +1187,7 @@ type Connections = {
netsuite: NetSuiteConnection;
/** Sage Intacct integration connection */
- intacct: Connection;
+ intacct: Connection;
};
/** Names of integration connections */
@@ -1065,7 +1241,7 @@ type PolicyReportField = {
deletable: boolean;
/** Value of the field */
- value: string | null;
+ value?: string | null;
/** Options to select from if field is of type dropdown */
values: string[];
@@ -1363,6 +1539,7 @@ export type {
PolicyConnectionSyncStage,
PolicyConnectionSyncProgress,
Connections,
+ SageIntacctOfflineStateKeys,
ConnectionName,
Tenant,
Account,
@@ -1373,7 +1550,18 @@ export type {
NetSuiteConnection,
ConnectionLastSync,
NetSuiteSubsidiary,
+ NetSuiteCustomList,
+ NetSuiteCustomSegment,
+ NetSuiteCustomListSource,
+ NetSuiteCustomFieldMapping,
NetSuiteAccount,
NetSuiteCustomFormIDOptions,
NetSuiteCustomFormID,
+ SageIntacctMappingValue,
+ SageIntacctMappingType,
+ SageIntacctMappingName,
+ SageIntacctDimension,
+ SageIntacctDataElementWithValue,
+ SageIntacctDataElement,
+ SageIntacctConnectionsConfig,
};
diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts
index 89d1b6c72566..05cd2db18006 100644
--- a/src/types/onyx/SearchResults.ts
+++ b/src/types/onyx/SearchResults.ts
@@ -1,8 +1,8 @@
import type {ValueOf} from 'type-fest';
+import type {SearchColumnType, SortOrder} from '@components/Search/types';
import type ReportListItem from '@components/SelectionList/Search/ReportListItem';
import type TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
-import type {SearchColumnType, SortOrder} from '@libs/SearchUtils';
import type CONST from '@src/CONST';
/** Types of search data */
@@ -93,6 +93,9 @@ type SearchPolicyDetails = {
name: string;
};
+/** The action that can be performed for the transaction */
+type SearchTransactionAction = ValueOf;
+
/** Model of report search result */
type SearchReport = {
/** The ID of the report */
@@ -120,7 +123,7 @@ type SearchReport = {
created?: string;
/** The action that can be performed for the report */
- action?: string;
+ action?: SearchTransactionAction;
};
/** Model of transaction search result */
@@ -213,7 +216,7 @@ type SearchTransaction = {
transactionThreadReportID: string;
/** The action that can be performed for the transaction */
- action: string;
+ action: SearchTransactionAction;
/** The MCC Group associated with the transaction */
mccGroup?: ValueOf;
@@ -241,6 +244,9 @@ type SearchResults = {
/** Search results data */
data: Record> & Record & Record;
+
+ /** Whether search data is being fetched from server */
+ isLoading?: boolean;
};
export default SearchResults;
@@ -249,6 +255,7 @@ export type {
SearchQuery,
SearchTransaction,
SearchTransactionType,
+ SearchTransactionAction,
SearchPersonalDetails,
SearchPolicyDetails,
SearchAccountDetails,
diff --git a/tests/actions/ReportFieldTest.ts b/tests/actions/ReportFieldTest.ts
new file mode 100644
index 000000000000..4676d187a211
--- /dev/null
+++ b/tests/actions/ReportFieldTest.ts
@@ -0,0 +1,737 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import DateUtils from '@libs/DateUtils';
+import {generateFieldID} from '@libs/WorkspaceReportFieldUtils';
+import CONST from '@src/CONST';
+import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
+import * as Policy from '@src/libs/actions/Policy/Policy';
+import * as ReportField from '@src/libs/actions/Policy/ReportField';
+import type {CreateReportFieldArguments} from '@src/libs/actions/Policy/ReportField';
+import * as ReportUtils from '@src/libs/ReportUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm';
+import type {PolicyReportField, Policy as PolicyType} from '@src/types/onyx';
+import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon';
+import createRandomPolicy from '../utils/collections/policies';
+import * as TestHelper from '../utils/TestHelper';
+import type {MockFetch} from '../utils/TestHelper';
+import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
+
+OnyxUpdateManager();
+describe('actions/ReportField', () => {
+ type PolicyReportFieldWithOfflineFeedback = Record>;
+
+ function connectToFetchPolicy(policyID: string): Promise> {
+ return new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ callback: (workspace) => {
+ Onyx.disconnect(connectionID);
+ resolve(workspace);
+ },
+ });
+ });
+ }
+
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ });
+ });
+
+ let mockFetch: MockFetch;
+ beforeEach(() => {
+ global.fetch = TestHelper.getGlobalFetchMock();
+ mockFetch = fetch as MockFetch;
+ return Onyx.clear().then(waitForBatchedUpdates);
+ });
+
+ describe('createReportField', () => {
+ it('creates a new text report field of a workspace', async () => {
+ mockFetch.pause();
+ Onyx.set(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {});
+ await waitForBatchedUpdates();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const newReportField: OnyxValueWithOfflineFeedback = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.TEXT,
+ defaultValue: 'Default Value',
+ values: [],
+ disabledOptions: [],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ externalIDs: [],
+ isTax: false,
+ };
+ const createReportFieldArguments: CreateReportFieldArguments = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.TEXT,
+ initialValue: 'Default Value',
+ };
+
+ ReportField.createReportField(policyID, createReportFieldArguments);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the new report field was added to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {...newReportField, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
+ });
+
+ // Check for success data
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // Check if the policy pending action was cleared
+ expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
+ });
+
+ it('creates a new date report field of a workspace', async () => {
+ mockFetch.pause();
+ Onyx.set(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {});
+ await waitForBatchedUpdates();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field 2';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const defaultDate = DateUtils.extractDate(new Date().toString());
+ const newReportField: OnyxValueWithOfflineFeedback = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.DATE,
+ defaultValue: defaultDate,
+ values: [],
+ disabledOptions: [],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ externalIDs: [],
+ isTax: false,
+ };
+ const createReportFieldArguments: CreateReportFieldArguments = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.DATE,
+ initialValue: defaultDate,
+ };
+
+ ReportField.createReportField(policyID, createReportFieldArguments);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the new report field was added to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {...newReportField, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
+ });
+
+ // Check for success data
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // Check if the policy pending action was cleared
+ expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
+ });
+
+ it('creates a new list report field of a workspace', async () => {
+ mockFetch.pause();
+ Onyx.set(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {
+ [INPUT_IDS.LIST_VALUES]: ['Value 1', 'Value 2'],
+ [INPUT_IDS.DISABLED_LIST_VALUES]: [false, true],
+ });
+ await waitForBatchedUpdates();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field 3';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const newReportField: OnyxValueWithOfflineFeedback = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.LIST,
+ defaultValue: '',
+ values: ['Value 1', 'Value 2'],
+ disabledOptions: [false, true],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ value: CONST.REPORT_FIELD_TYPES.LIST,
+ };
+ const createReportFieldArguments: CreateReportFieldArguments = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.LIST,
+ initialValue: '',
+ };
+
+ ReportField.createReportField(policyID, createReportFieldArguments);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the new report field was added to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {...newReportField, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
+ });
+
+ // Check for success data
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // Check if the policy pending action was cleared
+ expect(policy?.fieldList?.[reportFieldKey].pendingAction).toBeFalsy();
+ });
+ });
+
+ describe('deleteReportField', () => {
+ it('Deleted a report field from a workspace', async () => {
+ const fakePolicy = createRandomPolicy(0);
+ const reportFieldName = 'Test Field';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const fakeReportField: OnyxValueWithOfflineFeedback = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.TEXT,
+ defaultValue: 'Default Value',
+ values: [],
+ disabledOptions: [],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ value: 'default',
+ externalIDs: [],
+ isTax: false,
+ };
+ fakePolicy.fieldList = {
+ [reportFieldKey]: fakeReportField,
+ };
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(fakePolicy.id);
+
+ // check if the report field exists in the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: fakeReportField,
+ });
+
+ ReportField.deleteReportFields(fakePolicy.id, [reportFieldKey]);
+ await waitForBatchedUpdates();
+
+ // Check for success data
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(fakePolicy.id);
+
+ // Check if the policy report field was removed
+ expect(policy?.fieldList?.[reportFieldKey]).toBeFalsy();
+ });
+
+ it('Deleted a report field from a workspace when API fails', async () => {
+ const policyID = Policy.generatePolicyID();
+ const fakePolicy = createRandomPolicy(Number(policyID));
+ const reportFieldName = 'Test Field';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const fakeReportField: OnyxValueWithOfflineFeedback = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.TEXT,
+ defaultValue: 'Default Value',
+ values: [],
+ disabledOptions: [],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ value: 'default',
+ externalIDs: [],
+ isTax: false,
+ };
+ fakePolicy.fieldList = {
+ [reportFieldKey]: fakeReportField,
+ };
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the report field exists in the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: fakeReportField,
+ });
+
+ // Check for failure data
+ mockFetch.fail();
+ ReportField.deleteReportFields(policyID, [reportFieldKey]);
+ await waitForBatchedUpdates();
+
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // check if the deleted report field was reset in the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: fakeReportField,
+ });
+ });
+ });
+
+ describe('updateReportFieldInitialValue', () => {
+ it('updates the initial value of a text report field', async () => {
+ mockFetch.pause();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const oldInitialValue = 'Old initial value';
+ const newInitialValue = 'New initial value';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField: PolicyReportField = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.TEXT,
+ defaultValue: oldInitialValue,
+ values: [],
+ disabledOptions: [],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ };
+ const fakePolicy = createRandomPolicy(Number(policyID));
+
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {...fakePolicy, fieldList: {[reportFieldKey]: reportField}});
+ await waitForBatchedUpdates();
+
+ ReportField.updateReportFieldInitialValue(policyID, reportFieldID, newInitialValue);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the updated report field was set to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {
+ ...reportField,
+ defaultValue: newInitialValue,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ });
+
+ // Check for success data
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // Check if the policy pending action was cleared
+ expect(policy?.fieldList?.[reportFieldKey].pendingAction).toBeFalsy();
+ });
+
+ it('updates the initial value of a text report field when api returns an error', async () => {
+ mockFetch.pause();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const oldInitialValue = 'Old initial value';
+ const newInitialValue = 'New initial value';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField: PolicyReportField = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.TEXT,
+ defaultValue: oldInitialValue,
+ values: [],
+ disabledOptions: [],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ };
+ const fakePolicy = createRandomPolicy(Number(policyID));
+
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {...fakePolicy, fieldList: {[reportFieldKey]: reportField}});
+ await waitForBatchedUpdates();
+
+ ReportField.updateReportFieldInitialValue(policyID, reportFieldID, newInitialValue);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the updated report field was set to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {
+ ...reportField,
+ defaultValue: newInitialValue,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ });
+
+ // Check for failure data
+ mockFetch.fail();
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // check if the updated report field was reset in the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: reportField,
+ });
+ // Check if the policy errors was set
+ expect(policy?.errorFields?.[reportFieldKey]).toBeTruthy();
+ });
+ });
+
+ describe('updateReportFieldListValueEnabled', () => {
+ it('updates the enabled flag of report field list values', async () => {
+ mockFetch.pause();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const valueIndexesTpUpdate = [1, 2];
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField: PolicyReportField = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.LIST,
+ defaultValue: 'Value 2',
+ values: ['Value 1', 'Value 2', 'Value 3'],
+ disabledOptions: [false, false, true],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ value: CONST.REPORT_FIELD_TYPES.LIST,
+ };
+ const fakePolicy = createRandomPolicy(Number(policyID));
+
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {...fakePolicy, fieldList: {[reportFieldKey]: reportField}});
+ await waitForBatchedUpdates();
+
+ ReportField.updateReportFieldListValueEnabled(policyID, reportFieldID, valueIndexesTpUpdate, false);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the new report field was added to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {
+ ...reportField,
+ defaultValue: '',
+ disabledOptions: [false, true, true],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ });
+
+ // Check for success data
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // Check if the policy pending action was cleared
+ expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
+ });
+
+ it('updates the enabled flag of a report field list value when api returns an error', async () => {
+ mockFetch.pause();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const valueIndexesToUpdate = [1];
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField: PolicyReportField = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.LIST,
+ defaultValue: 'Value 2',
+ values: ['Value 1', 'Value 2', 'Value 3'],
+ disabledOptions: [false, false, true],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ value: CONST.REPORT_FIELD_TYPES.LIST,
+ };
+ const fakePolicy = createRandomPolicy(Number(policyID));
+
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {...fakePolicy, fieldList: {[reportFieldKey]: reportField}});
+ await waitForBatchedUpdates();
+
+ ReportField.updateReportFieldListValueEnabled(policyID, reportFieldID, valueIndexesToUpdate, false);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the new report field was added to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {
+ ...reportField,
+ defaultValue: '',
+ disabledOptions: [false, true, true],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ });
+
+ // Check for failure data
+ mockFetch.fail();
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // check if the updated report field was reset in the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: reportField,
+ });
+ // Check if the policy errors was set
+ expect(policy?.errorFields?.[reportFieldKey]).toBeTruthy();
+ });
+ });
+
+ describe('addReportFieldListValue', () => {
+ it('adds a new value to a report field list', async () => {
+ mockFetch.pause();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField: PolicyReportField = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.LIST,
+ defaultValue: 'Value 2',
+ values: ['Value 1', 'Value 2', 'Value 3'],
+ disabledOptions: [false, false, true],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ value: CONST.REPORT_FIELD_TYPES.LIST,
+ };
+ const fakePolicy = createRandomPolicy(Number(policyID));
+ const newListValueName = 'Value 4';
+
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {...fakePolicy, fieldList: {[reportFieldKey]: reportField}});
+ await waitForBatchedUpdates();
+
+ ReportField.addReportFieldListValue(policyID, reportFieldID, newListValueName);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+ // check if the new report field was added to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {
+ ...reportField,
+ values: [...reportField.values, newListValueName],
+ disabledOptions: [...reportField.disabledOptions, false],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ });
+
+ // Check for success data
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // Check if the policy pending action was cleared
+ expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
+ });
+
+ it('adds a new value to a report field list when api returns an error', async () => {
+ mockFetch.pause();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField: PolicyReportField = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.LIST,
+ defaultValue: 'Value 2',
+ values: ['Value 1', 'Value 2', 'Value 3'],
+ disabledOptions: [false, false, true],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ value: CONST.REPORT_FIELD_TYPES.LIST,
+ };
+ const fakePolicy = createRandomPolicy(Number(policyID));
+ const newListValueName = 'Value 4';
+
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {...fakePolicy, fieldList: {[reportFieldKey]: reportField}});
+ await waitForBatchedUpdates();
+
+ ReportField.addReportFieldListValue(policyID, reportFieldID, newListValueName);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // check if the new report field was added to the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {
+ ...reportField,
+ values: [...reportField.values, newListValueName],
+ disabledOptions: [...reportField.disabledOptions, false],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ });
+
+ // Check for failure data
+ mockFetch.fail();
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // check if the updated report field was reset in the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: reportField,
+ });
+ });
+ });
+
+ describe('removeReportFieldListValue', () => {
+ it('removes list values from a report field list', async () => {
+ mockFetch.pause();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField: PolicyReportField = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.LIST,
+ defaultValue: 'Value 2',
+ values: ['Value 1', 'Value 2', 'Value 3'],
+ disabledOptions: [false, false, true],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ value: CONST.REPORT_FIELD_TYPES.LIST,
+ };
+ const fakePolicy = createRandomPolicy(Number(policyID));
+
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {...fakePolicy, fieldList: {[reportFieldKey]: reportField}});
+ await waitForBatchedUpdates();
+
+ ReportField.removeReportFieldListValue(policyID, reportFieldID, [1, 2]);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // Check if the values were removed from the report field
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {
+ ...reportField,
+ defaultValue: '',
+ values: ['Value 1'],
+ disabledOptions: [false],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ });
+
+ // Check for success data
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+ // Check if the policy pending action was cleared
+ expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
+ });
+
+ it('removes list values from a report field list when api returns an error', async () => {
+ mockFetch.pause();
+
+ const policyID = Policy.generatePolicyID();
+ const reportFieldName = 'Test Field';
+ const reportFieldID = generateFieldID(reportFieldName);
+ const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
+ const reportField: PolicyReportField = {
+ name: reportFieldName,
+ type: CONST.REPORT_FIELD_TYPES.LIST,
+ defaultValue: 'Value 2',
+ values: ['Value 1', 'Value 2', 'Value 3'],
+ disabledOptions: [false, false, true],
+ fieldID: reportFieldID,
+ orderWeight: 1,
+ deletable: false,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ value: CONST.REPORT_FIELD_TYPES.LIST,
+ };
+ const fakePolicy = createRandomPolicy(Number(policyID));
+
+ Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {...fakePolicy, fieldList: {[reportFieldKey]: reportField}});
+ await waitForBatchedUpdates();
+
+ ReportField.removeReportFieldListValue(policyID, reportFieldID, [1, 2]);
+ await waitForBatchedUpdates();
+
+ let policy: OnyxEntry = await connectToFetchPolicy(policyID);
+
+ // Check if the values were removed from the report field
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: {
+ ...reportField,
+ defaultValue: '',
+ values: ['Value 1'],
+ disabledOptions: [false],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ });
+
+ // Check for failure data
+ mockFetch.fail();
+ mockFetch.resume();
+ await waitForBatchedUpdates();
+
+ policy = await connectToFetchPolicy(policyID);
+
+ // check if the updated report field was reset in the policy
+ expect(policy?.fieldList).toStrictEqual({
+ [reportFieldKey]: reportField,
+ });
+ // Check if the policy errors was set
+ expect(policy?.errorFields?.[reportFieldKey]).toBeTruthy();
+ });
+ });
+});
diff --git a/tests/actions/ReportFieldsTest.ts b/tests/actions/ReportFieldsTest.ts
deleted file mode 100644
index 46af89407eed..000000000000
--- a/tests/actions/ReportFieldsTest.ts
+++ /dev/null
@@ -1,386 +0,0 @@
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
-import Onyx from 'react-native-onyx';
-import DateUtils from '@libs/DateUtils';
-import {generateFieldID} from '@libs/WorkspaceReportFieldsUtils';
-import CONST from '@src/CONST';
-import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
-import * as Policy from '@src/libs/actions/Policy/Policy';
-import * as ReportField from '@src/libs/actions/Policy/ReportField';
-import type {CreateReportFieldArguments} from '@src/libs/actions/Policy/ReportField';
-import * as ReportUtils from '@src/libs/ReportUtils';
-import ONYXKEYS from '@src/ONYXKEYS';
-import INPUT_IDS from '@src/types/form/WorkspaceReportFieldsForm';
-import type {PolicyReportField, Policy as PolicyType} from '@src/types/onyx';
-import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon';
-import createRandomPolicy from '../utils/collections/policies';
-import * as TestHelper from '../utils/TestHelper';
-import type {MockFetch} from '../utils/TestHelper';
-import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
-
-OnyxUpdateManager();
-describe('actions/ReportField', () => {
- beforeAll(() => {
- Onyx.init({
- keys: ONYXKEYS,
- });
- });
-
- let mockFetch: MockFetch;
- beforeEach(() => {
- global.fetch = TestHelper.getGlobalFetchMock();
- mockFetch = fetch as MockFetch;
- return Onyx.clear().then(waitForBatchedUpdates);
- });
-
- describe('createReportField', () => {
- it('creates a new text report field of a workspace', async () => {
- mockFetch?.pause?.();
- Onyx.set(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {});
- await waitForBatchedUpdates();
-
- const policyID = Policy.generatePolicyID();
- const reportFieldName = 'Test Field';
- const reportFieldID = generateFieldID(reportFieldName);
- const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
- const newReportField: OnyxValueWithOfflineFeedback> = {
- name: reportFieldName,
- type: CONST.REPORT_FIELD_TYPES.TEXT,
- defaultValue: 'Default Value',
- values: [],
- disabledOptions: [],
- fieldID: reportFieldID,
- orderWeight: 1,
- deletable: false,
- keys: [],
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- externalIDs: [],
- isTax: false,
- };
- const createReportFieldArguments: CreateReportFieldArguments = {
- name: reportFieldName,
- type: CONST.REPORT_FIELD_TYPES.TEXT,
- initialValue: 'Default Value',
- };
-
- ReportField.createReportField(policyID, createReportFieldArguments);
- await waitForBatchedUpdates();
-
- let policy: OnyxEntry | OnyxCollection = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // check if the new report field was added to the policy
- expect(policy?.fieldList).toStrictEqual({
- [reportFieldKey]: newReportField,
- });
-
- // Check for success data
- mockFetch?.resume?.();
- await waitForBatchedUpdates();
-
- policy = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: ONYXKEYS.COLLECTION.POLICY,
- waitForCollectionCallback: true,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // Check if the policy pending action was cleared
- // @ts-expect-error pendingFields is not null
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
- });
-
- it('creates a new date report field of a workspace', async () => {
- mockFetch?.pause?.();
- Onyx.set(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {});
- await waitForBatchedUpdates();
-
- const policyID = Policy.generatePolicyID();
- const reportFieldName = 'Test Field 2';
- const reportFieldID = generateFieldID(reportFieldName);
- const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
- const defaultDate = DateUtils.extractDate(new Date().toString());
- const newReportField: OnyxValueWithOfflineFeedback> = {
- name: reportFieldName,
- type: CONST.REPORT_FIELD_TYPES.DATE,
- defaultValue: defaultDate,
- values: [],
- disabledOptions: [],
- fieldID: reportFieldID,
- orderWeight: 1,
- deletable: false,
- keys: [],
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- externalIDs: [],
- isTax: false,
- };
- const createReportFieldArguments: CreateReportFieldArguments = {
- name: reportFieldName,
- type: CONST.REPORT_FIELD_TYPES.DATE,
- initialValue: defaultDate,
- };
-
- ReportField.createReportField(policyID, createReportFieldArguments);
- await waitForBatchedUpdates();
-
- let policy: OnyxEntry | OnyxCollection = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // check if the new report field was added to the policy
- expect(policy?.fieldList).toStrictEqual({
- [reportFieldKey]: newReportField,
- });
-
- // Check for success data
- mockFetch?.resume?.();
- await waitForBatchedUpdates();
-
- policy = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // Check if the policy pending action was cleared
- // @ts-expect-error pendingFields is not null
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
- });
-
- it('creates a new list report field of a workspace', async () => {
- mockFetch?.pause?.();
- Onyx.set(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {
- [INPUT_IDS.LIST_VALUES]: ['Value 1', 'Value 2'],
- [INPUT_IDS.DISABLED_LIST_VALUES]: [false, true],
- });
- await waitForBatchedUpdates();
-
- const policyID = Policy.generatePolicyID();
- const reportFieldName = 'Test Field 3';
- const reportFieldID = generateFieldID(reportFieldName);
- const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
- const newReportField: OnyxValueWithOfflineFeedback = {
- name: reportFieldName,
- type: CONST.REPORT_FIELD_TYPES.LIST,
- defaultValue: '',
- values: ['Value 1', 'Value 2'],
- disabledOptions: [false, true],
- fieldID: reportFieldID,
- orderWeight: 1,
- deletable: false,
- keys: [],
- externalIDs: [],
- isTax: false,
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- value: CONST.REPORT_FIELD_TYPES.LIST,
- };
- const createReportFieldArguments: CreateReportFieldArguments = {
- name: reportFieldName,
- type: CONST.REPORT_FIELD_TYPES.LIST,
- initialValue: '',
- };
-
- ReportField.createReportField(policyID, createReportFieldArguments);
- await waitForBatchedUpdates();
-
- let policy: OnyxEntry | OnyxCollection = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // check if the new report field was added to the policy
- expect(policy?.fieldList).toStrictEqual({
- [reportFieldKey]: newReportField,
- });
-
- // Check for success data
- mockFetch?.resume?.();
- await waitForBatchedUpdates();
-
- policy = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // Check if the policy pending action was cleared
- // @ts-expect-error pendingFields is not null
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
- });
- });
-
- describe('deleteReportField', () => {
- it('Deleted a report field from a workspace', async () => {
- const fakePolicy = createRandomPolicy(0);
- const reportFieldName = 'Test Field';
- const reportFieldID = generateFieldID(reportFieldName);
- const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
- const fakeReportField: OnyxValueWithOfflineFeedback = {
- name: reportFieldName,
- type: CONST.REPORT_FIELD_TYPES.TEXT,
- defaultValue: 'Default Value',
- values: [],
- disabledOptions: [],
- fieldID: reportFieldID,
- orderWeight: 1,
- deletable: false,
- keys: [],
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
- value: 'default',
- externalIDs: [],
- isTax: false,
- };
- fakePolicy.fieldList = {
- [reportFieldKey]: fakeReportField,
- };
- Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy);
- await waitForBatchedUpdates();
-
- let policy: OnyxEntry | OnyxCollection = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // check if the report field exists in the policy
- expect(policy?.fieldList).toStrictEqual({
- [reportFieldKey]: fakeReportField,
- });
-
- ReportField.deleteReportFields(fakePolicy.id, [reportFieldKey]);
- await waitForBatchedUpdates();
-
- // Check for success data
- mockFetch?.resume?.();
- await waitForBatchedUpdates();
-
- policy = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // Check if the policy report field was removed
- // @ts-expect-error fieldList is not null
- expect(policy?.fieldList?.[reportFieldKey]).toBeFalsy();
-
- // Check if the policy pending action was cleared
- // @ts-expect-error pendingFields is not null
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
- });
-
- it('Deleted a report field from a workspace when API fails', async () => {
- const policyID = Policy.generatePolicyID();
- const fakePolicy = createRandomPolicy(Number(policyID));
- const reportFieldName = 'Test Field';
- const reportFieldID = generateFieldID(reportFieldName);
- const reportFieldKey = ReportUtils.getReportFieldKey(reportFieldID);
- const fakeReportField: OnyxValueWithOfflineFeedback = {
- name: reportFieldName,
- type: CONST.REPORT_FIELD_TYPES.TEXT,
- defaultValue: 'Default Value',
- values: [],
- disabledOptions: [],
- fieldID: reportFieldID,
- orderWeight: 1,
- deletable: false,
- keys: [],
- value: 'default',
- externalIDs: [],
- isTax: false,
- };
- fakePolicy.fieldList = {
- [reportFieldKey]: fakeReportField,
- };
- Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
- await waitForBatchedUpdates();
-
- let policy: OnyxEntry | OnyxCollection = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // check if the report field exists in the policy
- expect(policy?.fieldList).toStrictEqual({
- [reportFieldKey]: fakeReportField,
- });
-
- // Check for failure data
- mockFetch?.fail?.();
- ReportField.deleteReportFields(policyID, [reportFieldKey]);
- await waitForBatchedUpdates();
-
- mockFetch?.resume?.();
- await waitForBatchedUpdates();
-
- policy = await new Promise((resolve) => {
- const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- callback: (workspace) => {
- Onyx.disconnect(connectionID);
- resolve(workspace);
- },
- });
- });
-
- // check if the deleted report field was reset in the policy
- expect(policy?.fieldList).toStrictEqual({
- [reportFieldKey]: fakeReportField,
- });
- // Check if the policy pending action was cleared
- // @ts-expect-error pendingFields is not null
- expect(policy?.pendingFields?.[reportFieldKey]).toBeFalsy();
-
- // Check if the policy pending action was cleared
- // @ts-expect-error pendingFields is not null
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- expect(policy?.fieldList?.[reportFieldKey]?.pendingAction).toBeFalsy();
- });
- });
-});
diff --git a/tests/e2e/compare/compare.ts b/tests/e2e/compare/compare.ts
index 685c5628633b..8c7d95f72cb6 100644
--- a/tests/e2e/compare/compare.ts
+++ b/tests/e2e/compare/compare.ts
@@ -94,3 +94,4 @@ export default (main: Metric | string, delta: Metric | string, outputFile: strin
return writeToMarkdown(outputFile, outputData);
}
};
+export {compareResults};
diff --git a/tests/e2e/compare/output/markdown.ts b/tests/e2e/compare/output/markdown.ts
index 2e6ddfd5f03e..32af6c5e22ad 100644
--- a/tests/e2e/compare/output/markdown.ts
+++ b/tests/e2e/compare/output/markdown.ts
@@ -30,21 +30,19 @@ const buildDurationDetailsEntry = (entry: Entry) =>
.join('
');
const formatEntryDuration = (entry: Entry): string => {
- let formattedDuration = '';
-
if ('baseline' in entry && 'current' in entry) {
- formattedDuration = format.formatMetricDiffChange(entry);
+ return format.formatMetricDiffChange(entry);
}
if ('baseline' in entry) {
- formattedDuration = format.formatMetric(entry.baseline.mean, entry.unit);
+ return format.formatMetric((entry as Entry).baseline.mean, (entry as Entry).unit);
}
if ('current' in entry) {
- formattedDuration = format.formatMetric(entry.current.mean, entry.unit);
+ return format.formatMetric((entry as Entry).current.mean, (entry as Entry).unit);
}
- return formattedDuration;
+ return '';
};
const buildDetailsTable = (entries: Entry[]) => {
@@ -120,3 +118,4 @@ const writeToMarkdown = (filePath: string, data: Data) => {
};
export default writeToMarkdown;
+export {buildMarkdown};
diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts
index 4130b6788894..11c6d3772799 100644
--- a/tests/perf-test/ReportUtils.perf-test.ts
+++ b/tests/perf-test/ReportUtils.perf-test.ts
@@ -42,8 +42,10 @@ describe('ReportUtils', () => {
keys: ONYXKEYS,
safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
});
+ });
- Onyx.multiSet({
+ beforeEach(async () => {
+ await Onyx.multiSet({
...mockedPoliciesMap,
...mockedReportsMap,
});
@@ -55,13 +57,17 @@ describe('ReportUtils', () => {
test('[ReportUtils] findLastAccessedReport on 2k reports and policies', async () => {
const ignoreDomainRooms = true;
- const isFirstTimeNewExpensifyUser = true;
const reports = getMockedReports(2000);
const policies = getMockedPolicies(2000);
const openOnAdminRoom = true;
+ await Onyx.multiSet({
+ [ONYXKEYS.COLLECTION.REPORT]: reports,
+ [ONYXKEYS.COLLECTION.POLICY]: policies,
+ });
+
await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom));
+ await measureFunction(() => ReportUtils.findLastAccessedReport(ignoreDomainRooms, openOnAdminRoom));
});
test('[ReportUtils] canDeleteReportAction on 1k reports and policies', async () => {
diff --git a/tests/perf-test/SidebarLinks.perf-test.tsx b/tests/perf-test/SidebarLinks.perf-test.tsx
index 4caa22d062d4..0bd2e83f4e72 100644
--- a/tests/perf-test/SidebarLinks.perf-test.tsx
+++ b/tests/perf-test/SidebarLinks.perf-test.tsx
@@ -20,6 +20,11 @@ jest.mock('../../src/libs/Navigation/Navigation', () => ({
isNavigationReady: jest.fn(() => Promise.resolve()),
isDisplayedInModal: jest.fn(() => false),
}));
+jest.mock('../../src/libs/Navigation/navigationRef', () => ({
+ getState: () => ({
+ routes: [],
+ }),
+}));
jest.mock('@components/Icon/Expensicons');
jest.mock('@react-navigation/native');
diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx
index b10cae2e7736..ca30eb10b065 100644
--- a/tests/ui/UnreadIndicatorsTest.tsx
+++ b/tests/ui/UnreadIndicatorsTest.tsx
@@ -39,6 +39,10 @@ jest.mock('../../src/components/ConfirmedRoute.tsx');
TestHelper.setupApp();
TestHelper.setupGlobalFetchMock();
+beforeEach(() => {
+ Onyx.set(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true});
+});
+
function scrollUpToRevealNewMessagesBadge() {
const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages');
fireEvent.scroll(screen.getByLabelText(hintText), {
diff --git a/tests/unit/CIGitLogicTest.ts b/tests/unit/CIGitLogicTest.ts
index 83cfb05447b5..50471b76318a 100644
--- a/tests/unit/CIGitLogicTest.ts
+++ b/tests/unit/CIGitLogicTest.ts
@@ -27,6 +27,7 @@ type ExecSyncError = {stderr: Buffer};
function exec(command: string) {
try {
+ Log.info(command);
execSync(command, {stdio: 'inherit'});
} catch (error) {
if ((error as ExecSyncError).stderr) {
@@ -251,11 +252,24 @@ async function assertPRsMergedBetween(from: string, to: string, expected: number
checkoutRepo();
const PRs = await GitUtils.getPullRequestsMergedBetween(from, to);
expect(PRs).toStrictEqual(expected);
+ Log.success(`Verified PRs merged between ${from} and ${to} are [${expected.join(',')}]`);
}
+/*
+ * These tests are different from most jest tests. They create a dummy git repo and simulate the GitHub Actions CI environment
+ * and ensure that deploy checklists, comments, and releases are created correctly and completely,
+ * including a number of real-world edge cases we have encountered and fixed.
+ *
+ * However, because they are different, there are a few additional "rules" with these tests:
+ * - They should not be run in parallel with other tests on the same machine. They will not play nicely with other tests.
+ * - The whole suite should be run. Running individual tests from the suite may not work as expected.
+ */
+
+let startingDir: string;
describe('CIGitLogic', () => {
beforeAll(() => {
Log.info('Starting setup');
+ startingDir = process.cwd();
initGitServer();
checkoutRepo();
Log.success('Setup complete!');
@@ -267,15 +281,16 @@ describe('CIGitLogic', () => {
afterAll(() => {
fs.rmSync(DUMMY_DIR, {recursive: true, force: true});
fs.rmSync(path.resolve(GIT_REMOTE, '..'), {recursive: true, force: true});
+ process.chdir(startingDir);
});
- test('Merge a pull request while the checklist is unlocked', () => {
+ test('Merge a pull request while the checklist is unlocked', async () => {
createBasicPR(1);
mergePR(1);
deployStaging();
// Verify output for checklist and deploy comment
- assertPRsMergedBetween('1.0.0-0', '1.0.0-1', [1]);
+ await assertPRsMergedBetween('1.0.0-0', '1.0.0-1', [1]);
});
test("Merge a pull request with the checklist locked, but don't CP it", () => {
@@ -283,40 +298,40 @@ describe('CIGitLogic', () => {
mergePR(2);
});
- test('Merge a pull request with the checklist locked and CP it to staging', () => {
+ test('Merge a pull request with the checklist locked and CP it to staging', async () => {
createBasicPR(3);
cherryPickPR(3);
// Verify output for checklist
- assertPRsMergedBetween('1.0.0-0', '1.0.0-2', [1, 3]);
+ await assertPRsMergedBetween('1.0.0-0', '1.0.0-2', [1, 3]);
// Verify output for deploy comment
- assertPRsMergedBetween('1.0.0-1', '1.0.0-2', [3]);
+ await assertPRsMergedBetween('1.0.0-1', '1.0.0-2', [3]);
});
- test('Close the checklist', () => {
+ test('Close the checklist', async () => {
deployProduction();
// Verify output for release body and production deploy comments
- assertPRsMergedBetween('1.0.0-0', '1.0.0-2', [1, 3]);
+ await assertPRsMergedBetween('1.0.0-0', '1.0.0-2', [1, 3]);
// Verify output for new checklist and staging deploy comments
- assertPRsMergedBetween('1.0.0-2', '1.0.1-0', [2]);
+ await assertPRsMergedBetween('1.0.0-2', '1.0.1-0', [2]);
});
- test('Merging another pull request when the checklist is unlocked', () => {
+ test('Merging another pull request when the checklist is unlocked', async () => {
createBasicPR(5);
mergePR(5);
deployStaging();
// Verify output for checklist
- assertPRsMergedBetween('1.0.0-2', '1.0.1-1', [2, 5]);
+ await assertPRsMergedBetween('1.0.0-2', '1.0.1-1', [2, 5]);
// Verify output for deploy comment
- assertPRsMergedBetween('1.0.1-0', '1.0.1-1', [5]);
+ await assertPRsMergedBetween('1.0.1-0', '1.0.1-1', [5]);
});
- test('Deploying a PR, then CPing a revert, then adding the same code back again before the next production deploy results in the correct code on staging and production', () => {
+ test('Deploying a PR, then CPing a revert, then adding the same code back again before the next production deploy results in the correct code on staging and production', async () => {
Log.info('Creating myFile.txt in PR #6');
setupGitAsHuman();
exec('git switch main');
@@ -330,10 +345,10 @@ describe('CIGitLogic', () => {
deployStaging();
// Verify output for checklist
- assertPRsMergedBetween('1.0.0-2', '1.0.1-2', [2, 5, 6]);
+ await assertPRsMergedBetween('1.0.0-2', '1.0.1-2', [2, 5, 6]);
// Verify output for deploy comment
- assertPRsMergedBetween('1.0.1-1', '1.0.1-2', [6]);
+ await assertPRsMergedBetween('1.0.1-1', '1.0.1-2', [6]);
Log.info('Appending and prepending content to myFile.txt in PR #7');
setupGitAsHuman();
@@ -351,10 +366,10 @@ Appended content
deployStaging();
// Verify output for checklist
- assertPRsMergedBetween('1.0.0-2', '1.0.1-3', [2, 5, 6, 7]);
+ await assertPRsMergedBetween('1.0.0-2', '1.0.1-3', [2, 5, 6, 7]);
// Verify output for deploy comment
- assertPRsMergedBetween('1.0.1-2', '1.0.1-3', [7]);
+ await assertPRsMergedBetween('1.0.1-2', '1.0.1-3', [7]);
Log.info('Making an unrelated change in PR #8');
setupGitAsHuman();
@@ -392,13 +407,13 @@ Appended content
deployProduction();
// Verify production release list
- assertPRsMergedBetween('1.0.0-2', '1.0.1-4', [2, 5, 6, 7, 9]);
+ await assertPRsMergedBetween('1.0.0-2', '1.0.1-4', [2, 5, 6, 7, 9]);
// Verify PR list for the new checklist
- assertPRsMergedBetween('1.0.1-4', '1.0.2-0', [8, 10]);
+ await assertPRsMergedBetween('1.0.1-4', '1.0.2-0', [8, 10]);
});
- test('Force-pushing to a branch after rebasing older commits', () => {
+ test('Force-pushing to a branch after rebasing older commits', async () => {
createBasicPR(11);
exec('git push origin pr-11');
createBasicPR(12);
@@ -406,10 +421,10 @@ Appended content
deployStaging();
// Verify PRs for checklist
- assertPRsMergedBetween('1.0.1-4', '1.0.2-1', [8, 10, 12]);
+ await assertPRsMergedBetween('1.0.1-4', '1.0.2-1', [8, 10, 12]);
// Verify PRs for deploy comments
- assertPRsMergedBetween('1.0.2-0', '1.0.2-1', [12]);
+ await assertPRsMergedBetween('1.0.2-0', '1.0.2-1', [12]);
checkoutRepo();
setupGitAsHuman();
@@ -422,13 +437,13 @@ Appended content
deployProduction();
// Verify PRs for deploy comments / release
- assertPRsMergedBetween('1.0.1-4', '1.0.2-1', [8, 10, 12]);
+ await assertPRsMergedBetween('1.0.1-4', '1.0.2-1', [8, 10, 12]);
// Verify PRs for new checklist
- assertPRsMergedBetween('1.0.2-1', '1.0.3-0', [11]);
+ await assertPRsMergedBetween('1.0.2-1', '1.0.3-0', [11]);
});
- test('Manual version bump', () => {
+ test('Manual version bump', async () => {
Log.info('Creating manual version bump in PR #13');
checkoutRepo();
setupGitAsHuman();
@@ -449,7 +464,7 @@ Appended content
Log.success(`Deployed v${getVersion()} to staging!`);
// Verify PRs for deploy comments / release and new checklist
- assertPRsMergedBetween('1.0.3-0', '4.0.0-0', [13]);
+ await assertPRsMergedBetween('1.0.3-0', '4.0.0-0', [13]);
Log.info('Creating manual version bump in PR #14');
checkoutRepo();
@@ -477,9 +492,9 @@ Appended content
);
// Verify PRs for deploy comments
- assertPRsMergedBetween('4.0.0-0', '7.0.0-0', [14]);
+ await assertPRsMergedBetween('4.0.0-0', '7.0.0-0', [14]);
// Verify PRs for the deploy checklist
- assertPRsMergedBetween('1.0.3-0', '7.0.0-0', [13, 14]);
+ await assertPRsMergedBetween('1.0.3-0', '7.0.0-0', [13, 14]);
});
});
diff --git a/tests/unit/E2EMarkdownTest.ts b/tests/unit/E2EMarkdownTest.ts
new file mode 100644
index 000000000000..74c5659c9487
--- /dev/null
+++ b/tests/unit/E2EMarkdownTest.ts
@@ -0,0 +1,18 @@
+import {compareResults} from '../e2e/compare/compare';
+import {buildMarkdown} from '../e2e/compare/output/markdown';
+
+const results = {
+ main: {
+ commentLinking: [100.5145680010319, 121.8861090019345, 112.0048420019448, 124.26110899820924, 135.1571460030973, 140.33837900310755, 160.7034499980509, 158.5825610011816],
+ },
+ delta: {
+ commentLinking: [361.5145680010319, 402.8861090019345, 412.0048420019448, 414.26110899820924, 425.1571460030973, 440.33837900310755, 458.7034499980509, 459.5825610011816],
+ },
+};
+
+describe('markdown formatter', () => {
+ it('should format significant changes properly', () => {
+ const data = compareResults(results.main, results.delta, {commentLinking: 'ms'});
+ expect(buildMarkdown(data)).toMatchSnapshot();
+ });
+});
diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts
index c997e6f0048a..81cbebd14e97 100644
--- a/tests/unit/SubscriptionUtilsTest.ts
+++ b/tests/unit/SubscriptionUtilsTest.ts
@@ -8,8 +8,6 @@ import type {BillingGraceEndPeriod, BillingStatus, FundList, StripeCustomerID} f
import createRandomPolicy from '../utils/collections/policies';
const billingGraceEndPeriod: BillingGraceEndPeriod = {
- name: 'owner@email.com',
- permissions: 'read',
value: 0,
};
diff --git a/tests/unit/__snapshots__/E2EMarkdownTest.ts.snap b/tests/unit/__snapshots__/E2EMarkdownTest.ts.snap
new file mode 100644
index 000000000000..9a66a0e2e823
--- /dev/null
+++ b/tests/unit/__snapshots__/E2EMarkdownTest.ts.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`markdown formatter should format significant changes properly 1`] = `
+"## Performance Comparison Report đź“Š
+
+### Significant Changes To Duration
+| Name | Duration |
+| -------------- | --------------------------------------------------- |
+| commentLinking | 131.681 ms → 421.806 ms (+290.125 ms, +220.3%) 🔴🔴 |
+
+Show details
+
+| Name | Duration |
+| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| commentLinking | **Baseline**
Mean: 131.681 ms
Stdev: 19.883 ms (15.1%)
Runs: 100.5145680010319 112.0048420019448 121.8861090019345 124.26110899820924 135.1571460030973 140.33837900310755 158.5825610011816 160.7034499980509
**Current**
Mean: 421.806 ms
Stdev: 30.185 ms (7.2%)
Runs: 361.5145680010319 402.8861090019345 412.0048420019448 414.26110899820924 425.1571460030973 440.33837900310755 458.7034499980509 459.5825610011816 |
+
+
+
+
+### Meaningless Changes To Duration
+_There are no entries_
+
+"
+`;
diff --git a/workflow_tests/mocks/deployMocks.ts b/workflow_tests/mocks/deployMocks.ts
index 2fda1efe8e78..5d711b892002 100644
--- a/workflow_tests/mocks/deployMocks.ts
+++ b/workflow_tests/mocks/deployMocks.ts
@@ -10,11 +10,13 @@ const DEPLOY_STAGING__SETUP_GIT__STEP_MOCK = createMockStep('Setup git for OSBot
]);
const DEPLOY_STAGING__TAG_VERSION__STEP_MOCK = createMockStep('Tag version', 'Tagging new version', 'DEPLOY_STAGING');
const DEPLOY_STAGING__PUSH_TAG__STEP_MOCK = createMockStep('🚀 Push tags to trigger staging deploy 🚀', 'Pushing tag to trigger staging deploy', 'DEPLOY_STAGING');
+const DEPLOY_STAGING__WARN_DEPLOYERS__STEP_MOCK = createMockStep('Warn deployers if staging deploy failed', 'Warning deployers in slack for workflow failure', 'DEPLOY_STAGING');
const DEPLOY_STAGING_STEP_MOCKS = [
DEPLOY_STAGING__CHECKOUT__STEP_MOCK,
DEPLOY_STAGING__SETUP_GIT__STEP_MOCK,
DEPLOY_STAGING__TAG_VERSION__STEP_MOCK,
DEPLOY_STAGING__PUSH_TAG__STEP_MOCK,
+ DEPLOY_STAGING__WARN_DEPLOYERS__STEP_MOCK,
] as const satisfies StepIdentifier[];
const DEPLOY_PRODUCTION__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checking out', 'DEPLOY_PRODUCTION', ['ref', 'token']);
@@ -47,6 +49,7 @@ const DEPLOY_PRODUCTION__CREATE_RELEASE__STEP_MOCK = createMockStep(
[],
['GITHUB_TOKEN'],
);
+const DEPLOY_PRODUCTION__WARN_DEPLOYERS__STEP_MOCK = createMockStep('Warn deployers if production deploy failed', 'Warning deployers in slack for workflow failure', 'DEPLOY_STAGING');
const DEPLOY_PRODUCTION_STEP_MOCKS = [
DEPLOY_PRODUCTION__CHECKOUT__STEP_MOCK,
DEPLOY_PRODUCTION__SETUP_GIT__STEP_MOCK,
@@ -54,6 +57,7 @@ const DEPLOY_PRODUCTION_STEP_MOCKS = [
DEPLOY_PRODUCTION__RELEASE_PR_LIST__STEP_MOCK,
DEPLOY_PRODUCTION__GENERATE_RELEASE_BODY__STEP_MOCK,
DEPLOY_PRODUCTION__CREATE_RELEASE__STEP_MOCK,
+ DEPLOY_PRODUCTION__WARN_DEPLOYERS__STEP_MOCK,
] as const satisfies StepIdentifier[];
export default {DEPLOY_STAGING_STEP_MOCKS, DEPLOY_PRODUCTION_STEP_MOCKS};