- {% for article in section.articles %}
+ {% assign sortedArticles = section.articles | sort: 'order', 'last' | default: 999 %}
+ {% for article in sortedArticles %}
{% assign article_href = section.href | append: '/' | append: article.href %}
{% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %}
{% endfor %}
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/_sass/_main.scss b/docs/_sass/_main.scss
index 29b02d8aeb00..f3bf1035ed56 100644
--- a/docs/_sass/_main.scss
+++ b/docs/_sass/_main.scss
@@ -749,6 +749,7 @@ button {
width: 20px;
height: 20px;
cursor: pointer;
+ display: inline-block;
}
.homepage {
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md
index 2ff74760b376..0fd47f1341fa 100644
--- a/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md
@@ -7,29 +7,9 @@ description: International Reimbursements
If your company’s business bank account is in the US, Canada, the UK, Europe, or Australia, you now have the option to send direct reimbursements to nearly any country across the globe!
The process to enable global reimbursements is dependent on the currency of your reimbursement bank account, so be sure to review the corresponding instructions below.
-# How to request international reimbursements
-
-## The reimbursement account is in USD
-
-If your reimbursement bank account is in USD, the first step is connecting the bank account to Expensify.
-The individual who plans on sending reimbursements internationally should head to **Settings > Account > Payments > Add Verified Bank Account**. From there, you will provide company details, input personal information, and upload a copy of your ID.
-
-Once the USD bank account is verified (or if you already had a USD business bank account connected), click the support icon in your Expensify account to inform your Setup Specialist, Account Manager, or Concierge that you’d like to enable international reimbursements. From there, Expensify will ask you to confirm the currencies of the reimbursement and employee bank accounts.
-
-Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled.
-
-## The reimbursement account is in AUD, CAD, GBP, EUR
-
-To request international reimbursements, contact Expensify Support to make that request.
-
-You can do this by clicking on the support icon and informing your Setup Specialist, Account Manager, or Concierge that you’d like to set up global reimbursements on your account.
-From there, Expensify will ask you to confirm both the currencies of the reimbursement and employee bank accounts.
-
-Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled.
-
# How to verify the bank account for sending international payments
-Once international payments are enabled on your Expensify account, the next step is verifying the bank account to send the reimbursements.
+The steps for USD accounts and non-USD accounts differ slightly.
## The reimbursement account is in USD
@@ -38,9 +18,9 @@ First, confirm the workspace settings are set up correctly by doing the followin
2. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the reimbursement method to direct
3. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the USD bank account to the default account
-Once that’s all set, head to **Settings > Account > Payments**, and click **Enable Global Reimbursement** on the bank account (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account).
+Once that’s all set, head to **Settings > Account > Payments**, and click **Enable Global Reimbursement** on the bank account.
-From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required.
+From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. If additional information is required, our Support Team will contact you with more details.
## The reimbursement account is in AUD, CAD, GBP, EUR
@@ -53,7 +33,7 @@ Next, add the bank account to Expensify:
4. Enter the bank account details
5. Click **Save & Continue**
-From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required.
+From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. If additional information is required, our Support Team will contact you with more details.
# How to start reimbursing internationally
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.md
deleted file mode 100644
index 81dcf3488462..000000000000
--- a/docs/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.md
+++ /dev/null
@@ -1,113 +0,0 @@
----
-title: Pay Bills
-description: How to receive and pay company bills in Expensify
----
-
-
-# Overview
-Simplify your back office by receiving bills from vendors and suppliers in Expensify. Anyone with or without an Expensify account can send you a bill, and Expensify will file it as a Bill and help you issue the payment.
-
-# How to Receive Vendor or Supplier Bills in Expensify
-
-There are three ways to get a vendor or supplier bill into Expensify:
-
-**Option 1:** Have vendors send bills to Expensify directly: Ask your vendors to email all bills to your Expensify billing intake email.
-
-**Option 2:** Forward bills to Expensify: If your bills are emailed to you, you can forward those bills to your Expensify billing intake email yourself.
-
-**Option 3:** Manually upload bills to Expensify: If you receive physical bills, you can manually create a Bill in Expensify on the web from the Reports page:
-1. Click **New Report** and choose **Bill**
-2. Add the expense details and vendor's email address to the pop-up window
-3. Upload a pdf/image of the bill
-4. Click **Submit**
-
-# How to Pay Bills
-
-There are multiple ways to pay Bills in Expensify. Let’s go over each method below:
-
-## ACH bank-to-bank transfer
-
-To use this payment method, you must have a business bank account connected to your Expensify account.
-
-To pay with an ACH bank-to-bank transfer:
-
-1. Sign in to your Expensify account on the web at www.expensify.com.
-2. Go to the Inbox or Reports page and locate the Bill that needs to be paid.
-3. Click the **Pay** button to be redirected to the Bill.
-4. Choose the ACH option from the drop-down list.
-5. Follow the prompts to connect your business bank account to Expensify.
-
-**Fees:** None
-
-## Pay using a credit or debit card
-
-This option is available to all US and International customers receiving an bill from a US vendor with a US business bank account.
-
-To pay with a credit or debit card:
-1. Sign-in to your Expensify account on the web app at www.expensify.com.
-2, Click on the Bill you’d like to pay to see the details.
-3, Click the **Pay** button.
-4. You’ll be prompted to enter your credit card or debit card details.
-
-**Fees:** Includes 2.9% credit card payment fee
-
-## Venmo
-
-If both you and the vendor have Venmo setup in their Expensify account, you can opt to pay the bill through Venmo.
-
-**Fees:** Venmo charges a 3% sender’s fee
-
-## Pay Outside of Expensify
-
-If you are not able to pay using one of the above methods, then you can mark the Bill as paid manually in Expensify to update its status and indicate that you have made payment outside Expensify.
-
-To mark a Bill as paid outside of Expensify:
-
-1. Sign-in to your Expensify account on the web app at www.expensify.com.
-2. Click on the Bill you’d like to pay to see the details.
-3. Click on the **Reimburse** button.
-4. Choose **I’ll do it manually**
-
-**Fees:** None
-
-# Deep Dive: How company bills and vendor invoices are processed in Expensify
-
-Here is how a vendor or supplier bill goes from received to paid in Expensify:
-
-1. When a vendor or supplier bill is received in Expensify via, the document is SmartScanned automatically and a Bill is created. The Bill is owned by the primary domain contact, who will see the Bill on the Reports page on their default group policy.
-2. When the Bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow. Each time the Bill is approved, it is visible in the next approver's Inbox.
-3. The final approver pays the Bill from their Expensify account on the web via one of the methods.
-4. The Bill is coded with the relevant imported GL codes from a connected accounting software. After it has finished going through the approval workflow the Bill can be exported back to the accounting package connected to the policy.
-
-
-{% include faq-begin.md %}
-
-## What is my company's billing intake email?
-Your billing intake email is [yourdomain.com]@expensify.cash. Example, if your domain is `company.io` your billing email is `company.io@expensify.cash`.
-
-## When a vendor or supplier bill is sent to Expensify, who receives it?
-
-Bills are received by the Primary Contact for the domain. This is the email address listed at **Settings > Domains > Domain Admins**.
-
-## Who can view a Bill in Expensify?
-
-Only the primary contact of the domain can view a Bill.
-
-## Who can pay a Bill?
-
-Only the primary domain contact (owner of the bill) will be able to pay the Mill.
-
-## How can you share access to Bills?
-
-To give others the ability to view a Bill, the primary contact can manually “share” the Bill under the Details section of the report via the Sharing Options button.
-To give someone else the ability to pay Bills, the primary domain contact will need to grant those individuals Copilot access to the primary domain contact's account.
-
-## Is Bill Pay supported internationally?
-
-Payments are currently only supported for users paying in United States Dollars (USD).
-
-## What’s the difference between a Bill and an Invoice in Expensify?
-
-A Bill is a payable which represents an amount owed to a payee (usually a vendor or supplier), and is usually created from a vendor invoice. An Invoice is a receivable, and indicates an amount owed to you by someone else.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursements.md
deleted file mode 100644
index a31c0a582fd7..000000000000
--- a/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursements.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Overview
-
-If you want to know more about how and when you’ll be reimbursed through Expensify, we’ve answered your questions below.
-
-# How to Get Reimbursed
-
-To get paid back after submitting a report for reimbursement, you’ll want to be sure to connect your bank account. You can do that under **Settings** > **Account** > **Payments** > **Add a Deposit Account**. Once your employer has approved your report, the reimbursement will be paid into the account you added.
-
-# Deep Dive
-
-## Reimbursement Timing
-
-### US Bank Accounts
-
-If your company uses Expensify's ACH reimbursement we'll first check to see if the report is eligible for Rapid Reimbursement (next business day). For a report to be eligible for Rapid Reimbursement it must fall under two limits:
-
- - $100 per deposit bank account per day or less for the individuals being reimbursed or businesses receiving payments for bills.
- - Less than $10,000 being disbursed in a 24-hour time period from the verified bank account being used to pay the reimbursement.
-
-If the request passes both checks, then you can expect to see funds deposited into your bank account on the next business day.
-
-If either limit has been reached, then you can expect to see funds deposited within your bank account within the typical ACH timeframe of 3-5 business days.
-
-### International Bank Accounts
-
-If receiving reimbursement to an international deposit account via Global Reimbursement, you should expect to see funds deposited in your bank account within 4 business days.
-
-## Bank Processing Timeframes
-
-Banks only process transactions and ACH activity on weekdays that are not bank holidays. These are considered business days. Additionally, the business day on which a transaction will be processed depends upon whether or not a request is created before or after the cutoff time, which is typically 3 pm PST.
-For example, if your reimbursement is initiated at 4 pm on Wednesday, this is past the bank's cutoff time, and it will not begin processing until the next business day.
-If that same reimbursement starts processing on Thursday, and it's estimated to take 3-5 business days, this will cover a weekend, and both days are not considered business days. So, assuming there are no bank holidays added into this mix, here is how that reimbursement timeline would play out:
-
-**Wednesday**: Reimbursement initiated after 3 pm PST; will be processed the next business day by your company’s bank.
-**Thursday**: Your company's bank will begin processing the withdrawal request
-**Friday**: Business day 1
-**Saturday**: Weekend
-**Sunday**: Weekend
-**Monday**: Business day 2
-**Tuesday**: Business day 3
-**Wednesday**: Business day 4
-**Thursday**: Business day 5
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.md
deleted file mode 100644
index 69b39bae2874..000000000000
--- a/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-title: Reimbursing Reports
-description: How to reimburse employee expense reports
----
-# Overview
-
-One essential aspect of the Expensify workflow is the ability to reimburse reports. This process allows for the reimbursement of expenses that have been submitted for review to the person who made the request. Detailed explanations of the various methods for reimbursing reports within Expensify are provided below.
-
-# How to reimburse reports
-
-Reports can be reimbursed directly within Expensify by clicking the **Reimburse** button at the top of the report to reveal the available reimbursement options.
-
-## Direct Deposit
-
-To reimburse directly in Expensify, the following needs to be already configured:
-- The employee that's receiving reimbursement needs to add a deposit bank account to their Expensify account (under **Settings > Account > Payments > Add a Deposit-only Bank Account**)
-- The reimburser needs to add a business bank account to Expensify (under **Settings > Account > Payments > Add a Verified Business Bank Account**)
-- The reimburser needs to ensure Expensify is whitelisted to withdraw funds from the bank account
-
-If all of those settings are in place, to reimburse a report, you will click **Reimburse** on the report and then select **Via Direct Deposit (ACH)**.
-
-![Reimbursing Reports Dropdown]({{site.url}}/assets/images/Reimbursing Reports Dropdown.png){:width="100%"}
-
-## Indirect or Manual Reimbursement
-
-If you don't have the option to utilize direct reimbursement, you can choose to mark a report as reimbursed by clicking the **Reimburse** button at the top of the report and then selecting **I’ll do it manually – just mark as reimbursed**.
-
-This will effectively mark the report as reimbursed within Expensify, but you'll handle the payment elsewhere, outside of the platform.
-
-# Best Practices
-- Plan ahead! Consider sharing a business bank account with multiple workspace admins so they can reimburse employee reports if you're unavailable. We recommend having at least two workspace admins with reimbursement permissions.
-
-- Understand there is a verification process when sharing a business bank account. The new reimburser will need access to the business bank account’s transaction history (or access to someone who has access to it) to verify the set of test transactions sent from Expensify.
-
-- Get into the routine of having every new employee connect a deposit-only bank account to their Expensify account. This will ensure reimbursements happen in a timely manner.
-
-- Employees can see the expected date of their reimbursement at the top of and in the comments section of their report.
-
-# How to cancel a reimbursement
-
-Reimbursed a report by mistake? No worries! Any workspace admin with access to the same Verified Bank Account can cancel the reimbursement from within the report until it is withdrawn from the payment account.
-
-**Steps to Cancel an ACH Reimbursement:**
-1. On your web account, navigate to the Reports page
-2. Open the report
-3. Click **Cancel Reimbursement**
-4. After the prompt, "Are you sure you want to cancel the reimbursement?" click **Cancel Reimbursement**.
-
-It's important to note that there is a small window of time (roughly less than 24 hours) when a reimbursement can be canceled. If you don't see the **Cancel Reimbursement** button on a report, this means your bank has already begun withdrawing the funds from the reimbursement account and the withdrawal cannot be canceled.
-
-In that case, you’ll want to contact your bank directly to see if they can cancel the reimbursement on their end - or manage the return of funds directly with your employee, outside of Expensify.
-
-If you cancel a reimbursement after the withdrawal has started, it will be automatically returned to your Verified Bank Account within 3-5 business days.
-
-# Deep Dive
-
-## Rapid Reimbursement
-If your company uses Expensify's ACH reimbursement, we'll first check to see if the report is eligible for Rapid Reimbursement (next business day). For a report to be eligible for Rapid Reimbursement, it must fall under two limits:
-- $100 per deposit only bank account per day for the individuals being reimbursed or businesses receiving payments for bills
-- $10,000 per verified bank account for the company paying bills and reimbursing
-
-If neither limit is met, you can expect to see funds deposited into your bank account on the next business day.
-
-If either limit has been reached, you can expect funds deposited within your bank account within the typical ACH time frame of four to five business days.
-
-Rapid Reimbursement is not available for non-US-based reimbursement. If you are receiving a reimbursement to a non-US-based deposit account, you should expect to see the funds deposited in your bank account within four business days.
-
-{% include faq-begin.md %}
-
-## Who can reimburse reports?
-Only a workspace admin who has added a verified business bank account to their Expensify account can reimburse employees.
-
-## Why can’t I trigger direct ACH reimbursements in bulk?
-
-Instead of a bulk reimbursement option, you can set up automatic reimbursement. With this configured, reports below a certain threshold (defined by you) will be automatically reimbursed via ACH as soon as they're "final approved."
-
-To set your manual reimbursement threshold, head to **Settings > Workspace > Group > _[Workspace Name]_ > Reimbursement > Manual Reimbursement**.
-
-![Manual Reimbursement]({{site.url}}/assets/images/Reimbursing Manual.png){:width="100%"}
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Cancel-an-ACH-Reimbursement.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Cancel-an-ACH-Reimbursement.md
new file mode 100644
index 000000000000..ab75067b1a7f
--- /dev/null
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Cancel-an-ACH-Reimbursement.md
@@ -0,0 +1,17 @@
+---
+title: Cancel an ACH reimbursement
+description: Cancel an ACH payment after it has been sent
+---
+
+
+If a report was reimbursed with an ACH payment by mistake or otherwise needs to be canceled, a Workspace Admin with access to the verified bank account can cancel the reimbursement up until it is withdrawn from the payment account.
+
+To cancel an ACH reimbursement,
+
+1. Click the **Reports** tab.
+2. Open the report.
+3. Click **Cancel Reimbursement**.
+ - If you don’t see the Cancel Reimbursement button, this means your bank has already begun transferring the funds and it cannot be canceled. In this case, you’ll need to contact your bank for cancellation.
+4. Click **Cancel Reimbursement** to confirm cancellation.
+
+
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md
new file mode 100644
index 000000000000..00fb236e1763
--- /dev/null
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md
@@ -0,0 +1,36 @@
+---
+title: Receive payments
+description: Receive reimbursements from an employer
+---
+
+
+To get paid after submitting a report for reimbursement, you must first connect a personal U.S. bank account or a personal Australian bank account. Then once your employer approves your report or invoice, the reimbursement will be paid directly to your bank account.
+Funds for U.S. and global payments are generally deposited within a maximum of four to five business days: 2 days for the funds to be debited from the business bank account, and 2-3 business days for the ACH or wire to deposit into the employee account.
+
+However, banks only process ACH transactions before the daily cutoff (generally 3 p.m. PST) and only on business weekdays that are not bank holidays. This may affect when the payment is disbursed. If the payment qualifies for Rapid Reimbursement, you may receive the payment sooner.
+
+{% include info.html %}
+Companies also have the option to submit payments outside of Expensify via check, cash, or a third-party payment processor. Check with your Workspace Admin to know how you will be reimbursed.
+{% include end-info.html %}
+
+# Rapid Reimbursement (U.S. only)
+
+With Expensify’s ACH reimbursement, payments may be eligible for reimbursement by the next business day with Rapid Reimbursement if they meet the following qualifications:
+- **Deposit-only accounts**: Payment must not exceed $100
+- **Verified business bank accounts**: The account does not disburse more than $10,000 within a 24-hour time period.
+
+If the payment amount exceeds the limit, funds will be deposited within the typical ACH time frame of four to five business days.
+
+{% include faq-begin.md %}
+
+**Is there a way I can track my payment?**
+
+For U.S. ACH payments and global reimbursements, the expected date of reimbursement is provided at the top of the report and in the comments section of the report. Funds will be deposited within the typical ACH time frame of four to five business days unless the payment is eligible for Rapid Reimbursement.
+
+**For global payments, what currency is the payment provided in?**
+
+Global payments are reimbursed in the recipient's currency.
+
+{% include faq-end.md %}
+
+
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Australian-Reports.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Australian-Reports.md
new file mode 100644
index 000000000000..90a89ff3c75e
--- /dev/null
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Australian-Reports.md
@@ -0,0 +1,63 @@
+---
+title: Reimburse Australian reports
+description: Send payment for Australian expense reports
+---
+
+
+Workspace Admins can reimburse AUD expense reports by downloading an .aba file containing the accounts needing payment and uploading the file to the bank. This can be done for a single report or for a batch of payments at once.
+
+{% include info.html %}
+Your financial institution may require .aba files to include a self-balancing transaction. If you are unsure, check with your bank. Otherwise, the .aba file may not work with your bank’s internet banking platform.
+{% include end-info.html %}
+
+# Reimburse a single report
+
+1. Open the report, invoice, or bill from the email or Concierge notification, or from the **Reports** tab.
+2. Click the **Reimburse** dropdown and select **Via ABA File**.
+3. Click **Generate ABA and Mark as Reimbursed**.
+4. Click **Download**.
+5. Upload the .aba file to your bank. For additional guidance, use any of the following bank guides:
+ - [ANZ Bank](https://www.anz.com.au/support/internet-banking/pay-transfer-business/payroll/import-file/)
+ - [CommBank](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf)
+ - [Westpac](https://www.westpac.com.au/business-banking/online-banking/support-faqs/import-files/)
+ - [NAB](https://www.nab.com.au/business/online-banking/nab-connect/help)
+ - [Bendigo Bank](https://www.bendigobank.com.au/globalassets/documents/business/bulk-payments-user-guide.pdf)
+ - [Bank of Queensland](https://www.boq.com.au/help-and-support/online-banking/ob-faqs-and-support/faq-pfuf)
+
+# Send batch payments
+
+Once employees submit their expense reports, a Workspace Admin exports the reports (which contains the employees’ bank account information) and uploads the .aba file to the bank.
+
+## Step 1: Verify currency & reimbursement settings
+
+1. Hover over **Settings**, then click **Workspaces**.
+2. Select the desired workspace.
+3. Click the **Reports** tab on the left.
+4. Click the Report Currency dropdown and select **AUD A$**.
+5. Click the **Reimbursement** tab on the left.
+6. Verify that **Indirect** is selected as the Reimbursement type or select it if not.
+
+## Step 2: Download and upload the ABA file
+
+1. Click the **Reports** tab.
+2. Use the checkbox on the left to select all the reports needing payment.
+3. Click **Bulk Actions** and select **Reimburse via ABA**.
+5. Click **Generate ABA and Mark as Reimbursed**.
+6. Click **Download Report**.
+7. Upload the .aba file to your bank. For additional guidance, use any of the following bank guides:
+ - [ANZ Bank](https://www.anz.com.au/support/internet-banking/pay-transfer-business/payroll/import-file/)
+ - [CommBank](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf)
+ - [Westpac](https://www.westpac.com.au/business-banking/online-banking/support-faqs/import-files/)
+ - [NAB](https://www.nab.com.au/business/online-banking/nab-connect/help)
+ - [Bendigo Bank](https://www.bendigobank.com.au/globalassets/documents/business/bulk-payments-user-guide.pdf)
+ - [Bank of Queensland](https://www.boq.com.au/help-and-support/online-banking/ob-faqs-and-support/faq-pfuf)
+
+{% include faq-begin.md %}
+
+**Can I use direct deposit for an AUD bank account?**
+
+No, AUD bank accounts do not rely on direct deposit or ACH.
+
+{% include faq-end.md %}
+
+
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills.md
new file mode 100644
index 000000000000..b2cfbf833e13
--- /dev/null
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills.md
@@ -0,0 +1,108 @@
+---
+title: Reimburse reports, invoices, and bills
+description: Use direct deposit or indirect reimbursement to pay reports, invoices, and bills
+---
+
+
+Once a report, invoice, or bill has been submitted and approved for reimbursement, you can reimburse the expenses using direct deposit or an indirect reimbursement option outside of Expensify (like cash, a check, or a third-party payment processor).
+
+# Pay with direct deposit
+
+{% include info.html %}
+Before a report can be reimbursed with direct deposit, the employee or vendor receiving the reimbursement must connect their personal U.S. bank account, and the reimburser must connect a verified business bank account.
+
+Direct deposit is available for U.S. and global reimbursements. It is not available for Australian bank accounts. For Australian accounts, review the process for reimbursing Australian expenses.
+{% include end-info.html %}
+
+1. Open the report, invoice, or bill from the email or Concierge notification, or from the **Reports** tab.
+2. Click the **Reimburse** (for reports) or **Pay** (for bills and invoices) dropdown and select **Via Direct Deposit (ACH)**.
+3. Confirm that the correct VBA is selected or use the dropdown menu to select a different one.
+4. Click **Accept Terms & Pay**.
+
+The reimbursement is now queued in the daily batch.
+
+# Pay with indirect reimbursement
+
+When payments are submitted through Expensify, the report is automatically labeled as Reimbursed after it has been paid. However, if you are reimbursing reports via paper check, payroll, or any other method that takes place outside of Expensify, you’ll want to manually mark the bill as paid in Expensify to track the payment history.
+
+To label a report as Reimbursed after sending a payment outside of Expensify,
+
+1. Pay the report, invoice, or bill outside of Expensify.
+2. Open the report, invoice, or bill from the email or Concierge notification, or from the **Reports** tab.
+3. Click **Reimburse**.
+4. Select **I’ll do it manually - just mark it as reimbursed**. This changes the report status to Reimbursed.
+
+Once the recipient has received the payment, the submitter can return to the report and click **Confirm** at the top of the report. This will change the report status to Reimbursed: CONFIRMED.
+
+{% include faq-begin.md %}
+
+**Is there a maximum total report total?**
+
+Expensify cannot process a reimbursement for any single report over $20,000. If you have a report with expenses exceeding $20,000 we recommend splitting the expenses into multiple reports.
+
+**Why is my account locked?**
+
+When you reimburse a report, you authorize Expensify to withdraw the funds from your account and send them to the person requesting reimbursement. If your bank rejects Expensify’s withdrawal request, your verified bank account is locked until the issue is resolved.
+
+Withdrawal requests can be rejected if the bank account has not been enabled for direct debit or due to insufficient funds. If you need to enable direct debits from your verified bank account, your bank will require the following details:
+- The ACH CompanyIDs: 1270239450 and 4270239450
+- The ACH Originator Name: Expensify
+
+Once resolved, you can request to unlock the bank account by completing the following steps:
+
+1. Hover over **Settings**, then click **Account**.
+2. Click the **Payments** tab.
+3. Click **Bank Accounts**.
+4. Next to the bank account, click **Fix**.
+
+Our support team will review and process the request within 4-5 business days.
+
+**How are bills and invoices processed in Expensify?**
+
+Here is the process a vendor or supplier bill goes through from receipt to payment:
+
+1. A vendor or supplier bill is received in Expensify.
+2. Automatically, the document is SmartScanned and a bill is created for the primary domain contact. The bill will appear under the Reports tab on their default group policy.
+3. When the bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow until the bill has been fully approved.
+4. The final approver pays the bill from their Expensify account using one of the methods outlined in the article above.
+5. If the workspace is connected to an accounting integration, the bill is automatically coded with the relevant imported GL codes and can be exported back to the accounting software.
+
+**When a vendor or supplier bill is sent to Expensify, who receives it?**
+
+Bills are sent to the primary contact for the domain. They’ll see a notification from Concierge on their Home page, and they’ll also receive an email.
+
+**How can I share access to bills?**
+
+By default, only the primary contact for the domain can view and pay the bill. However, you can allow someone else to view or pay bills.
+
+- **To allow someone to view a bill**: The primary contact can manually share the bill with others to allow them to view it.
+ 1. Click the **Reports** tab.
+ 2. Click the report.
+ 3. Click **Details** in the top right.
+ 4. Click the **Add Person** icon.
+ 5. Enter the email address or phone number of the person you will share the report with.
+ 6. Enter a message, if desired.
+ 7. Click **Share Report**.
+
+- **To allow someone to pay bills**: The primary domain contact can allow others to pay bills on their behalf by [assigning those individuals as Copilots](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot).
+
+**Is Bill Pay supported internationally?**
+
+Payments are currently only supported for users paying in United States Dollars (USD).
+
+**What’s the difference between a bill and an invoice?**
+
+- A **bill** is a payable that represents an amount owed to a payee (usually a vendor or supplier), and it is usually created from a vendor invoice.
+- An **invoice** is a receivable that indicates an amount owed to you by someone else.
+
+**Who can reimburse reports?**
+
+Only a Workspace Admin who has added a verified business bank account to their Expensify account can reimburse employee reports.
+
+**Why can’t I trigger direct ACH reimbursements in bulk?**
+
+Expensify does not offer bulk reimbursement, but you can set up automatic reimbursement to automatically reimburse approved reports via ACH that do not exceed the threshold that you define.
+
+{% include faq-end.md %}
+
+
diff --git a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md
index 05366a91d9fa..f94e692f5e56 100644
--- a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md
+++ b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md
@@ -8,6 +8,15 @@ Whether you're encountering issues related to company cards, require assistance
## How to add company cards to Expensify
You can add company credit cards under the Domain settings in your Expensify account by navigating to *Settings* > *Domain* > _Domain Name_ > *Company Cards* and clicking *Import Card/Bank* and following the prompts.
+## To Locate Missing Card Transactions in Expensify
+1. **Wait for Posting**: Bank transactions may take up to 24 hours to import into Expensify after they have "posted" at your bank. Ensure sufficient time has passed for transactions to appear.
+2. **Update Company Cards**: Go to Settings > Domains > Company Cards. Click on the card in question and click "Update" to refresh the card feed.
+3. **Reconcile Cards**: Navigate to the Reconciliation section under Settings > Domains > Company Cards. Refer to the detailed guide on how to use the [Reconciliation Dashboard](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation#identifying-outstanding-unapproved-expenses-using-the-reconciliation-dashboard).
+4. **Review Transactions**: Use the Reconciliation Dashboard to view all transactions within a specific timeframe. Transactions will display on the Expenses page based on their "Posted Date". If needed, uncheck the "use posted date" checkbox near the filters to view transactions based on their "Transaction Date" instead.
+5. **Address Gaps**: If there is a significant gap in transactions or if transactions are still missing, contact Expensify's Concierge or your Account Manager. They can initiate a historical data update on your card feed to ensure all transactions are properly imported.
+
+Following these steps should help you identify and resolve any issues with missing card transactions in Expensify.
+
## Known issues importing transactions
The first step should always be to "Update" your card, either from Settings > Your Account > Credit Card Import or Settings > Domain > [Domain Name] > Company Cards for centrally managed cards. If a "Fix" or "Fix card" option appears, follow the steps to fix the connection. If this fails to import your missing transactions, there is a known issue whereby some transactions will not import for certain API-based company card connections. So far this has been reported on American Express, Chase and Wells Fargo. This can be temporarily resolved by creating the expenses manually instead:
@@ -63,6 +72,24 @@ If you've answered "yes" to any of these questions, a Domain Admins need to upda
Make sure you're importing your card in the correct spot in Expensify and selecting the right bank connection. For company cards, use the master administrative credentials to import your set of cards at *Settings* > *Domains* > _Domain Name_ > *Company Cards* > *Import Card*.
Please note there are some things that cannot be bypassed within Expensify, including two-factor authentication being enabled within your bank account. This will prevent the connection from remaining stable and will need to be turned off on the bank side.
+## Why Can’t I See the Transactions Before a Certain Date?
+When importing a card into Expensify, the platform typically retrieves 30-90 days of historical transactions, depending on the card or account type. For commercial feeds, transactions cannot be imported before the bank starts sending data. If needed, banks can send backdated files, and Expensify can run a historical update upon request.
+
+Additionally, Expensify does not import transactions dated before the "start date" you specify when assigning the card. Unless transitioning from an old card to a new one to avoid duplicates, it's advisable to set the start date to "earliest possible" or leave it blank.
+
+For historical expenses that cannot be imported automatically, consider using Expensify's [company card](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import) or [personal card](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards#importing-expenses-via-a-spreadsheet) spreadsheet import method. This allows you to manually input missing transactions into the system.
+
+## Why Am I / Why Is My Employee Seeing Duplicates?
+If an employee is seeing duplicate expenses, they may have accidentally imported the card as a personal credit card as well as having the Domain Admin assign them a company card.
+
+To troubleshoot:
+- Have the employee navigate to their Settings > Your Account > Credit Card Import and confirm that their card is only listed once.
+- If the card is listed twice, delete the entry without the "padlock" icon.
+
+**Important:** Deleting a duplicate card will delete all unapproved expenses from that transaction feed. Transactions associated with the remaining card will not be affected. If receipts were attached to those transactions, they will still be on the Expenses page, and the employee can click to SmartScan them again.
+
+Duplicate expenses might also occur if you recently unassigned and reassigned a company card with an overlapping start date. If this is the case and expenses on the “new” copy have not been submitted, you can unassign the card again and reassign it with a more appropriate start date. This action will delete all unsubmitted expenses from the new card feed.
+
## What are the most reliable bank connections in Expensify?*
All bank connections listed below are extremely reliable, but we recommend transacting with the Expensify Visa® Commercial Card. It also offers daily and monthly settlement, unapproved expense limits, realtime compliance for secure and efficient spending, and cash back on all US purchases. [Click here to learn more about the Expensify Card](https://use.expensify.com/company-credit-card).
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md b/docs/articles/expensify-classic/connections/ADP.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/HR-integrations/ADP.md
rename to docs/articles/expensify-classic/connections/ADP.md
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md b/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md
rename to docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md b/docs/articles/expensify-classic/connections/Egencia.md
similarity index 75%
rename from docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md
rename to docs/articles/expensify-classic/connections/Egencia.md
index 178621a62d90..35d232d2df67 100644
--- a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md
+++ b/docs/articles/expensify-classic/connections/Egencia.md
@@ -24,7 +24,7 @@ Egencia controls the feed, so to connect Expensify you will need to:
# How to Connect to a Central Purchasing Account
Once your Egencia account manager has established the feed, you can automatically forward all Egencia booking receipts to a single Expensify account. To do this:
1. Open a chat with Concierge.
-2. Tell Concierge “Please enable Central Purchasing Account for our Egencia feed. The account email is: xxx@yourdomain.com”.
+2. Tell Concierge the address of your central purchasing account, “Please enable Central Purchasing Account for our Egencia feed. The account email is: xxx@yourdomain.com”.
-The receipt the traveler receives is a "reservation expense." Reservation expenses are non-reimbursable and won’t be included in any integrated accounting system exports. The reservation sent to the traveler's account is added to their mobile app Trips feature so that the traveler can easily keep tabs on upcoming travel and receive trip notifications.
+A receipt will be sent to both the traveler and the central account. The receipt sent to the traveler is a "reservation expense." Reservation expenses are non-reimbursable and won’t be included in any integrated accounting system exports.
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Global-VaTax.md b/docs/articles/expensify-classic/connections/Global-VaTax.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/travel-integrations/Global-VaTax.md
rename to docs/articles/expensify-classic/connections/Global-VaTax.md
diff --git a/docs/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO.md b/docs/articles/expensify-classic/connections/Google-Apps-SSO.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO.md
rename to docs/articles/expensify-classic/connections/Google-Apps-SSO.md
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Greenhouse.md b/docs/articles/expensify-classic/connections/Greenhouse.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/HR-integrations/Greenhouse.md
rename to docs/articles/expensify-classic/connections/Greenhouse.md
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md b/docs/articles/expensify-classic/connections/Gusto.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md
rename to docs/articles/expensify-classic/connections/Gusto.md
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md b/docs/articles/expensify-classic/connections/Indirect-Accounting-Integrations.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md
rename to docs/articles/expensify-classic/connections/Indirect-Accounting-Integrations.md
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Lyft.md b/docs/articles/expensify-classic/connections/Lyft.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/travel-integrations/Lyft.md
rename to docs/articles/expensify-classic/connections/Lyft.md
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md b/docs/articles/expensify-classic/connections/Navan.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/travel-integrations/Navan.md
rename to docs/articles/expensify-classic/connections/Navan.md
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time.md b/docs/articles/expensify-classic/connections/QuickBooks-Time.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time.md
rename to docs/articles/expensify-classic/connections/QuickBooks-Time.md
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Rippling.md b/docs/articles/expensify-classic/connections/Rippling.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/HR-integrations/Rippling.md
rename to docs/articles/expensify-classic/connections/Rippling.md
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/TravelPerk.md b/docs/articles/expensify-classic/connections/TravelPerk.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/travel-integrations/TravelPerk.md
rename to docs/articles/expensify-classic/connections/TravelPerk.md
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/connections/Uber.md
similarity index 90%
rename from docs/articles/expensify-classic/integrations/travel-integrations/Uber.md
rename to docs/articles/expensify-classic/connections/Uber.md
index 16da0c0caa5b..213a90b6d288 100644
--- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md
+++ b/docs/articles/expensify-classic/connections/Uber.md
@@ -22,3 +22,5 @@ Now, every time you use Uber for Business – be it for rides or meals – the r
![Uber integration set up steps: Connecting your account](https://help.expensify.com/assets/images/Uber1.png){:width="100%"}
![Uber integration set up steps: Selecting Expensify](https://help.expensify.com/assets/images/Uber2.png){:width="100%"}
+
+To disconnect Uber and Expensify, simply follow the above path and select Disconnect on the Expensify option.
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Workday.md b/docs/articles/expensify-classic/connections/Workday.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/HR-integrations/Workday.md
rename to docs/articles/expensify-classic/connections/Workday.md
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Zenefits.md b/docs/articles/expensify-classic/connections/Zenefits.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/HR-integrations/Zenefits.md
rename to docs/articles/expensify-classic/connections/Zenefits.md
diff --git a/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md b/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md
new file mode 100644
index 000000000000..fd0a6ca59069
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md
@@ -0,0 +1,7 @@
+---
+title: Accelo Troubleshooting
+description: Accelo Troubleshooting
+order: 3
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/connections/accelo/Configure-Accelo.md b/docs/articles/expensify-classic/connections/accelo/Configure-Accelo.md
new file mode 100644
index 000000000000..abb14767b196
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/accelo/Configure-Accelo.md
@@ -0,0 +1,7 @@
+---
+title: Configure Accelo
+description: Configure Accelo
+order: 2
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Accelo.md b/docs/articles/expensify-classic/connections/accelo/Connect-To-Accelo.md
similarity index 99%
rename from docs/articles/expensify-classic/integrations/accounting-integrations/Accelo.md
rename to docs/articles/expensify-classic/connections/accelo/Connect-To-Accelo.md
index fffe0abb43aa..96b0a17a1528 100644
--- a/docs/articles/expensify-classic/integrations/accounting-integrations/Accelo.md
+++ b/docs/articles/expensify-classic/connections/accelo/Connect-To-Accelo.md
@@ -1,6 +1,7 @@
---
title: Accelo
description: Help doc for Accelo integration
+order: 1
---
diff --git a/docs/articles/expensify-classic/connections/certinia/Certinia-Troubleshooting.md b/docs/articles/expensify-classic/connections/certinia/Certinia-Troubleshooting.md
new file mode 100644
index 000000000000..82a2762ee99a
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/certinia/Certinia-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Certinia Troubleshooting
+description: Certinia Troubleshooting
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/connections/certinia/Configure-Certinia.md b/docs/articles/expensify-classic/connections/certinia/Configure-Certinia.md
new file mode 100644
index 000000000000..dc7398c11888
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/certinia/Configure-Certinia.md
@@ -0,0 +1,7 @@
+---
+title: Configure Certinia
+description: Configure Certinia
+order: 2
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md b/docs/articles/expensify-classic/connections/certinia/Connect-To-Certinia.md
similarity index 99%
rename from docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md
rename to docs/articles/expensify-classic/connections/certinia/Connect-To-Certinia.md
index 6c7014827ea6..5e5174a336b3 100644
--- a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md
+++ b/docs/articles/expensify-classic/connections/certinia/Connect-To-Certinia.md
@@ -1,6 +1,7 @@
---
title: Certinia
description: Guide to connecting Expensify and Certinia FFA and PSA/SRP (formerly known as FinancialForce)
+order: 1
---
# Overview
[Cetinia](https://use.expensify.com/financialforce) (formerly known as FinancialForce) is a cloud-based software solution that provides a range of financial management and accounting applications built on the Salesforce platform. There are two versions: PSA/SRP and FFA and we support both.
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
similarity index 73%
rename from docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md
rename to docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
index ee116f65a398..0650cf5b516f 100644
--- a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md
+++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
@@ -1,136 +1,14 @@
---
-title: NetSuite
-description: Connect and configure NetSuite directly to Expensify.
+title: Configure Netsuite
+description: Configure NetSuite's export, coding, and advanced settings.
---
-# Overview
-Expensify's seamless integration with NetSuite enables you to streamline your expense reporting process. This integration allows you to automate the export of reports, tailor your coding preferences, and tap into NetSuite's array of advanced features. By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient.
-Before getting started with connecting NetSuite to Expensify, there are a few things to note:
-- Token-based authentication works by ensuring that each request to NetSuite is accompanied by a signed token which is verified for authenticity
-- You must be able to login to NetSuite as an administrator to initiate the connection
-- You must have a Control Plan in Expensify to integrate with NetSuite
-- Employees don’t need NetSuite access or a NetSuite license to submit expense reports since the connection is managed by the Workspace Admin
-- Each NetSuite subsidiary will need its own Expensify Group Workspace
-- Ensure that your workspace's report output currency setting matches the NetSuite Subsidiary default currency
-- Make sure your page size is set to 1000 for importing your customers and vendors. Go to Setup > Integration > Web Services Preferences > 'Search Page Size'
+By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient.
-# How to Connect to NetSuite
-
-## Step 1: Install the Expensify Bundle in NetSuite
-
-1. While logged into NetSuite as an administrator, go to Customization > SuiteBundler > Search & Install Bundles, then search for "Expensify"
-2. Click on the Expensify Connect bundle (Bundle ID 283395)
-3. Click Install
-4. If you already have the Expensify Connect bundle installed, head to _Customization > SuiteBundler > Search & Install Bundles > List_ and update it to the latest version
-5. Select **Show on Existing Custom Forms** for all available fields
-
-## Step 2: Enable Token-Based Authentication
-
-1. Head to _Setup > Company > Enable Features > SuiteCloud > Manage Authentication_
-2. Make sure “Token Based Authentication” is enabled
-3. Click **Save**
-
-## Step 3: Add Expensify Integration Role to a User
-
-The user you select must have access to at least the permissions included in the Expensify Integration Role, but they're not required to be an Admin.
-1. In NetSuite, head to Lists > Employees, and find the user you want to add the Expensify Integration role to
-2. Click _Edit > Access_, then find the Expensify Integration role in the dropdown and add it to the user
-3. Click **Save**
-
-Remember that Tokens are linked to a User and a Role, not solely to a User. It's important to note that you cannot establish a connection with tokens using one role and then switch to another role afterward. Once you've initiated a connection with tokens, you must continue using the same token/user/role combination for all subsequent sync or export actions.
-
-## Step 4: Create Access Tokens
-
-1. Using Global Search in NetSuite, enter “page: tokens”
-2. Click **New Access Token**
-3. Select Expensify as the application (this must be the original Expensify integration from the bundle)
-4. Select the role Expensify Integration
-5. Press **Save**
-6. Copy and Paste the token and token ID to a saved location on your computer (this is the only time you will see these details)
-
-## Step 5: Confirm Expense Reports are Enabled in NetSuite.
-
-Enabling Expense Reports is required as part of Expensify's integration with NetSuite:
-1. Logged into NetSuite as an administrator, go to Setup > Company > Enable Features > Employees
-2. Confirm the checkbox next to Expense Reports is checked
-3. If not, click the checkbox and then Save to enable Expense Reports
-
-## Step 6: Confirm Expense Categories are set up in NetSuite.
-
-Once Expense Reports are enabled, Expense Categories can be set up in NetSuite. Expense Categories are an alias for General Ledger accounts for coding expenses.
-
-1. Logged into NetSuite as an administrator, go to Setup > Accounting > Expense Categories (a list of Expense Categories should show)
-2. If no Expense Categories are visible, click **New** to create new ones
-
-## Step 7: Confirm Journal Entry Transaction Forms are Configured Properly
-
-1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
-2. Click **Customize** or **Edit** next to the Standard Journal Entry form
-3. Then, click Screen Fields > Main. Please verify the "Created From" label has "Show" checked and the Display Type is set to Normal
-4. Click the sub-header Lines and verify that the "Show" column for "Receipt URL" is checked
-5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the journal type have this same configuration
-
-## Step 8: Confirm Expense Report Transaction Forms are Configured Properly
-
-1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
-2. Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main**
-3. Verify the "Created From" label has "Show" checked and the Display Type is set to Normal
-4. Click the second sub-header, Expenses, and verify that the 'Show' column for 'Receipt URL' is checked
-5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the expense report type have this same configuration
-
-## Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly
-
-1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
-2. Click **Customize** or **Edit** next to your preferred Vendor Bill form
-3. Then, click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked
-4. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class
-5. Go to _Customization > Forms > Transaction Forms_ and provide all other transaction forms with the vendor bill type have this same configuration
-
-## Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly
-
-1. While logged in as an administrator, go to _Customization > Forms > Transaction Forms_
-2. Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked
-3. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class
-4. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the vendor credit type have this same configuration
-
-## Step 11: Set up Tax Groups (only applicable if tracking taxes)
-
-Expensify imports NetSuite Tax Groups (not Tax Codes), which you can find in NetSuite under _Setup > Accounting > Tax Groups_.
-
-Tax Groups are an alias for Tax Codes in NetSuite and can contain one or more Tax Codes (Please note: for UK and Ireland subsidiaries, please ensure your Tax Groups do not have more than one Tax Code). We recommend naming Tax Groups so your employees can easily understand them, as the name and rate will be displayed in Expensify.
-
-Before importing NetSuite Tax Groups into Expensify:
-1. Create your Tax Groups in NetSuite by going to _Setup > Accounting > Tax Groups_
-2. Click **New**
-3. Select the country for your Tax Group
-4. Enter the Tax Name (this is what employees will see in Expensify)
-5. Select the subsidiary for this Tax Group
-6. Select the Tax Code from the table you wish to include in this Tax Group
-7. Click **Add**
-8. Click **Save**
-9. Create one NetSuite Tax Group for each tax rate you want to show in Expensify
-
-Ensure Tax Groups can be applied to expenses by going to _Setup > Accounting > Set Up Taxes_ and setting the Tax Code Lists Include preference to "Tax Groups And Tax Codes" or "Tax Groups Only."
-
-If this field does not display, it’s not needed for that specific country.
-
-## Step 12: Connect Expensify and NetSuite
-
-1. Log into Expensify as a Policy Admin and go to **Settings > Workspaces > _[Workspace Name]_ > Connections > NetSuite**
-2. Click **Connect to NetSuite**
-3. Enter your Account ID (Account ID can be found in NetSuite by going to _Setup > Integration > Web Services Preferences_)
-4. Then, enter the token and token secret
-5. Click **Connect to NetSuite**
-
-From there, the NetSuite connection will sync, and the configuration dialogue box will appear.
-
-Please note that you must create the connection using a NetSuite account with the Expensify Integration role
-
-Once connected, all reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration).
-
-# How to Configure Export Settings
+# Configure Export Settings
There are numerous options for exporting Expensify reports to NetSuite. Let's explore how to configure these settings to align with your business needs.
+
To access these settings, head to **Settings > Workspace > Group > Connections** and select the **Configure** button.
## Export Options
@@ -206,9 +84,9 @@ Select the Accounts Receivable account you want your Invoice Reports to export.
### Default Vendor Bills
-The list of vendors will be available in the dropdown when selecting the option to export non-reimbursable expenses as vendor bills.
+When selecting the option to export non-reimbursable expenses as vendor bills, the list of vendors will be available in the dropdown menu.
-# How to Configure Coding Settings
+# Configure Coding Settings
The Coding tab is where NetSuite information is configured in Expensify, which allows employees to code expenses and reports accurately. There are several coding options in NetSuite. Let’s go over each of those below.
@@ -313,7 +191,7 @@ If configuring Custom Segments as Report Fields, use the Field ID on the Transac
If configuring Custom Segments as Tags, use the Field ID on the Transaction Columns tab (under _Custom Segments > Transaction Columns_).
Lastly, head over to Expensify and do the following:
-1. Navigate to **Settings > Workspace > Group > [Workspace Name]_ > Connections > Configure > Coding tab**
+1. Navigate to **Settings > Workspace > Group > [Workspace Name] > Connections > Configure > Coding tab**
2. Choose how to import Custom Records (Report Fields or Tags)
3. Fill out the three fields (the name or label of the record, Internal ID, Transaction Column ID)
4. Click **Submit**
@@ -328,7 +206,7 @@ To add Custom Lists to your workspace, you’ll need to locate two fields in Net
**To find the record:**
1. Log into Expensify
-2. Head to **Settings > Workspace > Group > _[Workspace Name]_ > Connections > Configure > Coding tab**
+2. Head to **Settings > Workspace > Group > [Workspace Name] > Connections > Configure > Coding tab**
3. The name of the record will be populated in a dropdown list
The name of the record will populate in a dropdown list. If you don't see the one you are looking for, click **Refresh Custom List Options**.
@@ -339,14 +217,14 @@ The name of the record will populate in a dropdown list. If you don't see the on
3. Open the option that is holding the record to get the ID
Lastly, head over to Expensify, and do the following:
-1. Navigate to **Settings > Workspaces > Group > _[Workspace Name]_ > Connections > Configure > Coding tab**
+1. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Connections > Configure > Coding tab**
2. Choose how to import Custom Lists (Report Fields or Tags)
3. Enter the ID in Expensify in the configuration screen
4. Click **Submit**
From there, you should see the values for the Custom Lists under the Tag or Report Field settings in Expensify.
-# How to Configure Advanced Settings
+# Configure Advanced Settings
The NetSuite integration’s advanced configuration settings are accessed under **Settings > Workspaces > Group > _[Workspace Name]_ > Connections > NetSuite > Configure > Advanced tab**.
@@ -384,13 +262,13 @@ Besides inviting employees, you can also establish an approval process in NetSui
By doing this, the Approval Workflow in Expensify will automatically follow the same rules as NetSuite, typically starting with Manager Approval.
-- **Basic Approval:** A single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page.
+- **Basic Approval:** This is a single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page.
- **Manager Approval (default):** Two levels of approval route reports first to an employee's NetSuite expense approver or supervisor, and second to a workspace-wide Final Approver. By NetSuite convention, Expensify will map to the supervisor if no expense approver exists. The Final Approver defaults to the workspace owner but can be edited on the people page.
- **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured on the workspace's People settings page. If you enable this setting, it’s recommended you review the newly imported employees and managers on the **Settings > Workspaces > Group > _[Workspace Name]_ > People page**. You can set a user role for each new employee and enforce an approval workflow.
## Automatically Create Employees/Vendors
-With this feature enabled, Expensify will automatically create a new employee or vendor (if one doesn’t already exist) from the email of the report submitter in NetSuite.
+With this feature enabled, Expensify will automatically create a new employee or vendor (if one doesn’t already exist) from the report submitter's email in NetSuite.
## Export Foreign Currency Amount
@@ -430,13 +308,12 @@ If you do not wish to use Approval Routing in NetSuite, go to _Setup > Accountin
When exporting invoices, once marked as Paid, the payment is marked against the account selected after enabling the Collection Account setting.
-# Deep Dive
-
+# Additional Settings
## Categories
You can use the Auto-Categorization feature so that expenses are automatically categorized.
-To set Category Rules (e.g., receipt requirements or comments), go to the categories page in the workspace under **Settings > Workspaces > _[Workspace Name]_ > Categories**.
+To set Category Rules (e.g., receipt requirements or comments), go to the categories page in the workspace under **Settings > Workspaces > [Workspace Name] > Categories**.
With this setting enabled, when an Expense Category updates in NetSuite, it will update in Expensify automatically.
@@ -448,7 +325,7 @@ NetSuite's company card feature simplifies exporting reimbursable and non-reimbu
2. **Default Accounts Payable (A/P) Account:** Expense reports enable you to set a default A/P account for export on your subsidiary record. Unlike vendor bills, where the A/P account defaults to the last selected account, the expense report export option allows you to establish a default A/P account.
3. **Mix Reimbursable and Non-Reimbursable Expenses:** You can freely mix reimbursable and non-reimbursable expenses without categorizing them in NetSuite after export. NetSuite's corporate card feature automatically categorizes expenses into the correct GL accounts, ensuring a neat and organized GL impact.
-#### Let’s go over an example!
+**Let’s go over an example!**
Consider an expense report with one reimbursable and one non-reimbursable expense. Each needs to be exported to different accounts and expense categories.
@@ -456,7 +333,7 @@ In NetSuite, you can quickly identify the non-reimbursable expense marked as a c
Furthermore, each expense is categorized according to your selected expense category.
-You'll need to set up default corporate cards in NetSuite to use the expense report option for your corporate card expenses.
+To use the expense report option for your corporate card expenses, you'll need to set up default corporate cards in NetSuite.
For non-reimbursable expenses, choose the appropriate card on the subsidiary record. You can find the default in your accounting preferences if you're not using a OneWorld account.
@@ -545,7 +422,7 @@ When exporting to NetSuite, we match the recipient's email address on the invoic
Once exported, the invoice will appear in the Accounts Receivable account you selected during your NetSuite Export configuration.
-### Updating an Invoice to paid
+### Updating the status of an invoice to "paid"
When you mark an invoice as "Paid" in Expensify, this status will automatically update in NetSuite. Similarly, if the invoice is marked as "Paid" in NetSuite, it will sync with Expensify. The payment will be reflected in the Collection account specified in your Advanced Settings Configuration.
@@ -562,10 +439,6 @@ Send these two files to your Account Manager or Concierge so we can continue tro
{% include faq-begin.md %}
-## What type of Expensify plan is required for connecting to NetSuite?
-
-You need a group workspace on a Control Plan to integrate with NetSuite.
-
## How does Auto Sync work with reimbursed reports?
If a report is reimbursed via ACH or marked as reimbursed in Expensify and then exported to NetSuite, the report is automatically marked as paid in NetSuite during the next sync.
@@ -574,6 +447,24 @@ If a report is exported to NetSuite and then marked as paid in NetSuite, the rep
## If I enable Auto Sync, what happens to existing approved and reimbursed reports?
-If you previously had Auto Sync disabled but want to allow that feature to be used going forward, you can safely turn on Auto Sync without affecting existing reports. Auto Sync will only take effect for reports created after enabling that feature.
+If you previously had Auto Sync disabled but want to allow that feature to be used going forward, you can safely turn it on without affecting existing reports. Auto Sync will only take effect for reports created after enabling that feature.
+
+## Why are some of my customers not importing from NetSuite?
+
+If only part of your customer list is importing from NetSuite to Expensify, ensure your page size is set to 1000 for importing customers and vendors:
+1. Navigate to **Setup > Integration > Web Services Preferences > Search Page Size**
+2. Adjust this setting to 1000
+3. Sync your connection again under **Settings > Workspaces > Group > Workspace Name > Connections**
+
+Additionally, ensure the "Company Name" field is completed for each customer profile; otherwise, they won't import into the Group Workspace.
+
+## Why aren't all my Categories pulling into Expensify from NetSuite?
+
+If you're having trouble importing your Categories, you'll want to start by checking that they are set up in NetSuite as actual Expense Categories, not General Ledger accounts:
+- Log into NetSuite as an administrator and go to **Setup > Accounting > Expense Categories**
+- A list of Expense Categories should be available
+- If no Expense Categories are visible click on "New" to create new Expense Categories
+
+If you have confirmed that your categories are set as Expense Categories in NetSuite and they still aren't importing to Expensify, make sure that the subsidiary of the Expense Category matches the subsidiary selected in your connection settings.
{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md b/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md
new file mode 100644
index 000000000000..1f96d9b8a633
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md
@@ -0,0 +1,164 @@
+---
+title: NetSuite
+description: Set up the direct connection from Expensify to NetSuite.
+order: 1
+---
+# Overview
+Expensify's integration with NetSuite allows you to automate report exports, tailor your coding preferences, and tap into NetSuite's array of advanced features. By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient.
+
+**Before connecting NetSuite to Expensify, a few things to note:**
+- Token-based authentication works by ensuring that each request to NetSuite is accompanied by a signed token which is verified for authenticity
+- You must be able to login to NetSuite as an administrator to initiate the connection
+- You must have a Control Plan in Expensify to integrate with NetSuite
+- Employees don’t need NetSuite access or a NetSuite license to submit expense reports since the connection is managed by the Workspace Admin
+- Each NetSuite subsidiary will need its own Expensify Group Workspace
+- Ensure that your workspace's report output currency setting matches the NetSuite Subsidiary default currency
+- Make sure your page size is set to 1000 for importing your customers and vendors. You can check this in NetSuite under **Setup > Integration > Web Services Preferences > 'Search Page Size'**
+
+# Connect to NetSuite
+
+## Step 1: Install the Expensify Bundle in NetSuite
+
+1. While logged into NetSuite as an administrator, go to Customization > SuiteBundler > Search & Install Bundles, then search for "Expensify"
+2. Click on the Expensify Connect bundle (Bundle ID 283395)
+3. Click Install
+4. If you already have the Expensify Connect bundle installed, head to _Customization > SuiteBundler > Search & Install Bundles > List_ and update it to the latest version
+5. Select **Show on Existing Custom Forms** for all available fields
+
+## Step 2: Enable Token-Based Authentication
+
+1. Head to _Setup > Company > Enable Features > SuiteCloud > Manage Authentication_
+2. Make sure “Token Based Authentication” is enabled
+3. Click **Save**
+
+## Step 3: Add Expensify Integration Role to a User
+
+The user you select must have access to at least the permissions included in the Expensify Integration Role, but they're not required to be an Admin.
+1. In NetSuite, head to Lists > Employees, and find the user you want to add the Expensify Integration role to
+2. Click _Edit > Access_, then find the Expensify Integration role in the dropdown and add it to the user
+3. Click **Save**
+
+Remember that Tokens are linked to a User and a Role, not solely to a User. It's important to note that you cannot establish a connection with tokens using one role and then switch to another role afterward. Once you've initiated a connection with tokens, you must continue using the same token/user/role combination for all subsequent sync or export actions.
+
+## Step 4: Create Access Tokens
+
+1. Using Global Search in NetSuite, enter “page: tokens”
+2. Click **New Access Token**
+3. Select Expensify as the application (this must be the original Expensify integration from the bundle)
+4. Select the role Expensify Integration
+5. Press **Save**
+6. Copy and Paste the token and token ID to a saved location on your computer (this is the only time you will see these details)
+
+## Step 5: Confirm Expense Reports are Enabled in NetSuite.
+
+Enabling Expense Reports is required as part of Expensify's integration with NetSuite:
+1. Logged into NetSuite as an administrator, go to Setup > Company > Enable Features > Employees
+2. Confirm the checkbox next to Expense Reports is checked
+3. If not, click the checkbox and then Save to enable Expense Reports
+
+## Step 6: Confirm Expense Categories are set up in NetSuite.
+
+Once Expense Reports are enabled, Expense Categories can be set up in NetSuite. Expense Categories are an alias for General Ledger accounts used to code expenses.
+
+1. Logged into NetSuite as an administrator, go to Setup > Accounting > Expense Categories (a list of Expense Categories should show)
+2. If no Expense Categories are visible, click **New** to create new ones
+
+## Step 7: Confirm Journal Entry Transaction Forms are Configured Properly
+
+1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
+2. Click **Customize** or **Edit** next to the Standard Journal Entry form
+3. Then, click Screen Fields > Main. Please verify the "Created From" label has "Show" checked and the Display Type is set to Normal
+4. Click the sub-header Lines and verify that the "Show" column for "Receipt URL" is checked
+5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the journal type have this same configuration
+
+## Step 8: Confirm Expense Report Transaction Forms are Configured Properly
+
+1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
+2. Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main**
+3. Verify the "Created From" label has "Show" checked and the Display Type is set to Normal
+4. Click the second sub-header, Expenses, and verify that the 'Show' column for 'Receipt URL' is checked
+5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the expense report type have this same configuration
+
+## Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly
+
+1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
+2. Click **Customize** or **Edit** next to your preferred Vendor Bill form
+3. Then, click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked
+4. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class
+5. Go to _Customization > Forms > Transaction Forms_ and provide all other transaction forms with the vendor bill type have this same configuration
+
+## Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly
+
+1. While logged in as an administrator, go to _Customization > Forms > Transaction Forms_
+2. Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked
+3. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class
+4. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the vendor credit type have this same configuration
+
+## Step 11: Set up Tax Groups (only applicable if tracking taxes)
+
+Expensify imports NetSuite Tax Groups (not Tax Codes), which you can find in NetSuite under _Setup > Accounting > Tax Groups_.
+
+Tax Groups are an alias for Tax Codes in NetSuite and can contain one or more Tax Codes (Please note: for UK and Ireland subsidiaries, please ensure your Tax Groups do not have more than one Tax Code). We recommend naming Tax Groups so your employees can easily understand them, as the name and rate will be displayed in Expensify.
+
+Before importing NetSuite Tax Groups into Expensify:
+1. Create your Tax Groups in NetSuite by going to _Setup > Accounting > Tax Groups_
+2. Click **New**
+3. Select the country for your Tax Group
+4. Enter the Tax Name (this is what employees will see in Expensify)
+5. Select the subsidiary for this Tax Group
+6. Select the Tax Code from the table you wish to include in this Tax Group
+7. Click **Add**
+8. Click **Save**
+9. Create one NetSuite Tax Group for each tax rate you want to show in Expensify
+
+Ensure Tax Groups can be applied to expenses by going to _Setup > Accounting > Set Up Taxes_ and setting the Tax Code Lists Include preference to "Tax Groups And Tax Codes" or "Tax Groups Only."
+
+If this field does not display, it’s not needed for that specific country.
+
+## Step 12: Connect Expensify and NetSuite
+
+1. Log into Expensify as a Policy Admin and go to **Settings > Workspaces > _[Workspace Name]_ > Connections > NetSuite**
+2. Click **Connect to NetSuite**
+3. Enter your Account ID (Account ID can be found in NetSuite by going to _Setup > Integration > Web Services Preferences_)
+4. Then, enter the token and token secret
+5. Click **Connect to NetSuite**
+
+From there, the NetSuite connection will sync, and the configuration dialogue box will appear.
+
+Please note that you must create the connection using a NetSuite account with the Expensify Integration role
+
+Once connected, all reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration).
+
+{% include faq-begin.md %}
+
+## Can negative expenses be exported to NetSuite?
+You can export reports with a negative total to NetSuite by selecting “Vendor Bill” as your export option. When a report total is negative, we’ll create a Vendor Credit in NetSuite instead of a bill.
+
+**Important**: Only enable this if you pay your employees/vendors outside of Expensify. A Vendor Credit reduces the total amount payable in NetSuite, but not in Expensify.
+
+To use this feature, make sure you have configured your Vendor Credit transaction form in NetSuite and are using the latest version of the Expensify bundle (version 1.4). If you need to update, go to **Customization > SuiteBundler > Search & Install Bundles > List** and click **Update** next to **Expensify Connect**.
+
+## How do you switch the owner of the connection between NetSuite and Expensify?
+
+Follow the steps below to transfer ownership of the NetSuite connection to someone else:
+1. Head to **Settings > Workspaces > Workspace Name > Connections > NetSuite**
+2. Click **Configure** to review and save the settings for future reference
+3. Select **Do not connect to NetSuite**
+4. Select **Connect to NetSuite**
+5. Enter the email address of the new admin who will take over as the NetSuite User ID
+6. Enter the NetSuite Account ID (found in NetSuite under **Setup > Integration > Web Services Preferences**)
+7. Click **Create a new NetSuite Connection**
+8. Confirm completion of prerequisites and proceed by clicking Continue
+9. You will be redirected to the NetSuite SSO page, where you will enter the email address of the new connection owner and the NetSuite password for that account
+10. Once redirected to the NetSuite page, click **View all roles** and ensure you are logged in under the Administrator role
+11. After confirmation, sign out
+12. Return to Expensify to reconfigure the sync and export settings on the updated connection
+13. Click **Save**
+
+**If you run into any issues updating the connection, follow these additional troubleshooting steps:**
+- In NetSuite, access the role of the current connection owner
+- Click Edit > Access > Choose any role other than Administrator > Save
+- Click Edit > Access > Select Administrator role > Save
+- Repeat the steps outlined above
+
+{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md b/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md
new file mode 100644
index 000000000000..bd1f424a2dd3
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md
@@ -0,0 +1,466 @@
+---
+title: Netsuite Troubleshooting
+description: Troubleshoot common NetSuite sync and export errors.
+---
+
+# Overview of NetSuite Troubleshooting
+Synchronizing and exporting data between Expensify and NetSuite can streamline your financial processes, but occasionally, users may encounter errors that prevent a smooth integration. These errors often arise from discrepancies in settings, missing data, or configuration issues within NetSuite or Expensify.
+
+This troubleshooting guide aims to help you identify and resolve common sync and export errors, ensuring a seamless connection between your financial management systems. By following the step-by-step solutions provided for each specific error, you can quickly address issues and maintain accurate and efficient expense reporting and data management.
+
+# ExpensiError NS0005: Please enter value(s) for Department, Location or Class
+
+This error occurs when the classification (like Location) is required at the header level of your transaction form in NetSuite.
+
+For expense reports and journal entries, NetSuite uses classifications from the employee record default. Expensify only exports this information at the line item level.
+
+For vendor bills, these classifications can't be mandatory because we use the vendor record instead of the employee record, and vendor records don’t have default classifications.
+
+**Vendor Bills:**
+When exporting as a Vendor Bill, we pull from the vendor record, not the employee. Therefore, employee defaults don’t apply at the header ("main") level. This error appears if your NetSuite transaction form requires those fields.
+
+## How to Fix ExpensiError NS0005
+1. Go to **Customization > Forms > Transaction Forms**.
+2. Click **"Edit"** on your preferred vendor bill form.
+3. Go to **Screen Fields > Main**.
+4. Uncheck both **"Show"** and **"Mandatory"** for the listed fields in your error message.
+5. Sync the workspace connection in Expensify (**Settings > Workspaces > Workspace Name > Connections > Sync**).
+6. Attempt the export again.
+
+**Journal Entries and Expense Reports:**
+If you see this error when exporting a Journal Entry or Expense Report, it might be because the report submitter doesn’t have default settings for Departments, Classes, or Locations.
+
+**To fix this:**
+1. Go to **Lists > Employees** in NetSuite.
+2. Click **"Edit"** next to the employee's name who submitted the report.
+3. Scroll down to the **Classification** section.
+4. Select a default **Department**, **Class**, and **Location** for the employee.
+5. Click **Save**.
+6. Sync your NetSuite connection in Expensify.
+
+
+# ExpensiError NS0012: Currency Does Not Exist In NetSuite
+
+**Scenario One:** When dealing with foreign transactions, Expensify sends the conversion rate and currency of the original expense to NetSuite. If the currency isn't listed in your NetSuite subsidiary, you'll see an error message saying the currency does not exist in NetSuite.
+
+**To fix this:**
+1. Ensure the currency in Expensify matches what's in your NetSuite subsidiary.
+2. If you see an error saying 'The currency X does not exist in NetSuite', re-sync your connection to NetSuite through the workspace admin section in Expensify.
+3. Try exporting again.
+
+**Scenario Two:** This error can happen if you’re using a non-OneWorld NetSuite instance and exporting a currency other than EUR, GBP, USD, or CAD.
+
+**To fix this:**
+1. Head to NetSuite.
+2. Go to **Setup > Enable Features**.
+3. Check the **Multiple Currencies** box.
+
+Once you've done this, you can add the offending currency by searching **New Currencies** in the NetSuite global search.
+
+
+# ExpensiError NS0021: Invalid tax code reference key
+
+This error usually indicates an issue with the Tax Group settings in NetSuite, which can arise from several sources.
+
+#### Tax Group to Tax Code Mapping
+If a Tax Code on Sales Transactions is mapped to a Tax Group, an error will occur. To fix this, the Tax Code must be mapped to a Tax Code on Purchase Transactions instead.
+
+To verify if a Tax Code is for Sales or Purchase transactions, view the relevant Tax Code(s).
+
+**For Australian Taxes:**
+Ensure your Tax Groups are mapped correctly:
+- **GST 10%** to **NCT-AU** (not the Sales Transaction Tax Code TS-AU)
+- **No GST 0%** to **NCF-AU** (not the Sales Transaction Tax Code TFS-AU)
+
+#### Tax Group Type
+Tax Groups can represent different types of taxes. For compatibility with Expensify, ensure the tax type is set to GST/VAT.
+
+#### Enable Tax Groups
+Some subsidiaries require you to enable Tax Groups. Go to **Set Up Taxes** for the subsidiary's country and ensure the Tax Code lists include both Tax Codes and Tax Groups.
+
+
+# ExpensiError NS0023: Employee Does Not Exist in NetSuite (Invalid Employee)
+
+This can happen if the employee’s subsidiary in NetSuite doesn’t match what’s listed in Expensify.
+
+## How to Fix ExpensiError NS0023
+1. **Check the Employee's Subsidiary**
+ - Go to the employee record in NetSuite.
+ - Confirm the employee's subsidiary matches what’s listed as the subsidiary at the workspace level.
+ - To find this in Expensify navigate to **Settings > Workspaces > Workspace Name > Connections > Configure**.
+ - If the subsidiaries don’t match, update the subsidiary in Expensify to match what’s listed in NetSuite.
+ - Sync the NetSuite connection in Expensify.
+2. **Verify Access Restrictions:**
+ - Go to **Lists > Employees > Employees > [Select Employee] > Edit > Access**.
+ - Uncheck **Restrict Access to Expensify**.
+3. **Additional Checks:**
+ - Ensure the email on the employee record in NetSuite matches the email address of the report submitter in Expensify.
+ - In NetSuite, make sure the employee's hire date is in the past and/or the termination date is in the future.
+4. **Currency Match for Journal Entries:**
+ - If exporting as Journal Entries, ensure the currency for the NetSuite employee record, NetSuite subsidiary, and Expensify policy all match.
+ - In NetSuite, go to the **Human Resources** tab > **Expense Report Currencies**, and add the subsidiary/policy currency if necessary.
+
+
+# ExpensiError NS0024: Invalid Customer or Project Tag
+
+Employees must be listed as a resource on the customer/project in NetSuite to be able to apply it to an expense. If that isn’t set up in NetSuite, you can run into this error.
+
+## How to Fix ExpensiError NS0024
+
+1. **Ensure Employee Access:**
+ - In NetSuite, go to **Lists > Relationships > Customer/Projects**.
+ - Click **Edit** next to the desired Customer/Project.
+ - Click **Resources**, select the Employee from the drop-down menu, click **Add**, then **Save** your changes.
+2. **Sync with Expensify:**
+ - In Expensify, go to **Settings > Workspaces > [Workspace Name] > NetSuite > Sync Now**.
+ - Attempt to export again.
+3. **Remove Restriction for a Specific Customer/Project (Optional):**
+ - In NetSuite, edit the customer/project.
+ - Go to **Preferences**, then uncheck **Limit time and expenses to resources**.
+4. **Remove Restrictions for All Projects:**
+ - In NetSuite, go to **Setup > Accounting > Accounting Preferences > Time & Expenses**.
+ - Uncheck **Show Projects Only For Time And Expense Entry**.
+5. **Enable Cross-Subsidiary Customers/Projects in Expensify (Optional):**
+ - Go to **Settings > Workspaces > Group > [Workspace Name] > Connections > NetSuite > Configure > Advanced**.
+ - Enable **Cross-Subsidiary Customers/Projects** to remove the requirement for the employee's subsidiary and the customer's subsidiary to match.
+
+
+# ExpensiError NS0034: This record already exists
+
+This error occurs when the report in question was already exported to NetSuite.
+
+## How to fix ExpensiError NS0034
+1. **Check for the existing Report in NetSuite:**
+ - If the report already exists in NetSuite but has been edited in Expensify, you need to delete the report in NetSuite.
+2. **Find the Report ID in Expensify:**
+ - Locate the Report ID in Expensify under the Details section in the top right corner of the report.
+3. **Search for the Report ID in NetSuite:**
+ - Use the Global Search Box in NetSuite to find the report using the Report ID.
+4. **Delete the Report in NetSuite:**
+ - In NetSuite, click _**Edit > Actions > Delete**_ to remove the report.
+5. **Re-export the Report from Expensify to NetSuite:**
+ - After deleting the report in NetSuite, re-export it from Expensify to NetSuite.
+
+
+# ExpensiError NS0046: Billable Expenses Not Coded with a NetSuite Customer or Billable Project
+
+NetSuite requires billable expenses to be assigned to a Customer or a Project that is configured as billable to a Customer. If this is not set up correctly in NetSuite, this error can occur.
+
+## How to Fix ExpensiError NS0046
+1. **Access the Report in Expensify:**
+ - Go to the Expensify website and open the report.
+2. Click the **Details** icon in the upper-left corner of the report.
+3. Select the following options (this allows you to easily see which expenses are flagged as billable but don't have a valid customer tagged):
+ - **View:** Details
+ - **Group by:** Tag
+ - **Split report by:** Billable
+4. **Check Billable Expenses:**
+ - Click on each expense in the Billable section of the report.
+ - Ensure that the Customer or Project tag field is present.
+ - Verify that there are no violations and that a value has been applied to the field.
+5. Make any necessary adjustments to the billable expenses and try the export again.
+
+
+# ExpensiError NS0059: Elimination Settings for X Do Not Match
+
+This error occurs when an Intercompany Payable account is set as the default in the Default Payable Account field in the NetSuite subsidiary preferences, and the Accounting Approval option is enabled for Expense Reports.
+
+## How to Fix ExpensiError NS0059
+Set the Default Payable Account for Expense Reports on each subsidiary in NetSuite to ensure the correct payable account is active.
+1. Navigate to Subsidiaries:
+ - Go to Setup > Company > Subsidiaries.
+2. Edit Subsidiary Preferences:
+ - Click Edit for the desired subsidiary.
+ - Go to the Preferences tab.
+3. Set Default Payable Account:
+ - Choose the preferred account for Default Payable Account for Expense Reports.
+
+Repeat these steps for each subsidiary to ensure the settings are correct, and then sync Expensify to NetSuite to update the connection.
+
+
+# ExpensiError NS0059: A credit card account has not been selected for corporate card expenses.
+
+**To resolve this error:**
+1. Log into NetSuite as an admin.
+2. Type "Page: Subsidiaries" in the global search box and select the subsidiary you will export to.
+3. Under the Preferences tab of the subsidiary, locate the field: Default Account for Corporate Card Expenses.
+
+**If you want to assign different cards to different employees:**
+1. Go to each employee record in NetSuite.
+2. Under the Human Resources > Expense and Purchasing section, find the field: Default Account for Corporate Card Expenses.
+
+**For reports containing cash expenses that are not marked as Reimbursable:**
+1. Have the approver reject the report.
+2. Mark the expenses as Reimbursable.
+3. Re-submit the report, approve it, and try to export again.
+
+For accounts without subsidiaries (non-OneWorld accounts), the default field is in your accounting preferences.
+
+
+# ExpensiError NS0085: Expense Does Not Have Appropriate Permissions for Settings an Exchange Rate in NetSuite
+
+This error occurs when the exchange rate settings in NetSuite aren't updated correctly.
+
+## How to Fix ExpensiError NS0085
+1. In NetSuite, go to Customization > Forms > Transaction Forms.
+2. Search for the form type that the report is being exported as (Expense Report, Journal Entry, or Vendor Bill) and click Edit next to the form that has the Preferred checkbox checked.
+ - **For Expense Reports:**
+ - Go to Screen Fields > Expenses (the Expenses tab farthest to the right).
+ - Ensure the Exchange Rate field under the Description column has the Show checkbox checked.
+ - **For Vendor Bills:**
+ - Go to Screen Fields > Main.
+ - Ensure the Exchange Rate field under the Description column has the Show checkbox checked.
+ - **For Journal Entries:**
+ - Go to Screen Fields > Lines.
+ - Ensure the Exchange Rate field under the Description column has the Show checkbox checked.
+ - Go to Screen Fields > Main and ensure the Show checkbox is checked in the Exchange Rate field under the Description column.
+3. Go to **Settings > Workspaces > Group > [Workspace Name] > Connections**.
+4. Click Sync Now to sync the NetSuite connection.
+5. Export the report(s) again.
+
+
+# ExpensiError NS0079: The Transaction Date is Not Within the Date Range of Your Accounting Period
+
+The transaction date you specified is not within the date range of your accounting period. When the posting period settings in NetSuite are not configured to allow a transaction date outside the posting period, you can't export a report to the next open period, which is why you’ll run into this error.
+
+## How to Fix ExpensiError NS0079
+1. In NetSuite, navigate to Setup > Accounting > Accounting Preferences.
+2. Under the General Ledger section, ensure the field Allow Transaction Date Outside of the Posting Period is set to Warn.
+3. Then, choose whether to export your reports to the First Open Period or the Current Period.
+
+Additionally, ensure the Export to Next Open Period feature is enabled within Expensify:
+1. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Connections > Configure**.
+2. Open the **Advanced tab**.
+3. Confirm that the setting for **Export to Next Open Period** is enabled.
+
+If any configuration settings are updated on the NetSuite connection, be sure to sync the connection before trying the export again.
+
+
+# ExpensiError NS0055: The Vendor You are Trying to Export to Does Not Have Access to the Currency X
+
+This error occurs when a vendor tied to a report in Expensify does not have access to a currency on the report in NetSuite. The vendor used in NetSuite depends on the type of expenses on the report you're exporting.
+- For **reimbursable** (out-of-pocket) expenses, this is the report's submitter (the employee who submitted the report).
+- For **non-reimbursable** (e.g., company card) expenses, this is the default vendor set via Settings > Workspaces > Group > [Workspace Name] > Connections > NetSuite > Configure.
+
+## How to Fix ExpensiError NS0055
+To fix this, the vendor needs to be given access to the applicable currency:
+1. In NetSuite, navigate to Lists > Relationships > Vendors to access the list of Vendors.
+2. Click Edit next to the Vendor tied to the report:
+ - For reimbursable (out-of-pocket) expenses, this is the report's submitter.
+ - For non-reimbursable (e.g., company card) expenses, this is the default vendor set via **Settings > Workspaces > Group > [Workspace Name] > Connections > NetSuite > Configure**.
+3. Navigate to the Financial tab.
+4. Scroll down to the Currencies section and add all the currencies that are on the report you are trying to export.
+5. Click Save.
+
+
+# ExpensiError NS0068: You do not have permission to set a value for element - “Created From”
+
+**To resolve this error:**
+1. In NetSuite, go to Customization > Forms > Transaction Forms.
+2. Search for the form type that the report is being exported as in NetSuite (Expense Report, Journal Entry, Vendor Bill, or if the report total is negative, Vendor Credit).
+3. Click Edit next to the form that has the Preferred checkbox checked.
+4. Go to Screen Fields > Main and ensure the field Created From has the Show checkbox checked.
+5. Sync the NetSuite connection under **Settings > Workspaces > Group > [Workspace Name] > Connections > Sync Now**.
+6. Export the report(s) again.
+
+#### For reports with Expensify Card expenses
+Expensify Card expenses export as Journal Entries. If you encounter this error when exporting a report with Expensify Card non-reimbursable expenses, ensure the field Created From has the Show checkbox checked for Journal Entries in NetSuite.
+
+
+# ExpensiError NS0037: You do not have permission to set a value for element - “Receipt URL”
+
+**To resolve this error:**
+1. In NetSuite, go to Customization > Forms > Transaction Forms.
+2. Search for the form type that the report is being exported as in NetSuite (Expense Report, Journal Entry, or Vendor Bill).
+3. Click Edit next to the form that has the Preferred checkbox checked.
+ - If the report is being exported as an Expense Report:
+ - Go to Screen Fields > Expenses (the Expenses tab farthest to the right).
+ - Ensure the field ReceiptURL has the Show checkbox checked.
+ - If the report is being exported as a Journal Entry:
+ - Go to Screen Fields > Lines.
+ - Ensure the field ReceiptURL has the Show checkbox checked.
+ - If the report is being exported as a Vendor Bill:
+ - Go to Screen Fields > Main.
+ - Ensure the field ReceiptURL has the Show checkbox checked.
+4. Click Sync Now to sync the NetSuite connection at **Settings > Workspaces > Group > Workspace Name > Connections**.
+5. Export the report(s) again.
+
+
+# ExpensiError NS0042: Error creating vendor - this entity already exists
+
+This error occurs when a vendor record already exists in NetSuite, but Expensify is still attempting to create a new one. This typically means that Expensify cannot find the existing vendor during export.
+- The vendor record already exists in NetSuite, but there may be discrepancies preventing Expensify from recognizing it.
+- The email on the NetSuite vendor record does not match the email of the report submitter in Expensify.
+- The vendor record might not be associated with the correct subsidiary in NetSuite.
+
+## How to Fix ExpensiError NS0042
+
+Follow these steps to resolve the issue:
+1. **Check Email Matching:**
+ - Ensure the email on the NetSuite vendor record matches the email of the report submitter in Expensify.
+2. **Check Subsidiary Association:**
+ - Ensure the vendor record is associated with the same subsidiary selected in the connection configurations
+ - You can review this under **Settings > Workspaces > Group > Workspace Name > Connections > Configure**.
+3. **Automatic Vendor Creation:**
+ - If you want Expensify to automatically create vendors, ensure the "Automatically Create Vendor" option is enabled under **Settings > Workspaces > Group > Workspace Name > Connections > Advanced**.
+
+**Options to Resolve the Error:**
+- **Edit the Existing Vendor:** Update the existing vendor record in NetSuite to match the report submitter's email and name.
+- **Delete the Existing Vendor:** If appropriate, delete the existing vendor record in NetSuite to allow Expensify to create a new one.
+- **Add Email to Existing Vendor:** Add the email address of the report’s submitter to the existing vendor record in NetSuite.
+
+**Final Steps:**
+1. After making the necessary changes, head to **Settings > Workspaces > Group > Workspace Name > Connections** in Expensify.
+2. Sync the NetSuite workspace connection.
+3. Retry exporting the report.
+
+
+# ExpensiError NS0109: Failed to login to NetSuite, please verify your credentials
+
+This error indicates a problem with the tokens created for the connection between Expensify and NetSuite. The error message will say, "Login Error. Please check your credentials."
+
+## How to Fix ExpensiError NS0109
+1. Review the [Connect to NetSuite](https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite) guide and follow steps 1 and 2 exactly as outlined.
+2. If you're using an existing token and encounter a problem, you may need to create a new token.
+
+
+# ExpensiError NS0123 Login Error: Please make sure that the Expensify integration is enabled
+
+This error indicates that the Expensify integration is not enabled in NetSuite.
+
+## How to Fix ExpensiError NS0123
+1. **Enable the Expensify Integration:**
+ - In NetSuite, navigate to Setup > Integrations > Manage Integrations.
+ - Ensure that the Expensify Integration is listed and that the State is Enabled.
+2. **If you can't find the Expensify integration:**
+ - Click "Show Inactives" to see if Expensify is listed as inactive.
+ - If Expensify is listed, update its state to Enabled.
+
+Once the Expensify integration is enabled, try syncing the NetSuite connection again.
+
+
+# ExpensiError NS0045: Expenses Not Categorized with a NetSuite Account
+
+**To resolve this error:**
+1. Log into NetSuite
+2. Do a global search for the missing record.
+ - Ensure the expense category is active and correctly named.
+ - Ensure the category is associated with the correct subsidiary that the Expensify workspace is linked to.
+3. In Expensify, head to **Settings > Workspaces > Groups > Workspace Name > Connections** and click **Sync Now** on the NetSuite connection to resync the workspace.
+4. Go back to the report, click on the offending expense(s), and re-apply the category in question.
+5. Export the report again.
+
+
+# ExpensiError NS0061: Please Enter Value(s) for: Tax Code
+
+This error typically occurs when attempting to export expense reports to a Canadian subsidiary in NetSuite for the first time and/or if your subsidiary in NetSuite has Tax enabled.
+
+## How to Fix ExpensiError NS0061
+To fix this, you need to enable Tax in the NetSuite configuration settings.
+
+1. Go to **Settings > Workspaces > Group > Workspace Name > Connections > NetSuite**.
+ - Be sure to select posting accounts for GST/HST and PST if you plan on exporting any expenses with taxes on them to Journal Entries.
+2. Click **Save**
+3. Click **Sync Now** to sync the connection
+
+**Note:** Expenses created before Tax was enabled might need to have the newly imported taxes applied to them retroactively to be exported.
+
+
+# Error creating employee: Your current role does not have permission to access this record.
+
+This error indicates that the credentials or role used to connect NetSuite to Expensify do not have the necessary permissions within NetSuite. You can find setup instructions for configuring permissions in NetSuite [here](https://help.expensify.com/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite#step-3-add-expensify-integration-role-to-a-user).
+
+**To resolve this error:**
+1. If permissions are configured correctly, confirm the report submitter exists in the subsidiary set on the workspace and that their Expensify email address matches the email on the NetSuite Employee Record.
+2. If the above is true, try toggling off "Automatically create vendors/employees" under the Advanced tab of the NetSuite configuration window.
+ - Head to **Settings > Workspaces > Group > Workspace Name > Connections > NetSuite > Configure**
+ - Click on the **Advanced** tab
+ - Disable **Automatically create vendors/employees**
+3. Sync the NetSuite connection in Expensify
+4. Export the report again.
+
+
+# Why are reports exporting as `Accounting Approved` instead of `Paid in Full`?
+
+**This can occur for two reasons:**
+- Missing Locations, Classes, or Departments in the Bill Payment Form
+- Incorrect Settings in Expensify Workspace Configuration
+
+## Missing Locations, Classes, or Departments in Bill Payment Form
+
+If locations, classes, or departments are required in your accounting classifications but are not marked as 'Show' on the preferred bill payment form, this error can occur, and you will need to update the bill payment form in NetSuite:
+1. Go to Customization > Forms > Transaction Forms.
+2. Find your preferred (checkmarked) Bill Payment form.
+3. Click Edit or Customize.
+4. Under the Screen Fields > Main tab, check 'Show' near the department, class, and location options.
+
+## Incorrect Settings in Expensify Workspace Configuration
+
+To fix this, you'll want to confirm the NetSuite connection settings are set up correctly in Expensify:
+1. Head to **Settings > Workspaces > Group > Workspace Name > Connections > NetSuite > Configure > Advanced**
+2. **Ensure the following settings are correct:**
+ - Sync Reimbursed Reports: Enabled and payment account chosen.
+ - Journal Entry Approval Level: Approved for Posting.
+ - A/P Approval Account: This must match the current account being used for bill payment.
+3. **Verify A/P Approval Account:**
+ - To ensure the A/P Approval Account matches the account in NetSuite:
+ - Go to your bill/expense report causing the error.
+ - Click Make Payment.
+ - This account needs to match the account selected in your Expensify configuration.
+4. **Check Expense Report List:**
+ - Make sure this is also the account selected on the expense report by looking at the expense report list.
+
+Following these steps will help ensure that reports are exported as "Paid in Full" instead of "Accounting Approved."
+
+
+# Why are reports exporting as `Pending Approval`?
+
+If reports are exporting as "Pending Approval" instead of "Approved," you'll need to adjust the approval preferences in NetSuite.
+
+**Exporting as Journal Entries/Vendor Bills:**
+1. In NetSuite, go to Setup > Accounting > Accounting Preferences.
+2. On the **General** tab, uncheck **Require Approvals on Journal Entries**.
+3. On the **Approval Routing** tab, uncheck Journal Entries/Vendor Bills to remove the approval requirement for Journal Entries created in NetSuite.
+
+**Note:** This change affects all Journal Entries, not just those created by Expensify.
+
+**Exporting as Expense Reports:**
+1. In NetSuite, navigate to Setup > Company > Enable Features.
+2. On the "Employee" tab, uncheck "Approval Routing" to remove the approval requirement for Expense Reports created in NetSuite. Please note that this setting also applies to purchase orders.
+
+
+# How do I Change the Default Payable Account for Reimbursable Expenses in NetSuite?
+
+NetSuite is set up with a default payable account that is credited each time reimbursable expenses are exported as Expense Reports to NetSuite (once approved by the supervisor and accounting). If you need to change this to credit a different account, follow the below steps:
+
+**For OneWorld Accounts:**
+1. Navigate to Setup > Company > Subsidiaries in NetSuite.
+2. Next to the subsidiary you want to update, click Edit.
+3. Click the Preferences tab.
+4. In the Default Payable Account for Expense Reports field, select the desired payable account.
+5. Click Save.
+
+**For Non-OneWorld Accounts:**
+1. Navigate to Setup > Accounting > Accounting Preferences in NetSuite.
+2. Click the Time & Expenses tab.
+3. Under the Expenses section, locate the Default Payable Account for Expense Reports field and choose the preferred account.
+4. Click Save.
+
+
+# Why are my Company Card Expenses Exporting to the Wrong Account in NetSuite?
+
+If your company card transactions are exporting to the wrong account in your accounting system, there are a couple of factors to check:
+1. **Verify Card Mapping:**
+ - Ensure that the cards are mapped to the correct accounts at the domain level
+ - This can be viewed under **Settings > Domains > Domain Name > Company Cards**.
+2. **Check Default Account Settings:**
+ - Review the account where the expenses were exported
+ - It should be the default account under **Settings > Workspaces > Group > Workspace Name > Connections**.
+ - Click **Configure** to check the default export settings for your non-reimbursable expenses.
+
+The most common reason expenses export to the default account is that they are not actually imported from the mapped company card. Only company card expenses (notated with the “Card+Lock” icon) can use the export mapping settings configured at the domain level.
+
+Even if an expense was paid with the company card, it is considered a 'cash' expense unless it merges with a card expense marked with the Card+Lock icon.
+
+Less commonly, the issue may occur if the company card has been added to the user's personal settings. Expenses imported from a card linked at the individual account level will have a plain card icon.
+
diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md
new file mode 100644
index 000000000000..eda92d41e820
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md
@@ -0,0 +1,6 @@
+---
+title: Configure Quickbooks Desktop
+description: Configure Quickbooks Desktop
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md
similarity index 99%
rename from docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md
rename to docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md
index 8fe31f3ec4f4..cd0b23d327d7 100644
--- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md
+++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md
@@ -1,6 +1,7 @@
---
title: QuickBooks Desktop
description: How to connect Expensify to QuickBooks Desktop and troubleshoot issues.
+order: 1
---
# Overview
QuickBooks Desktop is an accounting package developed by Intuit. It is designed for small and medium-sized businesses to help them manage their financial and accounting tasks. You can connect Expensify to QuickBooks Desktop to make expense management seamless.
diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md
new file mode 100644
index 000000000000..cdbba9ec0b23
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Quickbooks Desktop Troubleshooting
+description: Quickbooks Desktop Troubleshooting
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md
new file mode 100644
index 000000000000..99e598adb06a
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md
@@ -0,0 +1,6 @@
+---
+title: Configure Quickbooks Online
+description: Configure Quickbooks Online
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md b/docs/articles/expensify-classic/connections/quickbooks-online/Connect-To-QuickBooks-Online.md
similarity index 99%
rename from docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md
rename to docs/articles/expensify-classic/connections/quickbooks-online/Connect-To-QuickBooks-Online.md
index 623e5f1dd997..1d536d7bdb66 100644
--- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md
+++ b/docs/articles/expensify-classic/connections/quickbooks-online/Connect-To-QuickBooks-Online.md
@@ -1,6 +1,7 @@
---
title: QuickBooks Online
description: Everything you need to know about using Expensify's direct integration with QuickBooks Online.
+order: 1
---
# Overview
diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md
new file mode 100644
index 000000000000..7e58ad83a164
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Quickbooks Online Troubleshooting
+description: Quickbooks Online Troubleshooting
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md b/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md
new file mode 100644
index 000000000000..444825973161
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md
@@ -0,0 +1,6 @@
+---
+title: Configure Sage Intacct
+description: Configure Sage Intacct
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md b/docs/articles/expensify-classic/connections/sage-intacct/Connect-To-Sage-Intacct.md
similarity index 99%
rename from docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md
rename to docs/articles/expensify-classic/connections/sage-intacct/Connect-To-Sage-Intacct.md
index 560a65d0d722..369c3aae9fa9 100644
--- a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md
+++ b/docs/articles/expensify-classic/connections/sage-intacct/Connect-To-Sage-Intacct.md
@@ -1,6 +1,7 @@
---
title: Sage Intacct
description: Connect your Expensify workspace with Sage Intacct
+order: 1
---
# Overview
Expensify’s seamless integration with Sage Intacct allows you to connect using either Role-based permissions or User-based permissions.
diff --git a/docs/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting.md b/docs/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting.md
new file mode 100644
index 000000000000..db341f87e930
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Sage Intacct Troubleshooting
+description: Sage Intacct Troubleshooting
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/connections/xero/Configure-Xero.md b/docs/articles/expensify-classic/connections/xero/Configure-Xero.md
new file mode 100644
index 000000000000..b23216c28401
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/xero/Configure-Xero.md
@@ -0,0 +1,6 @@
+---
+title: Configure Xero
+description: Configure Xero
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md b/docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md
similarity index 99%
rename from docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md
rename to docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md
index 9dd479e90cf1..3010d11c2ff1 100644
--- a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md
+++ b/docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md
@@ -1,6 +1,7 @@
---
title: The Xero Integration
description: Everything you need to know about Expensify's direct integration with Xero
+order: 1
---
# About
diff --git a/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md b/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md
new file mode 100644
index 000000000000..98ae5033db50
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Xero Troubleshooting
+description: Xero Troubleshooting
+---
+
+# Coming soon
diff --git a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md
index 043cc4be1e26..b9938b058ef6 100644
--- a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md
+++ b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md
@@ -1,174 +1,134 @@
---
title: Admin Card Settings and Features
-description: An in-depth look into the Expensify Card program's admin controls and settings.
+description: A deep dive into the available controls and settings for the Expensify Card.
---
+# Expensify Visa® Commercial Card Overview
+The Expensify Visa® Commercial Card offers various settings to help admins manage expenses and card usage efficiently. Here’s how to use these features:
-# Overview
+## Smart Limits
+Smart Limits allow you to set custom spending limits for each Expensify cardholder or default limits for groups. Setting a Smart Limit activates an Expensify card for your user and issues a virtual card for immediate use.
-The Expensify Visa® Commercial Card offers a range of settings and functionality to customize how admins manage expenses and card usage in Expensify. To start, we'll lay out the best way to make these options work for you.
+#### Set Limits for Individual Cardholders
+As a Domain Admin, you can set or edit Custom Smart Limits for a card:
+1. Go to _**Settings > Domains > Domain Name > Company Cards**_.
+2. Click **Edit Limit** to set the limit.
-Set Smart Limits to control card spend. Smart Limits are spend limits that can be set for individual cards or specific groups. Once a given Smart Limit is reached, the card is temporarily disabled until expenses are approved.
+This limit restricts the amount of unapproved (unsubmitted and processing) expenses a cardholder can incur. Once the limit is reached, the cardholder cannot use their card until they submit outstanding expenses and have their card spend approved. If you set the Smart Limit to $0, the user’s card cannot be used.
-Monitor spend using your Domain Limit and the Reconciliation Dashboard.
-Your Domain Limit is the total Expensify Card limit across your entire organization. No member can spend more than what's available here, no matter what their individual Smart Limit is. A Domain Limit is dynamic and depends on a number of factors, which we'll explain below.
+#### Set Default Group Limits
+Domain Admins can set or edit custom Smart Limits for a domain group:
-Decide the settlement model that works best for your business
-Monthly settlement is when your Expensify Card balance is paid in full on a certain day each month. Though the Expensify Card is set to settle daily by default, any Domain Admin can change this setting to monthly.
+1. Go to _**Settings > Domains > Domain Name > Groups**_.
+2. Click on the limit in-line for your chosen group and amend the value.
-Now, let's get into the mechanics of each piece mentioned above.
+This limit applies to all members of the Domain Group who do not have an individual limit set via _**Settings > Domains > Domain Name > Company Cards**_.
-# How to set Smart Limits
-Smart Limits allow you to set a custom spend limit for each Expensify cardholder, or default limits for groups. Setting a Smart Limit is the step that activates an Expensify card for your user (and issues a virtual card for immediate use).
+#### Refreshing Smart Limits
+To let cardholders continue spending, you can approve their pending expenses via the Reconciliation tab. This frees up their limit, allowing them to use their card again.
-## Set limits for individual cardholders
-As a Domain Admin, you can set or edit Custom Smart Limits for a card by going to Settings > Domains > Domain Name > Company Cards. Simply click Edit Limit to set the limit. This limit will restrict the amount of unapproved (unsubmitted and Processing) expenses that a cardholder can incur. After the limit is reached, the cardholder won't be able to use their card until they submit outstanding expenses and have their card spend approved. If you set the Smart Limit to $0, the user's card can't be used.
-## Set default group limits
-Domain Admins can set or edit custom Smart Limits for a domain group by going to Settings > Domains > Domain Name > Groups. Just click on the limit in-line for your chosen group and amend the value.
+To check an unapproved card balance and approve expenses:
+1. Click on **Reconciliation** and enter a date range.
+2. Click on the Unapproved total to see what needs approval.
+3. You can add to a new report or approve an existing report from here.
-This limit will apply to all members of the Domain Group who do not have an individual limit set via Settings > Domains > Domain Name > Company Cards.
+You can also increase a Smart Limit at any time by clicking **Edit Limit**.
-## Refreshing Smart Limits
-To let cardholders keep spending, you can approve their pending expenses via the Reconciliation tab. This will free up their limit, allowing them to use their card again.
+### Understanding Your Domain Limit
+To ensure you have the most accurate Domain Limit for your company, follow these steps:
-To check an unapproved card balance and approve expenses, click on Reconciliation and enter a date range, then click though the Unapproved total to see what needs approving. You can add to a new report or approve an existing report from here.
+1. **Connect Your Bank Account:** Go to _**Settings > Account > Payments > Add Verified Bank Account**_ and connect via Plaid.
-You can also increase a Smart Limit at any time by clicking Edit Limit.
+2. **Request a Custom Limit:** If your bank isn’t supported or you’re experiencing connection issues, you can request a custom limit at _**Settings > Domains > Domain Name > Company Cards > Request Limit Increase**_. Note that you’ll need to provide three months of unredacted bank statements for review by our risk management team.
-# Understanding your Domain Limit
+### Factors Affecting Your Domain Limit
+Your Domain Limit may fluctuate due to several factors:
-To get the most accurate Domain Limit for your company, connect your bank account via Plaid under Settings > Account > Payments > Add Verified Bank Account.
+- **Available Funds in Your Verified Business Bank Account:** We regularly monitor balances via Plaid. A sudden decrease in balance within the last 24 hours may impact your limit. For accounts with 'sweep' functionality, maintain a sufficient balance even when sweeping daily.
-If your bank isn't supported or you're having connection issues, you can request a custom limit under Settings > Domains > Domain Name > Company Cards > Request Limit Increase. As a note, you'll need to provide three months of unredacted bank statements for review by our risk management team.
+- **Pending Expenses:** Check the Reconciliation Dashboard for large pending expenses that could affect your available balance. Your Domain Limit automatically adjusts to include pending expenses.
-Your Domain Limit may fluctuate from time to time based on various factors, including:
+- **Processing Settlements:** Settlements typically take about three business days to process and clear. Multiple large settlements over consecutive days may affect your Domain Limit, which updates dynamically once settlements are cleared.
-- Available funds in your Verified Business Bank Account: We regularly check bank balances via Plaid. A sudden drop in balance within the last 24 hours may affect your limit. For 'sweep' accounts, be sure to maintain a substantial balance even if you're sweeping daily.
-- Pending expenses: Review the Reconciliation Dashboard to check for large pending expenses that may impact your available balance. Your Domain Limit will adjust automatically to include pending expenses.
-- Processing settlements: Settlements need about three business days to process and clear. Several large settlements over consecutive days may impact your Domain Limit, which will dynamically update when settlements have cleared.
+Please note: If your Domain Limit is reduced to $0, cardholders cannot make purchases, even if they have higher Smart Limits set on their individual cards.
-As a note, if your Domain Limit is reduced to $0, your cardholders can't make purchases even if they have a larger Smart Limit set on their individual cards.
+## Reconciling Expenses and Settlements
+Reconciling expenses ensures your financial records are accurate and up-to-date. Follow these steps to review and reconcile expenses associated with your Expensify Cards:
-# How to reconcile Expensify Cards
-## How to reconcile expenses
-Reconciling expenses is essential to ensuring your financial records are accurate and up-to-date.
+#### How to Reconcile Expenses:
+1. Go to _**Settings > Domains > Domain Name > Company Cards > Reconciliation > Expenses**_.
+2. Enter your start and end dates, then click *Run*.
+3. The Imported Total will display all Expensify Card transactions for the period.
+4. You'll see a list of all Expensify Cards, the total spend on each card, and a snapshot of expenses that have been approved and have not been approved (Approved Total and Unapproved Total, respectively).
+5. Click on the amounts to view the associated expenses.
-Follow the steps below to quickly review and reconcile expenses associated with your Expensify Cards:
+#### How to Reconcile Settlements:
+A settlement is the payment to Expensify for purchases made using the Expensify Cards. The program can settle on either a daily or monthly basis. Note that not all transactions in a settlement will be approved when running reconciliation.
-1. Go to Settings > Domains > Domain Name > Company Cards > Reconciliation > Expenses
-2. Enter your start and end dates, then click Run
-3. The Imported Total will show all Expensify Card transactions for the period
-4. You'll also see a list of all Expensify Cards, the total spend on each card, and a snapshot of expenses that have and have not been approved (Approved Total and Unapproved Total, respectively)
-By clicking on the amounts, you can view the associated expenses
+1. Log into the Expensify web app.
+2. Click _**Settings > Domains > Domain Name > Company Cards > Reconciliation > `Settlements**_.
+3. Use the Search function to generate a statement for the specific period you need.
+The search results will include the following info for each entry:
+- **Date:** When a purchase was made or funds were debited for payments.
+- **Posted Date:** When the purchase transaction is posted.
+- **Entry ID:** A unique number grouping card payments and transactions settled by those payments.
+- **Amount:** The amount debited from the Business Bank Account for payments.
+- **Merchant:** The business where a purchase was made.
+- **Card:** Refers to the Expensify Card number and cardholder’s email address.
+- **Business Account:** The business bank account connected to Expensify that the settlement is paid from.
+- **Transaction ID:** A special ID that helps Expensify support locate transactions if there’s an issue.
-## How to reconcile settlements
-A settlement is the payment to Expensify for the purchases made using the Expensify Cards.
+Review the individual transactions (debits) and the payments (credits) that settled them. Each cardholder will have a virtual and a physical card listed, handled the same way for settlements, reconciliation, and exporting.
-The Expensify Card program can settle on either a daily or monthly basis. One thing to note is that not all transactions in a settlement will be approved when running reconciliation.
+4. Click **Download CSV** for reconciliation. This will list everything you see on the screen.
+5. To reconcile pre-authorizations, use the Transaction ID column in the CSV file to locate the original purchase.
+6. Review account payments: You’ll see payments made from the accounts listed under _**Settings > Account > Payments > Bank Accounts**_. Payment data won’t show for deleted accounts.
-You can view the Expensify Card settlements under Settings > Domains > Domain Name > Company Cards > Reconciliation > Settlements.
+Use the Reconciliation Dashboard to confirm the status of expenses missing from your accounting system. It allows you to view both approved and unapproved expenses within your selected date range that haven’t been exported yet.
-By clicking each settlement amount, you can see the transactions contained in that specific payment amount.
+### Set a Preferred Workspace
+Many customers find it helpful to separate their company card expenses from other types of expenses for easier coding. To do this, create a separate workspace specifically for card expenses.
-Follow the below steps to run reconciliation on the Expensify Card settlements:
+**Using a Preferred Workspace:**
+Combine this feature with Scheduled Submit to automatically add new card expenses to reports connected to your card-specific workspace.
-1. Log into the Expensify web app
-2. Click Settings > Domains > Domain Name > Company Cards > Reconciliation tab > Settlements
-3. Use the Search function to generate a statement for the specific period you need
-4. The search results will include the following info for each entry:
- - Date: when a purchase was made or funds were debited for payments
- - Posted Date: when the purchase transaction posted
- - Entry ID: a unique number grouping card payments and transactions settled by those payments
- - Amount: the amount debited from the Business Bank Account for payments
- - Merchant: the business where a purchase was made
- - Card: refers to the Expensify Card number and cardholder's email address
- - Business Account: the business bank account connected to Expensify that the settlement is paid from
- - Transaction ID: a special ID that helps Expensify support locate transactions if there's an issue
+### Change the Settlement Account
+You can change your settlement account to any verified business bank account in Expensify. If your current bank account is closing, make sure to set up a replacement as soon as possible.
-5. Review the individual transactions (debits) and the payments (credits) that settled them
-6. Every cardholder will have a virtual and a physical card listed. They're handled the same way for settlements, reconciliation, and exporting.
-7. Click Download CSV for reconciliation
-8. This will list everything that you see on screen
-9. To reconcile pre-authorizations, you can use the Transaction ID column in the CSV file to locate the original purchase
-10. Review account payments
-11. You'll see payments made from the accounts listed under Settings > Account > Payments > Bank Accounts. Payment data won't show for deleted accounts.
+#### Steps to Select a Different Settlement Account:
+1. Go to _**Settings > Domains > Domain Name > Company Cards > Settings**_ tab.
+2. Use the Expensify Card settlement account dropdown to select a new account.
+3. Click **Save**.
-You can use the Reconciliation Dashboard to confirm the status of expenses that are missing from your accounting system. It allows you to view both approved and unapproved expenses within your selected date range that haven't been exported yet.
+### Change the Settlement Frequency
+By default, Expensify Cards settle daily. However, you can switch to monthly settlements.
+#### Monthly Settlement Requirements:
+ - The settlement account must not have had a negative balance in the last 90 days.
+ - There will be an initial settlement for any outstanding spending before the switch.
+ - The settlement date going forward will be the date you switch (e.g., if you switch on September 15th, future settlements will be on the 15th of each month).
-# Deep dive
-## Set a preferred workspace
-Some customers choose to split their company card expenses from other expense types for coding purposes. Most commonly this is done by creating a separate workspace for card expenses.
+#### Steps to Change the Settlement Frequency:
+1. Go to _**Settings > Domains > Domain Name > Company Cards > Settings**_ tab.
+2. Click the **Settlement Frequency** dropdown and select **Monthly**.
+3. Click **Save** to confirm the change.
-You can use the preferred workspace feature in conjunction with Scheduled Submit to make sure all newly imported card expenses are automatically added to reports connected to your card-specific workspace.
+### Declined Expensify Card Transactions
+If you have 'Receive real-time alerts' enabled, you'll get a notification explaining why a transaction was declined. To enable alerts:
+1. Open the mobile app.
+2. Click the three-bar icon in the upper-left corner.
+3. Go to Settings.
+4. Toggle 'Receive real-time alerts' on.
-## How to change your settlement account
-You can change your settlement account to any other verified business bank account in Expensify. If your bank account is closing, make sure you set up the replacement bank account in Expensify as early as possible.
+If you or your employees notice any unfamiliar purchases or need a new card, go to _**Settings > Account > Credit Card Import**_ and click on **Request a New Card**.
-To select a different settlement account:
+#### Common Reasons for Declines:
+- **Insufficient Card Limit:** If a transaction exceeds your card's limit, it will be declined. Always check your balance under _**Settings > Account > Credit Card Import**_ on the web or mobile app. Approve pending expenses to free up your limit.
-1. Go to Settings > Domains > Domain Name > Company Cards > Settings tab
-2. Use the Expensify Card settlement account dropdown to select a new account
-3. Click Save
+- **Card Not Activated or Canceled:** Transactions won't process if the card hasn't been activated or has been canceled.
+- **Incorrect Card Information:** Entering incorrect card details, such as the CVC, ZIP, or expiration date, will lead to declines.
-## Change the settlement frequency
+- **Suspicious Activity:** Expensify may block transactions if unusual activity is detected. This could be due to irregular spending patterns, risky vendors, or multiple rapid transactions. Check your Expensify Home page to approve unusual merchants. If further review is needed, Expensify will perform a manual due diligence check and lock your cards temporarily.
-By default, the Expensify Cards settle on a daily cadence. However, you can choose to have the cards settle on a monthly basis.
-
-1. Monthly settlement is only available if the settlement account hasn't had a negative balance in the last 90 days
-2. There will be an initial settlement to settle any outstanding spend that happened before switching the settlement frequency
-3. The date that the settlement is changed to monthly is the settlement date going forward (e.g. If you switch to monthly settlement on September 15th, Expensify Cards will settle on the 15th of each month going forward)
-
-To change the settlement frequency:
-1. Go to Settings > Domains > Domain Name > Company Cards > Settings tab
-2. Click the Settlement Frequency dropdown and select Monthly
-3. Click Save to confirm the change
-
-
-
-## Declined Expensify Card transactions
-As long as you have 'Receive realtime alerts' enabled, you'll get a notification explaining the decline reason. You can enable alerts in the mobile app by clicking on three-bar icon in the upper-left corner > Settings > toggle Receive realtime alerts on.
-
-If you ever notice any unfamiliar purchases or need a new card, go to Settings > Account > Credit Card Import and click on Request a New Card right away.
-
-Here are some reasons an Expensify Card transaction might be declined:
-
-1. You have an insufficient card limit
- - If a transaction amount exceeds the available limit on your Expensify Card, the transaction will be declined. It's essential to be aware of the available balance before making a purchase to avoid this - you can see the balance under Settings > Account > Credit Card Import on the web app or mobile app. Submitting expenses and having them approved will free up your limit for more spend.
-
-2. Your card hasn't been activated yet, or has been canceled
- - If the card has been canceled or not yet activated, it won't process any transactions.
-
-3. Your card information was entered incorrectly. Entering incorrect card information, such as the CVC, ZIP or expiration date will also lead to declines.
-
-4. There was suspicious activity
- - If Expensify detects unusual or suspicious activity, we may block transactions as a security measure. This could happen due to irregular spending patterns, attempted purchases from risky vendors, or multiple rapid transactions. Check your Expensify Home page to approve unsual merchants and try again.
- If the spending looks suspicious, we may do a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens.
-5. The merchant is located in a restricted country
- - Some countries may be off-limits for transactions. If a merchant or their headquarters (billing address) are physically located in one of these countries, Expensify Card purchases will be declined. This list may change at any time, so be sure to check back frequently: Belarus, Burundi, Cambodia, Central African Republic, Democratic Republic of the Congo, Cuba, Iran, Iraq, North Korea, Lebanon, Libya, Russia, Somalia, South Sudan, Syrian Arab Republic, Tanzania, Ukraine, Venezuela, Yemen, and Zimbabwe.
-
-{% include faq-begin.md %}
-## What happens when I reject an Expensify Card expense?
-Rejecting an Expensify Card expense from an Expensify report will simply allow it to be reported on a different report.
-
-If an Expensify Card expense needs to be rejected, you can reject the report or the specific expense so it can be added to a different report. The rejected expense will become Unreported and return to the submitter's Expenses page.
-
-If you want to dispute a card charge, please message Concierge to start the dispute process.
-
-If your employee has accidentally made an unauthorised purchase, you will need to work that out with the employee to determine how they will pay back your company.
-
-
-## What happens when an Expensify Card transaction is refunded?
-
-
-The way a refund is displayed in Expensify depends on the status of the expense (pending or posted) and whether or not the employee also submitted an accompanying SmartScanned receipt. Remember, a SmartScanned receipt will auto-merge with the Expensify Card expense.
-
-- Full refunds:
-If a transaction is pending and doesn't have a receipt attached (except for eReceipts), getting a full refund will make the transaction disappear.
-If a transaction is pending and has a receipt attached (excluding eReceipts), a full refund will zero-out the transaction (amount becomes zero).
-- Partial refunds:
-If a transaction is pending, a partial refund will reduce the amount of the transaction.
-- If a transaction is posted, a partial refund will create a negative transaction for the refund amount.
-
-{% include faq-end.md %}
+- **Merchant in a Restricted Country:** Transactions will be declined if the merchant is in a restricted country.
diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md
deleted file mode 100644
index fb84e3484598..000000000000
--- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md
+++ /dev/null
@@ -1,213 +0,0 @@
----
-title: Expensify Card Auto-Reconciliation
-description: Everything you need to know about Expensify Card Auto-Reconciliation
----
-
-
-# Overview
-If your company uses the Expensify Visa® Commercial Card, and connects to a direct accounting integration, you can auto-reconcile card spending each month.
-
-The integrations that auto-reconciliation are available on are:
-
-- QuickBooks Online
-- Xero
-- NetSuite
-- Sage Intacct
-
-# How-to Set Up Expensify Card Auto-Reconciliation
-
-## Auto-Reconciliation Prerequisites
-
-- Connection:
-1. A Preferred Workspace is set.
-2. A Reconciliation Account is set and matches the Expensify Card settlement account.
-- Automation:
-1. Auto-Sync is enabled on the Preferred Workspace above.
-2. Scheduled Submit is enabled on the Preferred Workspace above.
-- User:
-1. A Domain Admin is set as the Preferred Workspace’s Preferred Exporter.
-
-To set up your auto-reconciliation account with the Expensify Card, follow these steps:
-1. Navigate to your Settings.
-2. Choose "Domains," then select your specific domain name.
-3. Click on "Company Cards."
-4. From the dropdown menu, pick the Expensify Card.
-5. Head to the "Settings" tab.
-6. Select the account in your accounting solution that you want to use for reconciliation. Make sure this account matches the settlement business bank account.
-
-![Company Card Settings section](https://help.expensify.com/assets/images/Auto-Reconciliaton_Image1.png){:width="100%"}
-
-That's it! You've successfully set up your auto-reconciliation account.
-
-## How does Auto-Reconciliation work
-Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s go over those!
-
-### Handling Purchases and Card Balance Payments
-**What happens**: When an Expensify Card is used to make purchases, the amount spent is automatically deducted from your company’s 'Settlement Account' (your business checking account). This deduction happens on a daily or monthly basis, depending on your chosen settlement frequency. Don't worry; this settlement account is pre-defined when you apply for the Expensify Card, and you can't accidentally change it.
-**Accounting treatment**: After your card balance is settled each day, we update your accounting system with a journal entry. This entry credits your bank account (referred to as the GL account) and debits the Expensify Card Clearing Account. To ensure accuracy, please make sure that the 'bank account' in your Expensify Card settings matches your real-life settlement account. You can easily verify this by navigating to **Settings > Account > Payments**, where you'll see 'Settlement Account' next to your business bank account. To keep track of settlement figures by date, use the Company Card Reconciliation Dashboard's Settlements tab:
-
-![Company Card Reconciliation Dashboard](https://help.expensify.com/assets/images/Auto-Reconciliation_Image2.png){:width="100%"}
-
-### Submitting, Approving, and Exporting Expenses
-**What happens**: Users submit their expenses on a report, which might occur after some time has passed since the initial purchase. Once the report is approved, it's then exported to your accounting software.
-**Accounting treatment**: When the report is exported, we create a journal entry in your accounting system. This entry credits the Clearing Account and debits the Liability Account for the purchase amount. The Liability Account functions as a bank account in your ledger, specifically for Expensify Card expenses.
-
-# Deep Dive
-## QuickBooks Online
-
-### Initial Setup
-1. Start by accessing your group workspace linked to QuickBooks Online. On the Export tab, make sure that the user chosen as the Preferred Exporter holds the role of a Workspace Admin and has an email address associated with your Expensify Cards' domain. For instance, if your domain is company.com, the Preferred Exporter's email should be email@company.com.
-2. Head over to the Advanced tab and ensure that Auto-Sync is enabled.
-3. Now, navigate to **Settings > Domains > *Domain Name* > Company Cards > Settings**. Use the dropdown menu next to "Preferred Workspace" to select the group workspace connected to QuickBooks Online and with Scheduled Submit enabled.
-4. In the dropdown menu next to "Expensify Card reconciliation account," choose your existing QuickBooks Online bank account for reconciliation. This should be the same account you use for Expensify Card settlements.
-5. In the dropdown menu next to "Expensify Card settlement account," select your business bank account used for settlements (found in Expensify under **Settings > Account > Payments**).
-
-### How This Works
-1. On the day of your first card settlement, we'll create the Expensify Card Liability account in your QuickBooks Online general ledger. If you've opted for Daily Settlement, we'll also create an Expensify Clearing Account.
-2. During your QuickBooks Online auto-sync on that same day, if there are unsettled transactions, we'll generate a journal entry totaling all posted transactions since the last settlement. This entry will credit the selected bank account and debit the new Expensify Clearing Account (for Daily Settlement) or the Expensify Liability Account (for Monthly Settlement).
-3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories.
-
-### Example
-- We have card transactions for the day totaling $100, so we create the following journal entry upon sync:
-![QBO Journal Entry](https://help.expensify.com/assets/images/Auto-Reconciliation QBO 1.png){:width="100%"}
-- The current balance of the Expensify Clearing Account is now $100:
-![QBO Clearing Account](https://help.expensify.com/assets/images/Auto-reconciliation QBO 2.png){:width="100%"}
-- After transactions are posted in Expensify and the report is approved and exported, a second journal entry is generated:
-![QBO Second Journal Entry](https://help.expensify.com/assets/images/Auto-reconciliation QBO 3.png){:width="100%"}
-- We reconcile the matching amounts automatically, clearing the balance of the Expensify Clearing Account:
-![QBO Clearing Account 2](https://help.expensify.com/assets/images/Auto-reconciliation QBO 4.png){:width="100%"}
-- Now, you'll have a debit on your credit card account (increasing the total spent) and a credit on the bank account (reducing the available amount). The Clearing Account balance is $0.
-- Each expense will also create a credit card expense, similar to how we do it today, exported upon final approval. This action debits the expense account (category) and includes any other line item data.
-- This process occurs daily during the QuickBooks Online Auto-Sync to ensure your card remains reconciled.
-
-**Note:** If Auto-Reconciliation is disabled for your company's Expensify Cards, a Domain Admin can set an export account for individual cards via **Settings > Domains > *Domain Name* > Company Cards > Edit Exports**. The Expensify Card transactions will always export as Credit Card charges in your accounting software, even if the non-reimbursable setting is configured differently, such as a Vendor Bill.
-
-## Xero
-
-### Initial Setup
-1. Begin by accessing your group workspace linked to Xero. On the Export tab, ensure that the Preferred Exporter is a Workspace Admin with an email address from your Expensify Cards domain (e.g. company.com).
-2. Head to the Advanced tab and confirm that Auto-Sync is enabled.
-3. Now, go to **Settings > Domains > *Domain Name* > Company Cards > Settings**. From the dropdown menu next to "Preferred Workspace," select the group workspace connected to Xero with Scheduled Submit enabled.
-4. In the dropdown menu for "Expensify Card settlement account," pick your settlement business bank account (found in Expensify under **Settings > Account > Payments**).
-5. In the dropdown menu for "Expensify Card reconciliation account," select the corresponding GL account from Xero for your settlement business bank account from step 4.
-
-### How This Works
-1. During the first overnight Auto Sync after enabling Continuous Reconciliation, Expensify will create a Liability Account (Bank Account) on your Xero Dashboard. If you've opted for Daily Settlement, an additional Clearing Account will be created in your General Ledger. Two Contacts —Expensify and Expensify Card— will also be generated:
-![Xero Contacts](https://help.expensify.com/assets/images/Auto-reconciliation Xero 1.png){:width="100%"}
-2. The bank account for Expensify Card transactions is tied to the Liability Account Expensify created. Note that this doesn't apply to other cards or non-reimbursable expenses, which follow your workspace settings.
-
-### Daily Settlement Reconciliation
-- If you've selected Daily Settlement, Expensify uses entries in the Clearing Account to reconcile the daily settlement. This is because Expensify bills on posted transactions, which you can review via **Settings > Domains > *Domain Name* > Company Cards > Reconciliation > Settlements**.
-- At the end of each day (or month on your settlement date), the settlement charge posts to your Business Bank Account. Expensify assigns the Clearing Account (or Liability Account for monthly settlement) as a Category to the transaction, posting it in your GL. The charge is successfully reconciled.
-
-### Bank Transaction Reconciliation
-- Expensify will pay off the Liability Account with the Clearing Account balance and reconcile bank transaction entries to the Liability Account with your Expense Accounts.
-- When transactions are approved and exported from Expensify, bank transactions (Receive Money) are added to the Liability Account, and coded to the Clearing Account. Simultaneously, Spend Money transactions are created and coded to the Category field. If you see many Credit Card Misc. entries, add commonly used merchants as Contacts in Xero to export with the original merchant name.
-- The Clearing Account balance is reduced, paying off the entries to the Liability Account created in Step 1. Each payment to and from the Liability Account should have a corresponding bank transaction referencing an expense account. Liability Account Receive Money payments appear with "EXPCARD-APPROVAL" and the corresponding Report ID from Expensify.
-- You can run a Bank Reconciliation Summary displaying entries in the Liability Account referencing individual payments, as well as entries that reduce the Clearing Account balance to unapproved expenses.
-- **Important**: To bring your Liability Account balance to 0, enable marking transactions as reconciled in Xero. When a Spend Money bank transaction in the Liability Account has a matching Receive Transaction, you can mark both as Reconciled using the provided hyperlink.
-
-**Note**: If Auto-Reconciliation is disabled for your company's Expensify Cards, a Domain Admin can set an export account for individual cards via **Settings > Domains > *Domain Name* > Company Cards > Edit Exports**. The Expensify Card transactions will always export as a Credit Card charge in your accounting software, regardless of the non-reimbursable setting in their accounting configuration.
-
-## NetSuite
-
-### Initial Setup
-1. Start by accessing your group workspace connected to NetSuite and click on "Configure" under **Connections > NetSuite**.
-2. On the Export tab, ensure that the Preferred Exporter is a Workspace Admin with an email address from your Expensify Cards' domain. For example, if your domain is company.com, the Preferred Exporter's email should be email@company.com.
-3. Head over to the Advanced tab and make sure Auto-Sync is enabled.
-4. Now, go to **Settings > Domains > *Domain Name* > Company Cards > Settings**. From the dropdown menu next to "Preferred Workspace," select the group workspace connected to NetSuite with Scheduled Submit enabled.
-5. In the dropdown menu next to "Expensify Card reconciliation account," choose your existing NetSuite bank account used for reconciliation. This account must match the one set in Step 3.
-6. In the dropdown menu next to "Expensify Card settlement account," select your daily settlement business bank account (found in Expensify under **Settings > Account > Payments**).
-
-### How This Works with Daily Settlement
-1. After setting up the card and running the first auto-sync, we'll create the Expensify Card Liability account and the Expensify Clearing Account within your NetSuite subsidiary general ledger.
-2. During the same sync, if there are newly posted transactions, we'll create a journal entry totaling all posted transactions for the day. This entry will credit the selected bank account and debit the new Expensify Clearing account.
-3. Once transactions are approved in Expensify, the report will be exported to NetSuite, with each line recorded as individual credit card expenses. Additionally, another journal entry will be generated, crediting the Expensify Clearing Account and debiting the Expensify Card Liability account.
-
-### How This Works with Monthly Settlement
-1. After the first monthly settlement, during Auto-Sync, Expensify creates a Liability Account in NetSuite (without a clearing account).
-2. Each time the monthly settlement occurs, Expensify calculates the total purchase amount since the last settlement and creates a Journal Entry that credits the settlement bank account (GL Account) and debits the Expensify Liability Account in NetSuite.
-3. As expenses are approved and exported to NetSuite, Expensify credits the Liability Account and debits the correct expense categories.
-
-**Note**: By default, the Journal Entries created by Expensify are set to the approval level "Approved for posting," so they will automatically credit and debit the appropriate accounts. If you have "Require approval on Journal Entries" enabled in your accounting preferences in NetSuite (**Setup > Accounting > Accounting Preferences**), this will override that default. Additionally, if you have set up Custom Workflows (**Customization > Workflow**), these can also override the default. In these cases, the Journal Entries created by Expensify will post as "Pending approval." You will need to approve these Journal Entries manually to complete the reconciliation process.
-
-### Example
-- Let's say you have card transactions totaling $100 for the day.
-- We create a journal entry:
-![NetSuite Journal Entry](https://help.expensify.com/assets/images/Auto-reconciliation NS 1.png){:width="100%"}
-- After transactions are posted in Expensify, we create the second Journal Entry(ies):
-![NetSuite Second Journal Entry](https://help.expensify.com/assets/images/Auto-reconciliation NS 2.png){:width="100%"}
-- We then reconcile the matching amounts automatically, clearing the balance of the Expensify Clearing Account.
-- Now, you'll have a debit on your Credit Card account (increasing the total spent) and a credit on the bank account (reducing the amount available). The clearing account has a $0 balance.
-- Each expense will also create a Journal Entry, just as we do today, exported upon final approval. This entry will debit the expense account (category) and contain any other line item data.
-- This process happens daily during the NetSuite Auto-Sync to keep your card reconciled.
-
-**Note**: Currently, only Journal Entry export is supported for auto-reconciliation. You can set other export options for all other non-reimbursable spend in the **Configure > Export** tab. Be on the lookout for Expense Report export in the future!
-
-If Auto-Reconciliation is disabled for your company's Expensify Cards, a Domain Admin can set an export account for individual Expensify Cards via **Settings > Domains > Company Cards > Edit Exports**. The Expensify Card transactions will always export as a Credit Card charge in your accounting software, regardless of the non-reimbursable setting in their accounting configuration.
-
-## Sage Intacct
-
-### Initial Setup
-1. Start by accessing your group workspace connected to Sage Intacct and click on "Configure" under **Connections > Sage Intacct**.
-2. On the Export tab, ensure that you've selected a specific entity. To enable Expensify to create the liability account, syncing at the entity level is crucial, especially for multi-entity environments.
-3. Still on the Export tab, confirm that the user chosen as the Preferred Exporter is a Workspace Admin, and their email address belongs to the domain used for Expensify Cards. For instance, if your domain is company.com, the Preferred Exporter's email should be email@company.com.
-4. Head over to the Advanced tab and make sure Auto-Sync is enabled.
-5. Now, go to **Settings > Domains > *Domain Name* > Company Cards > Settings**. From the dropdown menu next to "Preferred Workspace," select the group workspace connected to Sage Intacct with Scheduled Submit enabled.
-6. In the dropdown menu next to "Expensify Card reconciliation account" pick your existing Sage Intacct bank account used for daily settlement. This account must match the one set in the next step.
-7. In the dropdown menu next to "Expensify Card settlement account" select your daily settlement business bank account (found in Expensify under **Settings > Account > Payments**).
-8. Use the dropdown menus to select your cash-only and accrual-only journals. If your organization operates on a cash-only or accrual-only basis, choose "No Selection" for the journals as needed. If your organization uses both cash and accrual methods, please select both a cash-only and an accrual-only journal. Don't forget to save your settings!
-
-### How This Works with Daily Settlement
-1. After setting up the card and running the first auto-sync, we'll create the Expensify Card Expensify Clearing Account within your Sage Intacct general ledger. Once the first card transaction is exported, we'll create a Liability Account.
-2. In the same sync, if there are newly posted transactions from your Expensify Cards, we'll then create a journal entry totaling all posted transactions for the day. This entry will credit the business bank account (set in Step 4 above) and debit the new Expensify Clearing account.
-3. Once Expensify Card transactions are approved in Expensify, the report will be exported to Sage Intacct, with each line recorded as individual credit card expenses. Additionally, another journal entry will be generated, crediting the Expensify Clearing Account and debiting the Expensify Card Liability Account.
-
-### How This Works with Monthly Settlement
-1. After the initial export of a card transaction, Expensify establishes a Liability Account in Intacct (without a clearing account).
-2. Each time a monthly settlement occurs, Expensify calculates the total purchase amount since the last settlement and creates a Journal Entry. This entry credits the settlement bank account (GL Account) and debits the Expensify Liability Account in Intacct.
-3. As expenses are approved and exported to Intacct, Expensify credits the Liability Account and debits the appropriate expense categories.
-
-{% include faq-begin.md %}
-
-## What are the timeframes for auto-reconciliation in Expensify?
-We offer either daily or monthly auto-reconciliation:
-- Daily Settlement: each day, as purchases are made on your Expensify Cards, the posted balance is withdrawn from your Expensify Card Settlement Account (your business bank account).
-- Monthly Settlement: each month, on the day of the month that you enabled Expensify Cards (or switched from Daily to Monthly Settlement), the posted balance of all purchases since the last settlement payment is withdrawn from your Expensify Card Settlement Account (your business bank account).
-
-## Why is my Expensify Card auto-reconciliation not working with Xero?
-When initially creating the Liability and Bank accounts to complete the auto-reconciliation process, we rely on the system to match and recognize those accounts created. You can't make any changes or we will not “find” those accounts.
-
-If you have changed the accounts. It's an easy fix, just rename them!
-- Internal Account Code: must be **ExpCardLbl**
-- Account Type: must be **Bank**
-
-## My accounting integration is not syncing. How will this affect the Expensify Card auto-reconciliation?
-When you receive a message that your accounting solution’s connection failed to sync, you will also receive an email or error message with the steps to correct the sync issue. If you do not, please contact Support for help. When your accounting solution’s sync reconnects and is successful, your auto-reconciliation will resume.
-
-If your company doesn't have auto-reconciliation enabled for its Expensify Cards, you can still set up individual export accounts. Here's how:
-
-1. Make sure you have Domain Admin privileges.
-2. Navigate to **Settings > Domains**
-3. Select 'Company Cards'
-4. Find the Expensify Card you want to configure and choose 'Edit Exports.'
-5. Pick the export account where you want the Expensify Card transactions to be recorded.
-6. Please note that these transactions will always be exported as Credit Card charges in your accounting software. This remains the case even if you've configured non-reimbursable settings as something else, such as a Vendor Bill.
-
-These simple steps will ensure your Expensify Card transactions are correctly exported to the designated account in your accounting software.
-
-## Why does my Expensify Card Liability Account have a balance?
-If you’re using the Expensify Card with auto-reconciliation, your Expensify Card Liability Account balance should always be $0 in your accounting system.
-
-If you see that your Expensify Card Liability Account balance isn’t $0, then you’ll need to take action to return that balance to $0.
-
-If you were using Expensify Cards before auto-reconciliation was enabled for your accounting system, then any expenses that occurred prior will not be cleared from the Liability Account.
-You will need to prepare a manual journal entry for the approved amount to bring the balance to $0.
-
-To address this, please follow these steps:
-1. Identify the earliest date of a transaction entry in the Liability Account that doesn't have a corresponding entry. Remember that each expense will typically have both a positive and a negative entry in the Liability Account, balancing out to $0.
-2. Go to the General Ledger (GL) account where your daily Expensify Card settlement withdrawals are recorded, and locate entries for the dates identified in Step 1.
-3. Adjust each settlement entry so that it now posts to the Clearing Account.
-4. Create a Journal Entry or Receive Money Transaction to clear the balance in the Liability Account using the funds currently held in the Clearing Account, which was set up in Step 2.
-
-{% include faq-end.md %}
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 f24ed57dc655..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
@@ -3,78 +3,55 @@ title: Cardholder Settings and Features
description: Expensify Card Settings for Employees
---
-# How to use your Expensify Visa® Commercial Card
-Once you receive your card, you can start using it right away.
+# Using Your Expensify Visa® Commercial Card
-First, you'll want to take note of the Smart Limit tied to your card – this is listed in your card settings via **Settings > Account > Credit Card Import**. This limit represents the total amount of unapproved expenses you can have on the card.
+### 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.
-It's crucial to continuously submit your expenses promptly, as that'll ensure they can be approved and restore your full limit. You can always chat with your admin if you need your limit adjusted.
+### Review your Card's Smart Limit
+Check your card’s Smart Limit via _**Settings > Account > Credit Card Import**_:
+- This limit is the total amount of unapproved expenses you can have on the card.
+- If a purchase is more than your card's Smart Limit, it will be declined.
-You can swipe your Expensify Card like you would with any other card. As you make purchases, you'll get instant alerts on your phone letting you know if you need to SmartScan receipts. Any SmartScanned receipts should merge with the card expense automatically.
+## Managing Expenses
+- **Submit Expenses Promptly**: Submit your expenses regularly to restore your full limit. Contact your admin if you need a limit adjustment.
+- **Using Your Card**: Swipe your Expensify Card like any other card. You’ll receive instant alerts on your phone for SmartScan receipts. SmartScanned receipts will merge automatically with card expenses.
+- **eReceipts**: If your organization doesn’t require itemized receipts, Expensify will generate IRS-compliant eReceipts for all non-lodging transactions.
+- **Reporting Expenses**: Report and submit Expensify Card expenses as usual. Approved expenses refresh your Smart Limit.
-If your organization doesn't require itemized receipts, you can rely on eReceipts instead. As long as the expense isn't lodging-related, Expensify will automatically generate an IRS-compliant eReceipt for every transaction.
+## Enabling Notifications
+Download the Expensify mobile app and enable push notifications to stay updated on spending activity and potential fraud.
-You can report and submit Expensify Card expenses just like any other expenses. As they're approved, your Smart Limit will be refreshed accordingly, allowing you to keep making purchases.
+#### For iPhone:
+1. Open the Expensify app and tap the three-bar icon in the upper-left corner.
+2. Tap _**Settings > enable Receive real-time alerts**_.
+3. Accept the confirmation to access your iPhone’s notification settings for Expensify.
+4. Turn on **Allow Notifications** and select your notification types.
-## Enable Notifications
-Download the Expensify mobile app and enable push notifications to stay current on your spending activity. 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.
-
-Follow the steps below to enable real-time alerts on your mobile device.
-
-**If you have an iPhone**:
-1. Open the Expensify app and tap the three-bar icon in the upper-left corner
-2. Tap **Settings** and enable **Receive realtime alerts**
-3. Accept the confirmation dialogue to go to your iPhone's notification settings for Expensify. Turn on Allow Notifications, and choose the notification types you’d like!
-
-**If you have an Android**:
-1. Go to Settings and open 'Apps and Notifications'.
+#### For Android:
+1. Go to _**Settings > Apps and Notifications**_.
2. Find and open Expensify and enable notifications.
-3. Customize your alerts. Depending on your phone model, you may have extra options to customize the types of notifications you receive.
-
-## Your virtual card
-Once you're assigned a limit, you'll be able to use your virtual card immediately. You can view your virtual card details via **Settings > Account > Credit Card Import > Show Details**. Keep in mind that your virtual card and physical card share a limit.
-
-The virtual Expensify Card includes a card number, expiration date, and security code (CVC). You can use the virtual card for online purchases, in-app transactions, and in-person payments once it's linked to a mobile wallet (Apple Pay or Google Pay).
-
-## How to access your virtual card details
-Here's how to access your virtual card details via the Expensify mobile app:
-1. Tap the three-bar icon in the upper-left corner
-2. Tap **Settings > Connected Cards**
-3. Under **Virtual Card**, tap **Show Details**
-
-From there, you can view your virtual card's number, CVV, expiration date, and billing address.
-
-Here's how to access your virtual card details via the Expensify web app:
-1. Head to **Settings > Account > Credit Card Import**
-2. Under **Virtual Card**, click **Show Details**
-
-From there, you can view your virtual card's card number, CVV, expiration date, and billing address.
-
-## How to add your virtual card to a digital wallet (Apple Pay or Google Pay)
-
-To use the Expensify Card for contactless payment, add it to your digital wallet from the mobile app:
-1. Tap the three-bar icon in the upper-left corner
-2. Tap **Settings > Connected Cards**
-3. Depending on your device, tap **Add to Apple Wallet** or **Add to Gpay**
-4. Complete the remaining steps
-
-## Expensify Card declines
-As long as you've enabled 'Receive real-time alerts', you'll get a notification explaining the reason for each decline. You can enable alerts in the mobile app by clicking on the three-bar icon in the upper-left corner > **Settings** > toggle **Receive real-time alerts**.
-
-Here are some reasons an Expensify Card transaction might be declined:
-
-- You have an insufficient card limit
- - If a transaction exceeds your Expensify Card's available limit, the transaction will be declined. You can see the remaining limit in the mobile app under **Settings > Connected Cards** or in the web app under **Settings > Account > Credit Card Import**.
- - Submitting expenses and getting them approved will free up your limit for more spending.
-
-- Your card isn't active yet or it was disabled by your Domain Admin
-- Your card information was entered incorrectly with the merchant. Entering incorrect card information, such as the CVC, ZIP, or expiration date, will also lead to declines.
-There was suspicious activity
-- If Expensify detects unusual or suspicious activity, we may block transactions as a security measure
- - This could happen due to irregular spending patterns, attempted purchases from risky vendors, or multiple rapid transactions.
- - Check your Expensify Home page to approve unusual merchants and try again.
- - If the spending looks suspicious, we may complete a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens.
-- The merchant is located in a restricted country
+3. Customize your alerts based on your phone model.
+
+## Using Your Virtual Card
+- **Access Details**: You can view your virtual card details (card number, expiration date, CVC) via _**Settings > Account > Credit Card Import > Show Details**_. The virtual and physical cards share the same limit.
+- **Purchases**: Use the virtual card for online, in-app, and in-person payments when linked to a mobile wallet (Apple Pay or Google Pay).
+
+#### Adding to a Digital Wallet
+To add your Expensify Card to a digital wallet, follow the steps below:
+ 1. Tap the three-bar icon in the upper-left corner.
+ 2. Tap _**Settings > Connected Cards**_.
+ 3. Tap **Add to Apple Wallet** or **Add to Gpay**, depending on your device.
+ 4. Complete the steps as prompted.
+
+## Handling Declines
+- **Real-Time Alerts**: Enable real-time alerts in the mobile app (_**Settings > toggle Receive real-time alerts**_) to get notifications for declines.
+- **Common Decline Reasons**:
+ - **Insufficient Limit**: Transactions exceeding the available limit will be declined. You can check your limit in _**Settings > Connected Cards**_ or _**Settings > Account > Credit Card Import**_.
+ - **Inactive or Disabled Card**: Ensure your card is active and not disabled by your Domain Admin.
+ - **Incorrect Information**: Entering incorrect card details (CVC, ZIP, expiration date) will result in declines.
+ - **Suspicious Activity**: Transactions may be blocked for unusual or suspicious activity. Check the Expensify Home page to approve unusual merchants. Suspicious spending may prompt a manual due diligence check, during which your cards will be locked.
+ - **Restricted Country**: Transactions from restricted countries will be declined.
{% include faq-begin.md %}
## Can I use Smart Limits with a free Expensify account?
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/Expensify-Card-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation.md
new file mode 100644
index 000000000000..81eae56fa774
--- /dev/null
+++ b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation.md
@@ -0,0 +1,108 @@
+---
+title: Expensify Card reconciliation
+description: Reconcile expenses from Expensify Cards
+---
+
+
+
+To handle unapproved Expensify Card expenses that are left after you close your books for the month, you can set up auto-reconciliation with an accounting integration, or you can manually reconcile the expenses.
+
+# Set up automatic reconciliation
+
+Auto-reconciliation automatically deducts Expensify Card purchases from your company’s settlement account on a daily or monthly basis.
+
+{% include info.html %}
+You must link a business bank account as your settlement account before you can complete this process.
+{% include end-info.html %}
+
+1. Hover over Settings, then click **Domains**.
+2. Click the desired domain name.
+3. On the Company Cards tab, click the dropdown under the Imported Cards section to select the desired Expensify Card.
+4. To the right of the dropdown, click the **Settings** tab.
+5. Click the Expensify Card settlement account dropdown and select your settlement business bank account.
+ - To verify which account is your settlement account: Hover over Settings, then click **Account**. Click the **Payments** tab on the left and verify the bank account listed as the Settlement Account. If these accounts do not match, repeat the steps above to select the correct bank account.
+6. Click **Save**.
+
+If your workspace is connected to a QuickBooks Online, Xero, NetSuite, or Sage Intacct integration, complete the following additional steps.
+
+1. Click the Expensify Card Reconciliation Account dropdown and select the GL account from your integration for your Settlement Account. Then click **Save**.
+2. (Optional) If using the Sage Intacct integration, select your cash-only and accrual-only journals. If your organization operates on a cash-only or accrual-only basis, choose **No Selection** for the journals that do not apply.
+3. Click the **Advanced** tab and ensure Auto-Sync is enabled. Then click **Save**
+4. Hover over **Settings**, then click **Workspaces**.
+5. Open the workspace linked to the integration.
+6. Click the **Connections** tab.
+7. Next to the desired integration, click **Configure**.
+8. Under the Export tab, ensure that the Preferred Exporter is also a Workspace Admin and has an email address associated with your Expensify Cards' domain. For example, if your domain is company.com, the Preferred Exporter's email should be name@company.com.
+
+# Manually reconcile expenses
+
+To manually reconcile Expensify Card expenses,
+
+1. Hover over Settings, then click **Domains**.
+2. Click the desired domain name.
+3. On the Company Cards tab, click the dropdown under the Imported Cards section to select the desired Expensify Card.
+4. To the right of the dropdown, click the **Reconciliation** tab.
+5. For the Reconcile toggle, ensure Expenses is selected.
+6. Select the start and end dates, then click **Run**.
+7. Use the Imported, Approved, and Unapproved totals to manually reconcile your clearing account in your accounting system.
+ - The Unapproved total should match the final clearing account balance. Depending on your accounting policies, you can use this balance to book an accrual entry by debiting the appropriate expense and crediting the offsetting clearing account in your accounting system.
+
+## Troubleshooting
+
+Use the steps below to do additional research if:
+- The amounts vary to a degree that needs further investigation.
+- The Reconciliation tab was not run when the accounts payable (AP) was closed.
+- Multiple subsidiaries within the accounting system closed on different dates.
+- There are foreign currency implications in the accounting system.
+
+To do a more in-depth reconciliation,
+
+1. In your accounting system, lock your AP.
+
+{% include info.html %}
+It’s best to do this step at the beginning or end of the day. Otherwise, expenses with the same export date may be posted in different accounting periods.
+{% include end-info.html %}
+
+2. In Expensify, click the **Reports** tab.
+3. Set the From date filter to the first day of the month or the date of the first applicable Expensify Card expense, and set the To date filter to today’s date.
+4. Set the other filters to show **All**.
+5. Select all of the expense reports by clicking the checkbox to the top left of the list. If you have more than 50 expense reports, click **Select All**.
+6. In the top right corner of the page, click **Export To** and select **All Data - Expense Level Export**. This will generate and send a CSV report to your email.
+7. Click the link from the email to automatically download a copy of the report to your computer.
+8. Open the report and apply the following filters (or create a pivot with these filters) depending on whether you want to view the daily or monthly settlements:
+ - Daily settlements:
+ - Date = the month you are reconciling
+ - Bank = Expensify Card
+ - Posted Date = the month you are reconciling
+ - [Accounting system] Export Non Reimb = blank/after your AP lock date
+ - Monthly settlements:
+ - Date = the month you are reconciling
+ - Bank = Expensify Card
+ - Posted Date = The first date after your last settlement until the end of the month
+ - [Accounting system] Export Non Reimb = the current month and new month until your AP lock date
+ - To determine your total Expensify Card liability at the end of the month, set this filter to blank/after your AP lock date.
+
+This filtered list should now only include Expensify Card expenses that have a settlement/card payment entry in your accounting system but don’t have a corresponding expense entry (because they have not yet been approved in Expensify). The sum is shown at the bottom of the sheet.
+
+The sum of the expenses should equal the balance in your Expensify Clearing or Liability Account in your accounting system.
+
+# Tips
+
+- Enable [Scheduled Submit](https://help.expensify.com/articles/expensify-classic/workspaces/reports/Scheduled-Submit) to ensure that expenses are submitted regularly and on time.
+- Expenses that remain unapproved for several months can complicate the reconciliation process. If you're an admin in Expensify, you can communicate with all employees who have an active Expensify account by going to [new.expensify.com](http://new.expensify.com) and using the #announce room to send a message. This way, you can remind employees to ensure their expenses are submitted and approved before the end of each month.
+- Keep in mind that although Expensify Card settlements/card payments will post to your general ledger on the date it is recorded in Expensify, the payment may not be withdrawn from your bank account until the following business day.
+- Based on your internal policies, you may want to accrue for the Expensify Cards.
+
+{% include faq-begin.md %}
+
+**Why is the amount in my Expensify report so different from the amount in my accounting system?**
+
+If the Expensify report shows an amount that is significantly different to your accounting system, there are a few ways to identify the issues:
+- Double check that the expenses posted to the GL are within the correct month. Filter out these expenses to see if they now match those in the CSV report.
+- Use the process outlined above to export a report of all the transactions from your Clearing (for Daily Settlement) or Liability (for monthly settlement) account, then create a pivot table to group the transactions into expenses and settlements.
+ - Run the settlements report in the “settlements” view of the Reconciliation Dashboard to confirm that the numbers match.
+ - Compare “Approved” activity to your posted activity within your accounting system to confirm the numbers match.
+
+{% include faq-end.md %}
+
+
diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
index 724745f458ef..1f412665fc2f 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,36 @@
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!”
+ - Completing that task will send an in-product notification to your admin team that you requested the card.
+ - Once you’re assigned a card limit, you’ll receive an email notification. Click the link in the email to provide your shipping address on your account’s homepage.
+ - Enter your address, and the physical card will be shipped within 3-5 business days.
+3. **Activate the Card:** When your physical card arrives, activate it in Expensify by entering the last four digits of the card in the activation task on your homepage.
+
+### Virtual Cards
+Once you've been assigned a limit, a virtual card is available immediately. You can view its details via _**Settings > Account > Credit Card Import > Show Details**_.
+
+To protect your account and card spend, enable two-factor authentication under _**Settings > Account > Account Details**_.
+
+### Notifications
+- Download the Expensify mobile app and enable push notifications to stay updated on your card’s limit and spending.
+- Each transaction triggers a push notification.
+- You’ll also get notifications for potentially fraudulent activity, allowing you to confirm or dispute charges.
+
+## Request a Replacement Expensify Card
+### If the card is lost, stolen, or damaged Card:
+ - Go to _**Settings > Account > Credit Card Import** and click **Request a New Card**_.
+ - Confirm your shipping information and complete the prompts. The new card will arrive in 2-3 business days.
+ - Selecting “lost” or “stolen” deactivates your current card to prevent fraud. Choosing “damaged” keeps the current card active until the new one arrives.
+ - If you can’t access the website or app, call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally) to cancel your card.
+
+### If the card is expiring
+- If your card is about to expire, Expensify will notify you via your account’s Home (Inbox) tab.
+- Enter your address if it has changed; otherwise, do nothing, and the new card will ship to your address on file.
+- The new card will have a unique number and will not be linked to the old one.
{% include faq-begin.md %}
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/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md
index 239da6518be7..fdbc178737e1 100644
--- a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md
+++ b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md
@@ -66,6 +66,8 @@ There are two different limit types that are best suited for their intended purp
- _Fixed limit_ spend cards are ideal for one-time expenses or providing employees access to a card for a designated purchase.
- _Monthly_ limit spend cards are perfect for managing recurring expenses such as subscriptions and memberships.
+A virtual card with either of these limit types doesn't share its limit with any other cards, including the cardholder's smart limit cards.
+
**Where can employees see their virtual cards?**
Employees can see their assigned virtual cards by navigating to **Settings** > **Account** > [**Credit Cards Import**](https://www.expensify.com/settings?param=%7B%22section%22:%22creditcards%22%7D) in their account.
diff --git a/docs/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings.md b/docs/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings.md
new file mode 100644
index 000000000000..aea84d338934
--- /dev/null
+++ b/docs/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings.md
@@ -0,0 +1,55 @@
+---
+title: Configure Reimbursement Settings
+description: Set up direct or indirect reimbursements for your workspace.
+---
+
+
+Reimbursing employees in Expensify is quick, easy, and completely free. Let Expensify do the tedious work for you by taking advantage of the features available to automate employee reimbursement.
+
+# Configure a Workspace's Reimbursement Settings
+There are a few ways to reimburse employees in Expensify. The option that's best suited for you and your business will depend on a few different factors:
+- **Direct Reimbursement**: For companies with a business bank account located in the US that reimburse employees within the US.
+- **Indirect Reimbursement**: This option is available to all members, and connecting a bank account to Expensify is not required. Indirect reimbursement indicates that all reports are reimbursed outside of Expensify.
+- **Global Reimbursement**: If your company’s business bank account is in the US, Canada, the UK, Europe, or Australia, you can reimburse employees directly in nearly any country worldwide.
+
+## Set Up Direct Reimbursement
+
+Once a [business bank account is connected to Expensify](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD#how-to-add-a-verified-business-bank-account), a workspace admin can enable indirect reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Direct**.
+
+#### Additional features available with Direct Reimbursement:
+- **Select a default reimburser for the Workspace from the dropdown menu**:
+ - The default reimburser will receive notifications to reimburse reports in Expensify.
+ - Any workspace admin who also has access to the business bank account can be added as a default reimburser.
+- **Set a default withdrawal account for the Workspace**:
+ - The default bank account is used to reimburse all of the reports submitted on the corresponding workspace.
+- **Set a manual reimbursement threshold to automate reimbursement**:
+ - If the total of a given report is less than the threshold set, reimbursement will occur automatically upon final approval.
+ - If the total of a given report is more than the threshold, it will need to be reimbursed manually.
+
+## Set Up Indirect Reimbursement
+
+A Workspace admin can enable indirect reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Indirect**.
+
+**Additional features under Reimbursement > Indirect:**
+If you reimburse through a separate system or through payroll, Expensify can collect and export employee bank account details for you. Reach out to your Account Manager or Concierge to have the Reimbursement Details Export format added to the account.
+
+## Set Up Global Reimbursement
+
+Once [a business bank account is connected to Expensify](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD#how-to-add-a-verified-business-bank-account), a workspace admin can enable indirect reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Direct > Enable Global Reimbursements**.
+
+More information on setting up global reimbursements can be found **[here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements)**.
+
+{% include faq-begin.md %}
+
+## How do I export employee bank account details once the Reimbursement Details Export format is added to my account?
+
+Employee bank account details can be exported from the Reports page by selecting the relevant Approved reports and then clicking **Export to > Reimbursement Details Export**.
+
+## Is it possible to change the name of a verified business bank account in Expensify?
+
+Bank account names can be updated by going to _**Settings > Accounts > Payments**_ and clicking the pencil icon next to the bank account name.
+
+## What is the benefit of setting a default reimburser?
+
+Setting a default reimburser on the Workspace ensures that all outstanding reports are reimbursed as this member will receive notifications alerting them to reports that require their action.
+{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/workspaces/Expenses.md b/docs/articles/expensify-classic/workspaces/Expense-Settings.md
similarity index 61%
rename from docs/articles/expensify-classic/workspaces/Expenses.md
rename to docs/articles/expensify-classic/workspaces/Expense-Settings.md
index 4a2dc56c430f..c3a8ab31394d 100644
--- a/docs/articles/expensify-classic/workspaces/Expenses.md
+++ b/docs/articles/expensify-classic/workspaces/Expense-Settings.md
@@ -2,19 +2,29 @@
title: Expensify Workspace Expense Settings
description: Expense Settings
---
-# Overview
+Expensify offers multiple ways to customize how expenses are created and managed at the workspace level. Whether you’re using an individual workspace or managing expenses in a group workspace, there are various expense settings you can customize.
-Expensify offers multiple ways to customize how expenses are created in your workspace. In this doc, you’ll learn how to set up expense basics, distance expenses, and time expenses.
+# Set up the expense settings on a workspace
-Whether you’re flying solo with your Individual workspace or submitting with a team on your Group workspace, we have settings to support how you use Expensify.
+You can manage the expense settings on a workspace under **Settings** > **Workspaces** > **Individual** or **Group** > [_Workspace Name_] > **Expenses**. From here you can customize the following expense-level settings:
+- **Violations**: When enabled, employee expenses that fall outside of workspace preferences are flagged as violations.
+- **Preferences**: Configure the reimbursable and billable settings for the expenses submitted to the corresponding workspace.
+- **Distance**: This is where you can set the reimbursable mileage rates for yourself or your employees.
+- **Time**: Set an hourly billable rate so members of the workspace can create time expenses for reimbursement.
-# How to manage expense settings in your workspace
+## Violations
+A workspace admin can customize the following parameters at the expense level:
+- **Max Expense Age (Days)**
+- **Max Expense Amount**
+- **Receipt Required Amount**
-Let’s cover the expense basics first! In the following sections, we’ll go through each part of managing expense settings in your workspace.
+If an expense is submitted that falls outside of those parameters, Expensify will automatically detect it as a violation and alert both the expense creator and reviewer that it needs to be corrected.
-## Controlling cash expenses
+More information on violations can be found [**here**](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations).
-A cash expense is any expense created manually or by uploading a receipt for SmartScan; a cash expense does not mean the expense was paid for with cash. The other type of expense you’ll most commonly see is credit card expenses, which means the expenses imported from a credit card or bank connection.
+## Preferences
+
+A cash expense is any expense created manually or by uploading a receipt for SmartScan; it does not mean the expense was paid for with cash. The other type of expense you’ll most commonly see is credit card expenses, which are expenses imported from a credit card or bank connection.
There are four options for cash expenses:
@@ -23,7 +33,7 @@ There are four options for cash expenses:
- **Forced always reimbursable** - All cash expenses are forced to be reimbursable; they cannot be marked as non-reimbursable.
- **Forced always non-reimbursable** - All cash expenses are forced to be non-reimbursable; they cannot be marked as reimbursable.
-## Setting up billable expenses
+### Billable expenses
Billable expenses refer to expenses you or your employees incur that need to be re-billed to a specific client or vendor.
@@ -37,7 +47,7 @@ Under Expense Basics, you can choose the setting that is best for you.
If your Group workspace is connected to Xero, QuickBooks Online, NetSuite, or Sage Intacct, you can export billable expenses to be invoiced to customers. To set this up, go to the Coding tab in the connection configuration settings.
-## Using eReceipts
+### eReceipts
eReceipts are full digital replacements of their paper equivalents for purchases of $75 or less.
@@ -46,65 +56,57 @@ Click the toggle to your preferred configuration.
- **Enabled** - All imported credit card expenses in US dollars of $75 or less will have eReceipts in the receipt image.
- **Disabled** - No expenses will generate an eReceipt.
-Note: _We will not generate an eReceipt for lodging expenses._
+Note: Expensify will not generate an eReceipt for lodging expenses.
-## Securing receipt images
+### Secure receipt images
Whether you’re sharing your receipts with your accountant, having an auditor review exported expenses, or simply wanting to export to keep a hard copy for yourself, receipt visibility will be an essential consideration.
Under _Public Receipt Visibility_, you can determine who can view receipts on your workspace.
- **Enabled** means receipts are viewable by anyone with the URL. They don't need to be an Expensify user or a workspace member to view receipts.
-- **Disabled** means receipts are viewable by users of Expensify, who would have access to view the receipt in the application. You must be an Expensify user with access to the report a receipt is on and logged into your account to view a receipt image via URL.
+- **Disabled** means receipts are viewable by Expensify users, who would have access to view the receipt in the application. You must be an Expensify user with access to the report a receipt is on and logged into your account to view a receipt image via URL.
-## Track mileage expenses
+## Distance Expenses
+How to set up distance expenses:
+1. Select whether you want to capture _miles_ or _kilometers_,
+2. Set the default category to be used on distance expenses,
+3. Click **Add A Mileage Rate** to add as many rates as you need,
+4. Set the reimbursable amount per mile or kilometer.
-Whether using the Individual or Group workspace, you can create distance rates to capture expenses in miles or kilometers.
+**Note:** If a rate is toggled off it is immediately disabled. This means that users are no longer able to select it when creating a new distance expense. If only one rate is available then that rate will be toggled on by default.
-Preliminary setup steps include:
+### Track tax on mileage expenses
+If you’re tracking tax in Expensify you can also track tax on distance expenses. The first step is to enable tax in the workspace. You can do this by going to **Settings** > **Workspaces** > **Individual** or **Group** > [_Workspace Name_] > **Tax**.
-1. Selecting whether you want to capture _miles_ or _kilometers_,
-2. Setting the default category to be used on distance expenses,
-3. Click **Add A Mileage Rate** to add as many rates as you need,
-4. Set the reimbursable amount per mile or kilometer.
+Once tax is enabled on a workspace level you will see a toggle to _Track Tax_ in the Distance section of the workspace settings. If tax is disabled on the workspace the Track Tax toggle will not display.
-Note: _If a rate is toggled off it is immediately disabled. This means that users are no longer able to select it when creating a new distance expense. If only one rate is available then that rate will be toggled on by default._
+When Track Tax is enabled, you will need to enter additional information about the rates you have set. This includes the _Tax Reclaimable on_ and _Tax Rate_ fields. With that information, Expensify will work out the correct tax reclaim for each expense.
-## Set an hourly rate
+If you enable tax but don’t select a tax rate or enter a tax reclaimable amount, we will not calculate any tax amount for that rate. If, at any point, you switch the tax rate or enter a different reclaimable portion for an existing distance rate, the mileage rate will need to be re-selected on expenses for the tax amount to update according to the new values.
-Using Expensify you can track time-based expenses to bill your clients at an hourly rate or allow employees to claim an hourly stipend.
+**Note:** Expensify won’t automatically track cumulative mileage. If you need to track cumulative mileage per employee, we recommend building a mileage report using our custom export formulas.
-Click the toggle under the _Time_ section to enable the feature and set a default hourly rate. After that, you and your users will be able to create time-based expenses from the [**Expenses**](https://expensify.com/expenses) page of the account.
+## Time Expenses
-# Deep dives
+Using Expensify you can track time-based expenses to bill your clients at an hourly rate or allow employees to claim an hourly stipend.
-## What is Concierge Receipt Audit for the Control Plan?
+Click the toggle under the _Time_ section to enable the feature and set a default hourly rate. Then, you and your users can create time-based expenses from the [**Expenses**](https://expensify.com/expenses) page of the account.
-Concierge Receipt Audit is a real-time audit and compliance of receipts submitted by employees and workspace users. Concierge checks every receipt for accuracy and compliance, flagging any expenses that seem fishy before expense reports are even submitted for approval. All risky expenses are highlighted for manual review, leaving you with more control over and visibility into expenses. When a report is submitted and there are risky expenses on it, you will be immediately prompted to review the risky expenses and determine the next steps.
+## Concierge Receipt Audit
-**Why you should use Concierge Receipt Audit**
+Concierge Receipt Audit is a real-time audit and compliance of receipts submitted by employees and workspace users. Concierge checks every receipt for accuracy and compliance, flagging any expenses that seem fishy before expense reports are even submitted for approval. All risky expenses are highlighted for manual review, leaving you with more control over and visibility into expenses. When a report is submitted and there are risky expenses on it, you will be immediately prompted to review the risky expenses and determine the next steps.
+**Benefits of Concierge Receipt Audit**
- To make sure you don't miss any risky expenses that need human oversight.
- To avoid needing to manually review all your company receipts.
- It's included at no extra cost with the [Control Plan](https://www.expensify.com/pricing).
- Instead of paying someone to audit your company expenses or being concerned that your expenses might be audited by a government agency.
-- It's easy to use! Concierge will alert you to the risky expense and present it to you in an easy-to-follow review tutorial.
+- It's easy -- Concierge will alert you to the risky expense and present it to you in an easy-to-follow review tutorial.
- In addition to the risky expense alerts, Expensify will include a Note with audit details on every report.
-Note: _If a report has audit alerts on it, you'll need to Review the report and Accept the alerts before it can be approved._
-
-## Tracking tax on mileage expenses
-
-If you’re tracking tax in Expensify you can also track tax on distance expenses. The first step is to enable tax in the workspace. You can do this by going to **Settings** > **Workspaces** > **Individual** or **Group** > [_Workspace Name_] > **Tax**.
-
-Once tax is enabled on a workspace level you will see a toggle to _Track Tax_ in the Distance section of the workspace settings. If tax is disabled on the workspace the Track Tax toggle will not display.
-
-When Track Tax is enabled you will need to enter additional information to the rates you have set, this includes the _Tax Reclaimable on_ and _Tax Rate_ fields. With that information, Expensify will work out the correct tax reclaim for each expense.
-
-If you enable tax but don’t select a tax rate or enter a tax reclaimable amount, we will not calculate any tax amount for that rate. If, at any point, you switch the tax rate or enter a different reclaimable portion for an existing distance rate, the mileage rate will need to be re-selected on expenses for the tax amount to update according to the new values.
-
-Note: _Expensify won’t automatically track cumulative mileage. If you need to track cumulative mileage per employee, we recommend building a mileage report using our custom export formulas._
+**Note:** If a report has audit alerts on it, you'll need to Review the report and Accept the alerts before it can be approved.
{% include faq-begin.md %}
diff --git a/docs/articles/expensify-classic/workspaces/Invoicing.md b/docs/articles/expensify-classic/workspaces/Invoicing.md
deleted file mode 100644
index f692f8f8d62e..000000000000
--- a/docs/articles/expensify-classic/workspaces/Invoicing.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-title: Expensify Invoicing
-description: Expensify Invoicing supports your business with unlimited invoice sending and receiving, payments, and status tracking in one single location.
----
-# Overview
-Expensify Invoicing lets you create and send invoices, receive payments, and track the status of your invoices with Expensify, regardless of whether your customer has an Expensify account. Invoicing is included with all Expensify subscriptions, no matter the plan — just pay the processing fee (2.9%) per transaction.
-
-# How to Set Up Expensify Invoicing
-
-**If you have a Group Workspace:**
-
-1. Log into your Expensify account from the web (not the mobile app)
-3. Head to **Settings** > **Workspaces** > **Group** > [_Workspace Name_] > [**Invoices**](https://expensify.com/policy?param={"policyID":"20AB6A03EB9CE54D"}#invoices).
-
-**If you have an Individual Workspace:**
-
-1. Log into your Expensify account from the web (not the mobile app)
-2. Head to **Settings** > **Workspaces** > **Individual** > [_Workspace Name_]> [**Invoices**](https://expensify.com/policy?param={"policyID":"BD5FB746D3B220D6"}#invoices).
-
-Here, you’ll be able to create a markup or add a payment account. Don’t forget you need a verified bank account to send or accept invoice payments via ACH.
-
-# Deep Dive
-
-To help your invoice stand out and look more professional, you can:
-
-- Add your logo
-- Set your workspace currency to add default report-level fields
-- Create additional report-level fields to display more details
-
-## Add a Logo
-
-From your Expensify account on the web (not the mobile app), go to **Settings** > **Account** > **Account Details**. Then click **Edit Photo** under _Your Details_ to upload your logo.
-
-## Set the Workspace Currency
-
-To set your currency, head to **Settings** > **Workspaces** > **Individual** or **Group** > **Reports**. This will add default report-level fields to your invoices. You can see these at the bottom of your [**Reports**](https://expensify.com/reports) page.
-
-Here are the default report-level fields based on common currencies:
-
-- GBP: VAT Number & Supplier Address (your company address)
-- EUR: VAT Number & Supplier Address (your company address)
-- AUD: ABN Number & Supplier Address (your company address)
-- NZD: GST Number & Supplier Address (your company address)
-- CAD: Business Number & Supplier Address (your company address)
-
-## Adding Additional Fields to Your Invoices
-
-In addition to the default report-level fields, you can create custom invoice fields.
-
-At the bottom of the same Reports page, under the _Add New Field_ section, you’ll have multiple options.
-
-- **Field Title**: This is the name of the field as displayed on your invoice.
-- **Type**: You have the option to select a _text-based_ field, a _dropdown_ of selections, or a _date_ selector.
-- **Report Type**: Select _Invoice_ to add the field to your invoices.
-
-Don’t forget to click the **Add** button once you’ve set your field parameters!
-
-For example, you may want to add a PO number, business address, website, or any other custom fields.
-
-_Please check the regulations in your local jurisdiction to ensure tax and business compliance._
-
-## Removing Fields from Your Invoices
-
-If you want to delete a report field, click the red trashcan on the field in your **Workspace** > **Individual** or **Group** > **Report** settings to remove it from all future invoices. Unsent invoices will have a red **X** next to the report field, which you can click to remove before sending the invoice to your customer.
diff --git a/docs/articles/expensify-classic/workspaces/Reimbursement.md b/docs/articles/expensify-classic/workspaces/Reimbursement.md
deleted file mode 100644
index ed2384d12006..000000000000
--- a/docs/articles/expensify-classic/workspaces/Reimbursement.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-title: Reimbursement
-description: Enable reimbursement and reimburse expense reports
----
-
-
-# Overview
-Reimbursement in Expensify is quick, easy, and completely free. Let Expensify do the tedious work for you by taking advantage of features to automate employee reimbursement.
-
-# How to Enable Reimbursement
-There are several options for reimbursing employees in Expensify. The options available will depend on which country your business bank account is domiciled in.
-
-## Direct Reimbursement
-
-Direct reimbursement is available to companies who have a verified US bank account and are reimbursing employees within the US. To use direct reimbursement, you must have a US business bank account verified in Expensify.
-
-A Workspace admin can enable direct reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Direct**.
-
-**Additional features under Reimbursement > Direct:**
- - Select a **default reimburser** for the Workspace from the dropdown menu. The default reimburser is the person who will receive notifications to reimburse reports in Expensify. You’ll be able to choose among all Workspace Admins who have access to the business bank account.
- - Set a **default withdrawal account** for the Workspace. This will set a default bank account that report reimbursements are withdrawn from.
- - Set a **manual reimbursement threshold** to automate reimbursement. Reports whose total falls under the manual reimbursement threshhold will be reimbursed automatocally upon final approval; reports whose total falls above the threshhold will need to be reimbursed manually by the default reimburser.
-
-Expensify also offers direct global reimbursement to some companies with verified bank accounts in USD, GBP, EUR and AUD who are reimbursing employees internationally. For more information about Global Reimbursement, see LINK
-
-## Indirect Reimbursement
-
-Indirect reimbursement is available to all companies in Expensify and no bank account is required. Indirect reimbursement indicates that the report will be reimbursed outside of Expensify.
-
-A Workspace admin can enanble indirect reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Indirect**.
-
-**Additional features under Reimbursement > Indirect:**
-If you reimburse through a seperate system or through payroll, Expensify can collect and export employee bank account details for you. Just reach out to your Account Manager or concierge@expensify.com for us to add the Reimbursement Details Export format to the account.
-
-{% include faq-begin.md %}
-
-## How do I export employee bank account details once the Reimbursement Details Export format is added to my account?
-
-Employee bank account details can be exported from the Reports page by selecting the relevant Approved reports and then clicking **Export to > Reimbursement Details Export**.
-
-## Is it possible to change the name of a verified business bank account in Expensify?
-
-Bank account names can be updated via **Settings > Accounts > Payments** and clicking the pencil icon next to the bank account name.
-
-## What is the benefit of setting a default reimburser?
-
-The main benefit of being defined as the "reimburser" in the Workspace settings is that this user will receive notifications on their Home page alerting them when reports need to be reimbursed.
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/workspaces/Set-Up-Invoicing.md b/docs/articles/expensify-classic/workspaces/Set-Up-Invoicing.md
new file mode 100644
index 000000000000..8ec279da29a6
--- /dev/null
+++ b/docs/articles/expensify-classic/workspaces/Set-Up-Invoicing.md
@@ -0,0 +1,51 @@
+---
+title: Expensify Invoicing
+description: Expensify Invoicing offers the ability to send, receive, and track the status of payments in one location.
+---
+Invoicing lets you create and send invoices, receive payments, and track the status of your invoices, regardless of whether the customer has an Expensify account. This feature is included with all Expensify subscriptions, no matter the plan — you'll just pay the processing fee (2.9%) per transaction.
+
+# Set Up Expensify Invoicing
+Before using the invoice feature, you'll need to [connect a business bank account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD) to Expensify.
+
+Then, do the following:
+1. Log into your Expensify account from the web (not the mobile app)
+2. Head to _**Settings > Workspaces > Workspace Name > [Invoices](https://expensify.com/policy?param={"policyID":"20AB6A03EB9CE54D"}#invoices)**_.
+
+Here, you’ll be able to create a markup or add a payment account.
+
+## Add a Logo
+
+From your Expensify account on the web, go to _**Settings > Account > Account Details**_. Then click **Edit Photo** under _Your Details_ to upload your company logo.
+
+## Set the Workspace Currency
+
+To set the currency, head to _**Settings** > **Workspaces** > **Reports**_. This will add default report-level fields to your invoices. You can see these at the bottom of the [**Reports**](https://expensify.com/reports) page.
+
+Below are the default report-level fields based on common currencies:
+- GBP: VAT Number & Supplier Address (your company address)
+- EUR: VAT Number & Supplier Address (your company address)
+- AUD: ABN Number & Supplier Address (your company address)
+- NZD: GST Number & Supplier Address (your company address)
+- CAD: Business Number & Supplier Address (your company address)
+
+## Adding Additional Fields to Invoices
+
+In addition to the default report-level fields, you can create custom invoice fields.
+
+At the bottom of the same Reports page, under the _Add New Field_ section, you’ll have multiple options.
+
+- **Field Title**: This is the name of the field as displayed on your invoice.
+- **Type**: You have the option to select a _text-based_ field, a _dropdown_ of selections, or a _date_ selector.
+- **Report Type**: Select _Invoice_ to add the field to your invoices.
+
+Don’t forget to click the **Add** button once you’ve set your field parameters!
+
+For example, you may want to add a PO number, business address, website, or any other custom fields.
+
+_Please check the regulations in your local jurisdiction to ensure tax and business compliance._
+
+## Removing Fields from Invoices
+
+If you want to delete a report field, click the red trashcan on the field under _**Settings** > **Workspaces** > **Reports**_. This will remove that field from all future invoices.
+
+Unsent invoices will have a red **X** next to the report field, which you can click to remove before sending the invoice to your customer.
diff --git a/docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md b/docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md
new file mode 100644
index 000000000000..5c6678e068be
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md
@@ -0,0 +1,375 @@
+---
+title: Set up NetSuite connection
+description: Integrate NetSuite with Expensify
+---
+
+
+# Connect to NetSuite
+
+## Overview
+Expensify’s integration with NetSuite allows you to sync data between the two systems. Before you start connecting Expensify with NetSuite, there are a few things to note:
+
+- You must use NetSuite administrator credentials to initiate the connection
+- A Control Plan in Expensify is required to integrate with NetSuite
+- Employees don’t need NetSuite access or a NetSuite license to submit expense reports and sync them to NetSuite
+- Each NetSuite subsidiary must be connected to a separate Expensify workspace
+- The workspace currency in Expensify must match the NetSuite subsidiary's default currency
+
+## Step 1: Install the Expensify Bundle in NetSuite
+1. While logged into NetSuite as an administrator, go to **Customization > SuiteBundler > Search & Install Bundles**, then search for “Expensify”
+2. Click on the Expensify Connect bundle (Bundle ID 283395)
+3. Click **Install**
+4. If you already have the Expensify Connect bundle installed, head to **Customization > SuiteBundler > Search & Install Bundles > List**, and update it to the latest version
+5. Select "Show on Existing Custom Forms" for all available fields
+
+## Step 2: Enable Token-Based Authentication
+1. In NetSuite, go to **Setup > Company > Enable Features > SuiteCloud > Manage Authentication**
+2. Make sure “Token Based Authentication” is enabled
+3. Click **Save**
+
+
+## Step 3: Add Expensify Integration Role to a User
+1. In NetSuite, head to **Lists > Employees**, and find the user who you would like to add the Expensify Integration role to. The user you select must at least have access to the permissions included in the Expensify Integration Role, and Admin access works too, but Admin access is not required.
+2. Click **Edit > Access**, then find the Expensify Integration role in the dropdown and add it to the user
+3. Click **Save**
+
+Remember that Tokens are linked to a User and a Role, not solely to a User. It’s important to note that you cannot establish a connection with tokens using one role and then switch to another role afterward. Once you’ve initiated a connection with tokens, you must continue using the same token/user/role combination for all subsequent sync or export actions.
+
+## Step 4: Create Access Tokens
+1. In NetSuite, enter “page: tokens” in the Global Search
+2. Click **New Access Token**
+3. Select Expensify as the application (this must be the original Expensify integration from the bundle)
+4. Select the role Expensify Integration
+5. Click **Save**
+6. Copy and paste the token and token ID to a saved location on your computer (this is the only time you will see these details)
+
+## Step 5: Confirm Expense Reports are enabled in NetSuite
+Expense Reports must be enabled in order to use Expensify’s integration with NetSuite.
+
+1. In NetSuite, go to **Setup > Company > Enable Features > Employees**
+2. Confirm the checkbox next to "Expense Reports" is checked
+3. If not, click the checkbox and then click **Save** to enable Expense Reports
+
+## Step 6: Confirm Expense Categories are set up in NetSuite
+Once Expense Reports are enabled, Expense Categories can be set up in NetSuite. Expense Categories are synced to Expensify as Categories. Each Expense Category is an alias mapped to a General Ledger account so that employees can more easily categorize expenses.
+
+1. In NetSuite, go to **Setup > Accounting > Expense Categories** (a list of Expense Categories should show)
+2. If no Expense Categories are visible, click **New** to create new ones
+
+## Step 7: Confirm Journal Entry Transaction Forms are Configured Properly
+1. In NetSuite, go to **Customization > Forms > Transaction Forms**
+2. Click **Customize** or **Edit** next to the Standard Journal Entry form
+3. Click **Screen Fields > Main**. Please verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal"
+4. Click the sub-header **Lines** and verify that the “Show” column for “Receipt URL” is checked
+5. Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the journal type have this same configuration
+
+## Step 8: Confirm Expense Report Transaction Forms are Configured Properly
+1. In NetSuite, go to **Customization > Forms > Transaction Forms**
+2. Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main**
+3. Verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal"
+4. Click the second sub-header, **Expenses**, and verify that the "Show" column for "Receipt URL" is checked
+5. Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the expense report type have this same configuration
+
+## Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly
+1. In NetSuite, go to **Customization > Forms > Transaction Forms**
+2. Click **Customize** or **Edit** next to your preferred Vendor Bill form
+3. Click **Screen Fields > Main** and verify that the “Created From” label has “Show” checked and that Departments, Classes, and Locations have the “Show” label unchecked
+4. Under the **Expenses** sub-header (make sure to click the “Expenses” sub-header at the very bottom and not “Expenses & Items”), ensure “Show” is checked for Receipt URL, Department, Location, and Class
+5. Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the vendor bill type have this same configuration
+
+## Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly
+1. In NetSuite, go to **Customization > Forms > Transaction Forms**
+2. Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click **Screen Fields > Main** and verify that the “Created From” label has “Show” checked and that Departments, Classes, and Locations have the “Show” label unchecked
+3. Under the **Expenses** sub-header (make sure to click the “Expenses” sub-header at the very bottom and not “Expenses & Items”), ensure “Show” is checked for Receipt URL, Department, Location, and Class
+4. Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the vendor credit type have this same configuration
+
+## Step 11: Set up Tax Groups (only applicable if tracking taxes)
+Expensify imports NetSuite Tax Groups (not Tax Codes), which you can find in NetSuite under **Setup > Accounting > Tax Groups**.
+
+Tax Groups are an alias for Tax Codes in NetSuite and can contain one or more Tax Codes (Please note: for UK and Ireland subsidiaries, please ensure your Tax Groups do not have more than one Tax Code). We recommend naming Tax Groups so your employees can easily understand them, as the name and rate will be displayed in Expensify.
+
+To set up Tax Groups in NetSuite:
+
+1. Go to **Setup > Accounting > Tax Groups**
+2. Click **New**
+3. Select the country for your Tax Group
+4. Enter the Tax Name (this is what employees will see in Expensify)
+5. Select the subsidiary for this Tax Group
+6. Select the Tax Code from the table you wish to include in this Tax Group
+7. Click **Add**
+8. Click **Save**
+9. Create one NetSuite Tax Group for each tax rate you want to show in Expensify
+
+Ensure Tax Groups can be applied to expenses by going to **Setup > Accounting > Set Up Taxes** and setting the Tax Code Lists Include preference to “Tax Groups And Tax Codes” or “Tax Groups Only.” If this field does not display, it’s not needed for that specific country.
+
+## Step 12: Connect Expensify to NetSuite
+1. Log into Expensify as a workspace admin
+2. Click your profile image or icon in the bottom left menu
+3. Scroll down and click **Workspaces** in the left menu
+4. Select the workspace you want to connect to NetSuite
+5. Click **More features** in the left menu
+6. Scroll down to the Integrate section and enable Accounting
+7. Click **Accounting** in the left menu
+8. Click **Set up** next to NetSuite
+9. Click **Next** until you reach setup step 5 (If you followed the instructions above, then the first four setup steps will be complete)
+10. On setup step 5, enter your NetSuite Account ID, Token ID, and Token Secret (the NetSuite Account ID can be found in NetSuite by going to **Setup > Integration > Web Services Preferences**)
+11. Click **Confirm** to complete the setup
+
+After completing the setup, the NetSuite connection will sync. It can take 1-2 minutes to sync with NetSuite.
+
+Once connected, all reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration).
+
+## FAQ
+### What type of Expensify plan is required to connect to NetSuite?
+You need a Control workspace to integrate with NetSuite. If you have a Collect workspace, you will need to upgrade to Control.
+
+### Page size
+Make sure your page size is set to 1000 in NetSuite for importing your customers and vendors. Go to **Setup > Integration > Web Services Preferences** and search **Page Size** to determine your page size.
+
+
+# Configure NetSuite integration
+## Step 1: Configure import settings
+
+The following section will help you determine how data will be imported from NetSuite into Expensify. To change your import settings, navigate to the Accounting settings for your workspace, then click **Import** under the NetSuite connection.
+
+### Expense Categories
+Your NetSuite Expense Categories are automatically imported into Expensify as categories. This cannot be amended, and any new categories you'd like to add must be added as Expense Categories in NetSuite.
+
+Once imported, you can turn specific Categories on or off under **Settings > Workspaces > [Workspace Name] > Categories**.
+
+### Departments, Classes, and Locations
+The NetSuite integration allows you to import departments, classes, and locations from NetSuite into Expensify as Tags, Report Fields, or using the NetSuite Employee Default.
+
+- **NetSuite Employee Default:** If default Department, Class, and Locations have been configured on NetSuite employee records, then you can choose to have the NetSuite employee default applied upon export from Expensify to NetSuite. With this selection, employees will not make a selection in Expensify.
+- **Tags:** Employees can select the department, class, or location on each individual expense. If the employee's NetSuite employee record has a default value, then each expense will be defaulted to that tag upon creation, with the option for the employee to select a different value on each expense.
+- **Report Fields:** Employees can select one department/class/location for each expense report.
+
+
+New departments, classes, and locations must be added in NetSuite. Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**.
+
+### Customers and Projects
+The NetSuite integration allows you to import customers and projects into Expensify as Tags or Report Fields.
+
+- **Tags:** Employees can select the customer or project on each individual expense.
+- **Report Fields:** Employees can select one department/class/location for each expense report.
+
+New customers and projects must be added in NetSuite. Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**.
+
+When importing customers or projects, you can also choose to enable **Cross-subsidiary customers/projects**. This setting allows you to import Customers and Projects across all NetSuite subsidiaries to a single Expensify workspace. This setting requires you to enable “Intercompany Time and Expense” in NetSuite. To enable that feature in NetSuite, go to **Setup > Company > Setup Tasks: Enable Features > Advanced Features**.
+
+### Tax
+The NetSuite integration allows users to apply a tax rate and amount to each expense for non-US NetSuite subsidiaries. To do this, import Tax Groups from NetSuite:
+
+1. In NetSuite, head to **Setup > Accounting > Tax Groups**
+2. Once imported, go to the NetSuite connection configuration page in Expensify (under **Settings > Workspaces > [Workspace Name] > Accounting > NetSuite > Import**)
+3. Enable Tax
+4. Go back to the Accounting screen, click the three dots next to NetSuite, and click **Sync now**
+5. All Tax Groups for the connected NetSuite subsidiary will be imported to Expensify as taxes.
+6. After syncing, go to **Settings > Workspace > [Workspace Name] > Tax** to see the tax groups imported from NetSuite
+
+### Custom Segments
+You can import one or more Custom Segments from NetSuite for selection in Expensify. To add a Custom Segment to your Expensify workspace:
+
+1. Go to **Settings > Workspaces > [Workspace Name] > Accounting**
+2. Click **Import** under NetSuite
+3. Click **Custom segments/records**
+4. Click **Add custom segment/record**
+
+From there, you'll walk through a simple setup wizard. You can find detailed instructions below for each setup step.
+
+1. In Step 1, you'll select whether you'd like to import a custom segment or a custom record. For a Custom Segment, continue. We have separate instructions for [Custom Records](link) and [Custom Lists](link).
+2. **Segment Name**
+ a. Log into NetSuite as an administrator
+ b. Go to **Customization > Lists, Records, & Fields > Custom Segments**
+ c. You’ll see the Segment Name on the Custom Segments page
+3. Internal ID
+ a. Ensure you have internal IDs enabled in NetSuite under **Home > Set Preferences**
+ b. Navigate back to the **Custom Segments** page
+ c. Click the **Custom Record Type** link
+ d. You’ll see the Internal ID on the Custom Record Type page
+4. **Script ID/Field ID**
+ a. If configuring Custom Segments as Report Fields, use the Field ID on the Transactions tab (under **Custom Segments > Transactions**). If no Field ID is shown, use the unified ID (just called “ID” right below the “Label”).
+ b. If configuring Custom Segments as Tags, use the Field ID on the Transaction Columns tab (under **Custom Segments > Transaction Columns**). If no Field ID is shown, use the unified ID (just called “ID” right below the “Label”).
+ c. Note that as of 2019.1, any new custom segments that you create automatically use the unified ID, and the "Use as Field ID" box is not visible. If you are editing a custom segment definition that was created before 2019.1, the "Use as Field ID" box is available. To use a unified ID for the entire custom segment definition, check the "Use as Field ID" box. When the box is checked, no field ID fields or columns are shown on the Application & Sourcing subtabs because one ID is used for all fields.
+5. Select whether you'd like to import the custom segment as Tags or Report Fields
+6. Finally, confirm that all the details look correct
+
+**Note:** Don’t use the “Filtered by” feature available for Custom Segments. Expensify can’t make these dependent on other fields. If you do have a filter selected, we suggest switching that filter in NetSuite to “Subsidiary” and enabling all subsidiaries to ensure you don’t receive any errors upon exporting reports.
+
+### Custom Records
+You can import one or more Custom Records from NetSuite for selection in Expensify. To add a Custom Record to your Expensify workspace:
+
+1. Go to **Settings > Workspaces > [Workspace Name] > Accounting**
+2. Click **Import** under NetSuite
+3. Click **Custom segments/records**
+4. Click **Add custom segment/record**
+
+From there, you'll walk through a simple setup wizard. You can find detailed instructions below for each setup step.
+
+1. In Step 1, you'll select whether you'd like to import a custom segment or a custom record. For a Custom Record, continue. We have separate instructions for [Custom Segments](link) and [Custom Lists](link).
+2. **Segment Name**
+ a. Log into NetSuite as an administrator
+ b. Go to **Customization > Lists, Records, & Fields > Custom Segments**
+ c. You’ll see the Custom Record Name on the Custom Segments page
+3. **Internal ID**
+ a. Make sure you have Internal IDs enabled in NetSuite under **Home > Set Preferences**
+ b. Navigate back to the **Custom Segment** page
+ c. Click the **Custom Record Type** hyperlink
+ d. You’ll see the Internal ID on the Custom Record Type page
+4. **Transaction Column ID**
+ a. If configuring Custom Records as Report Fields, use the Field ID on the Transactions tab (under **Custom Segments > Transactions**).
+ b. If configuring Custom Records as Tags, use the Field ID on the Transaction Columns tab (under **Custom Segments > Transaction Columns**).
+5. Select whether you'd like to import the custom record as Tags or Report Fields
+6. Finally, confirm that all the details look correct
+
+### Custom Lists
+You can import one or more Custom Lists from NetSuite for selection in Expensify. To add a Custom List to your Expensify workspace:
+
+1. Go to **Settings > Workspaces > [Workspace Name] > Accounting**
+2. Click **Import** under NetSuite
+3. Click **Custom list**
+4. Click **Add custom list**
+
+From there, you'll walk through a simple setup wizard. You can find detailed instructions below for each setup step.
+
+1. In Step 1, you'll select which Custom List you'd like to import from a pre-populated list
+2. **Transaction Line Field ID**
+ a. Log into NetSuite as an admin
+ b. Search **“Transaction Line Fields”** in the global search
+ c. Click into the desired Custom List
+ d. You'll find the transaction Line Field ID along the left-hand side of the page
+3. Select whether you'd like to import the custom list as Tags or Report Fields
+4. Finally, confirm that all the details look correct
+
+From there, you should see the values for the Custom Lists under the Tag or Report Field settings in Expensify.
+## Step 2: Configure export settings
+There are numerous options for exporting data from Expensify to NetSuite. To access these settings, head to **Settings > Workspaces > [Workspace name] > Accounting** and click **Export** under NetSuite.
+
+### Preferred Exporter
+Any workspace admin can export reports to NetSuite. For auto-export, Concierge will export on behalf of the preferred exporter. The preferred exporter will also be notified of any expense reports that fail to export to NetSuite due to an error.
+
+### Date
+You can choose which date to use for the records created in NetSuite. There are three date options:
+
+1. **Date of last expense:** This will use the date of the previous expense on the report
+2. **Submitted date:** The date the employee submitted the report
+3. **Exported date:** The date you export the report to NetSuite
+
+### Export out-of-pocket expenses as
+**Expense Reports**
+Out-of-pocket expenses will be exported to NetSuite as expense reports, which will be posted to the payables account designated in NetSuite.
+
+**Vendor Bills**
+Out-of-pocket expenses will be exported to NetSuite as vendor bills. Each report will be posted as payable to the vendor associated with the employee who submitted the report. You can also set an approval level in NetSuite for vendor bills.
+
+**Journal Entries**
+Out-of-pocket expenses will be exported to NetSuite as journal entries. All the transactions will be posted to the payable account specified in the workspace. You can also set an approval level in NetSuite for the journal entries.
+
+Note: By default, journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option. Also, The credit line and header level classifications are pulled from the employee record.
+
+### Export company card expenses as
+**Expense Reports**
+To export company card expenses as expense reports, you will need to configure your default corporate cards in NetSuite. To do this, you must select the correct card on the NetSuite employee records (for individual accounts) or the subsidiary record (If you use a non-One World account, the default is found in your accounting preferences).
+
+To update your expense report transaction form in NetSuite:
+
+1. Go to **Customization > Forms > Transaction Forms**
+2. Click **Edit** next to the preferred expense report form
+3. Go to the **Screen Fields > Main** tab
+4. Check “Show” for "Account for Corporate Card Expenses"
+5. Go to the **Screen Fields > Expenses** tab
+6. Check “Show” for "Corporate Card"
+
+You can also select the default account on your employee record to use individual corporate cards for each employee. Make sure you add this field to your employee entity form in NetSuite. If you have multiple cards assigned to a single employee, you cannot export to each account. You can only have a single default per employee record.
+
+**Vendor Bills**
+Company card expenses will be posted as a vendor bill payable to the default vendor specified in your workspace Accounting settings. You can also set an approval level in NetSuite for the bills.
+
+
+**Journal Entries**
+Company Card expenses will be posted to the Journal Entries posting account selected in your workspace Accounting settings.
+
+Important Notes:
+
+- Expensify Card expenses will always export as Journal Entries, even if you have Expense Reports or Vendor Bills configured for non-reimbursable expenses on the Export tab
+- Journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option
+- The credit line and header level classifications are pulled from the employee record
+
+### Export invoices to
+Select the Accounts Receivable account where you want your Invoice reports to export. In NetSuite, the invoices are linked to the customer, corresponding to the email address where the invoice was sent.
+
+### Export foreign currency amount
+Enabling this feature allows you to send the original amount of the expense rather than the converted total when exporting to NetSuite. This option is only available when exporting out-of-pocket expenses as Expense Reports.
+
+### Export to next open period
+When this feature is enabled and you try exporting an expense report to a closed NetSuite period, we will automatically export to the next open period instead of returning an error.
+
+
+## Step 3: Configure advanced settings
+To access the advanced settings of the NetSuite integration, head to **Settings > Workspaces > [Workspace name] > Accounting** and click **Advanced** under NetSuite.
+
+
+Let’s review the different advanced settings and how they interact with the integration.
+
+### Auto-sync
+We strongly recommend enabling auto-sync to ensure that the information in NetSuite and Expensify is always in sync. The following will occur when auto-sync is enabled:
+
+**Daily sync from NetSuite to Expensify:** Once a day, Expensify will sync any changes from NetSuite into Expensify. This includes any new, updated, or removed departments/classes/locations/projects/etc.
+
+**Auto-export:** When an expense report reaches its final state in Expensify, it will be automatically exported to NetSuite. The final state will either be reimbursement (if you reimburse members through Expensify) or final approval (if you reimburse members outside of Expensify).
+
+**Reimbursement-sync:** If Sync Reimbursed Reports (more details below) is enabled, then we will sync the reimbursement status of reports between Expensify and NetSuite.
+
+### Sync reimbursed reports
+When Sync reimbursed reports is enabled, the reimbursement status will be synced between Expensify and NetSuite.
+
+**If you reimburse members through Expensify:** Reimbursing an expense report will trigger auto-export to NetSuite. When the expense report is exported to NetSuite, a corresponding bill payment will also be created in NetSuite.
+
+**If you reimburse members outside of Expensify:** Expense reports will be exported to NetSuite at time of final approval. After you mark the report as paid in NetSuite, the reimbursed status will be synced back to Expensify the next time the integration syncs.
+
+To ensure this feature works properly for expense reports, make sure that the reimbursement account you choose within the settings matches the default account for Bill Payments in NetSuite. When exporting invoices, once marked as Paid, the payment is marked against the account selected after enabling the Collection Account setting.
+
+### Invite employees and set approvals
+Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will send them an email letting them know they’ve been added to a workspace.
+
+In addition to inviting employees, this feature enables a custom set of approval workflow options, which you can manage in Expensify Classic:
+
+- **Basic Approval:** A single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page.
+- **Manager Approval (default):** Two levels of approval route reports first to an employee’s NetSuite expense approver or supervisor, and second to a workspace-wide Final Approver. By NetSuite convention, Expensify will map to the supervisor if no expense approver exists. The Final Approver defaults to the workspace owner but can be edited on the people page.
+- **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured on the workspace’s People settings page. If you enable this setting, it’s recommended you review the newly imported employees and managers on the **Settings > Workspaces > Group > [Workspace Name] > People** page.
+
+### Auto-create employees/vendors
+With this feature enabled, Expensify will automatically create a new employee or vendor in NetSuite (if one doesn’t already exist) using the name and email of the report submitter.
+
+### Enable newly imported categories
+With this feature enabled, anytime a new Expense Category is created in NetSuite, it will be imported into Expensify as an enabled category. If the feature is disabled, then new Expense Categories will be imported into Expensify as disabled.
+
+### Setting approval levels
+You can set the NetSuite approval level for each different export type:
+
+- **Expense report approval level:** Choose from "NetSuite default preference," “Only supervisor approved,” “Only accounting approved,” or “Supervisor and accounting approved.”
+- **Vendor bill approval level and Journal entry approval level:** Choose from "NetSuite default preference," “Pending approval,” or “Approved for posting.”
+
+If you have Approval Routing selected in your accounting preference, this will override the selections in Expensify. If you do not wish to use Approval Routing in NetSuite, go to **Setup > Accounting > Accounting Preferences > Approval Routing** and ensure Vendor Bills and Journal Entries are not selected.
+
+### Custom form ID
+By default, Expensify will create entries using the preferred transaction form set in NetSuite. Alternatively, you have the option to designate a specific transaction form to be used.
+
+
+
+## FAQ
+
+### How does Auto-sync work with reimbursed reports?
+If a report is reimbursed via ACH or marked as reimbursed in Expensify and then exported to NetSuite, the report is automatically marked as paid in NetSuite.
+
+If a report is exported to NetSuite, then marked as paid in NetSuite, the report will automatically be marked as reimbursed in Expensify during the next sync.
+
+### Will enabling auto-sync affect existing approved and reimbursed reports?
+Auto-sync will only export newly approved reports to NetSuite. Any reports that were approved or reimbursed before enabling auto-sync will need to be manually exported in order to sync them to NetSuite.
+
+
+### When using multi-currency features in NetSuite, can expenses be exported with any currency?
+When using multi-currency features with NetSuite, remember these points:
+
+**Employee/Vendor currency:** The currency set for a NetSuite vendor or employee record must match the subsidiary currency for whichever subsidiary you export that user's reports to. A currency mismatch will cause export errors.
+**Bank Account Currency:** When synchronizing bill payments, your bank account’s currency must match the subsidiary’s currency. Failure to do so will result in an “Invalid Account” error.
diff --git a/docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md b/docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md
new file mode 100644
index 000000000000..1f5d9662bb4f
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md
@@ -0,0 +1,317 @@
+---
+title: Set up Sage Intacct connection
+description: Integrate Sage Intacct with Expensify
+---
+
+
+# Connect to Sage Intacct
+
+## Overview
+Expensify’s integration with Sage Intacct allows you to connect using either role-based permissions or user-based permissions and exporting either expense reports or vendor bills.
+
+Checklist of items to complete:
+
+1. Create a web services user and configure permissions
+1. Enable the T&E module (only required if exporting out-of-pocket expenses as Expense Reports)
+1. Set up Employees in Sage Intacct (only required if exporting expenses as Expense Reports)
+1. Set up Expense Types (only required if exporting expenses as Expense Reports)
+1. Enable Customization Services
+1. Download the Expensify Package
+1. Upload the Expensify Package in Sage Intacct
+1. Add web services authorization
+1. Enter credentials and connect Expensify and Sage Intacct
+1. Configure integration sync options
+
+## Step 1a: Create a web services user (Connecting with User-based permissions)
+Note: If the steps in this section look different in your Sage Intacct instance, you likely use role-based permissions. If that’s the case, follow the steps [here].
+
+To connect to Sage Intacct, you’ll need to create a special web services user (please note that Sage Intacct does not charge extra for web services users).
+
+1. Go to **Company > Web Services Users > New**.
+2. Configure the user as outlined below:
+ - **User ID**: “xmlgateway_expensify”
+ - **Last Name and First Name:** “Expensify”
+ - **Email Address:** Your shared accounting team email
+ - **User Type:** “Business”
+ - **Admin Privileges:** “Full”
+ - **Status:** “Active”
+
+Next, configure correct permissions for the new web services user.
+
+1. Go to the subscription link for this user in the user list
+1. Click on the checkbox next to the Application/Module
+1. Click **Permissions**
+
+These are the permissions required for this integration when exporting out-of-pocket expenses as vendor bills:
+
+- **Administration (All)**
+- **Company (Read-only)**
+- **Cash Management (All)**
+- **General Ledger (All)**
+- **Time & Expense (All)** - Only required if exporting out-of-pocket expenses as expense reports
+- **Projects (Read-only)** - Only required if using Projects or Customers
+- **Accounts Payable (All)** - Only required if exporting any expenses expenses as vendor bills
+
+## Step 1b: Create a web services user (Connecting with Role-based permissions)
+Note: If the steps in this section look different in your Sage Intacct instance, you likely use role-based permissions. If that’s the case, follow the steps [here].
+
+**First, you'll need to create the new role:**
+
+1. In Sage Intacct, click **Company**, then click on the **+ button** next to Roles
+1. Name the role "Expensify", then click **Save**
+1. Go to **Roles > Subscriptions** and find the “Expensify” role you just created
+1. Configure correct permissions for this role by clicking the checkbox and then clicking on the Permissions hyperlink. These are the permissions required for this integration when exporting out-of-pocket expenses as vendor bills:
+ - **Administration (All)**
+ - **Company (Read-only)**
+ - **Cash Management (All)**
+ - **General Ledger (All)**
+ - **Time & Expense (All)** - Only required if exporting out-of-pocket expenses as expense reports
+ - **Projects (Read-only)** - Only required if using Projects or Customers
+ - **Accounts Payable (All)** - Only required if exporting any expenses expenses as vendor bills
+
+**Next, you’ll create a web services user and assign the role to that user:**
+
+1. Go to **Company > Web Services Users > New**
+2. Set up the user as described below:
+ - **User ID:** “xmlgateway_expensify”
+ - **Last name and First name:** “Expensify”
+ - **Email address:** your shared accounting team email
+ - **User type:** “Business”
+ - **Admin privileges:** “Full”
+ - **Status:** “Active”
+3. Assign the role to that user: click the **+ button**, then select the “Expensify” role and click **Save**
+
+
+## Step 2: Enable and configure the Time & Expenses Module
+**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.**
+
+Enabling the T&E module is a paid subscription through Sage Intacct and the T&E module is often included in your Sage Intacct instance. For information on the costs of enabling this module, please contact your Sage Intacct account manager.
+
+**To enable the Time & Expenses module:**
+
+In Sage Intacct, go to **Company menu > Subscriptions > Time & Expenses** and toggle the switch to subscribe.
+
+**After enabling T&E, configure it as follows:**
+
+1. Ensure that **Expense types** is checked
+2. Under **Auto-numbering sequences** set the following:
+ - **Expense Report:** EXP
+ - **Employee:** EMP
+ - **Duplicate Numbers:** Select “Do not allow creation”
+ - To create the EXP sequence, click on the down arrow on the expense report line and select **Add:
+ 1. **Sequence ID:** EXP
+ 1. **Print Title:** EXPENSE REPORT
+ 1. **Starting Number:** 1
+ 1. **Next Number:** 2
+3. Select **Advanced Settings** and configure the following:
+ a. **Fixed Number Length:** 4
+ b. **Fixed Prefix:** EXP
+4. Click **Save**
+5. Under Expense Report approval settings, ensure that **Enable expense report approval** is unchecked
+6. Click **Save** to confirm your configuration
+
+
+## Step 3: Set up Employees in Sage Intacct
+**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.**
+
+To set up employees in Sage Intacct:
+
+1. Navigate to Time & Expenses and click the **plus button** next to Employees.
+ - If you don’t see the Time & Expense option in the top ribbon, you may need to adjust your settings. Go to **Company > Roles > Time & Expenses** and enable all permissions.
+2. To create an employee, you’ll need to provide the following information:
+ - Employee ID
+ - Primary contact name
+ - Email address
+ 1. In the "Primary contact name" field, click the dropdown arrow.
+ 1. Select the employee if they’ve already been created.
+ 1. Otherwise, click **+ Add** to create a new employee.
+ 1. Fill in their Primary Email Address along with any other required information
+
+
+## Step 4: Set up Expense Types in Sage Intacct
+**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.**
+
+Expense Types provide a user-friendly way to display the names of your expense accounts to your employees. To set up expense types in Sage Intacct:
+
+1. **Setup Your Chart of Accounts**
+ - Before configuring Expense Types, ensure your Chart of Accounts is set up. You can set up accounts in bulk by going to **Company > Open Setup > Company Setup Checklist** and clicking **Import**.
+2. **Set up Expense Types**
+ - Go to Time & Expense
+ - Open Setup and click the **plus button** next to Expense Types
+3. For each Expense Type, provide the following information:
+ - **Expense Type**
+ - **Description**
+ - **Account Number** (from your General Ledger)
+
+## Step 5: Enable Customization Services
+**Note:** If you already have Platform Services enabled, you can skip this step.
+
+To enable Customization Services, go to **Company > Subscriptions > Customization Services**.
+
+
+## Step 6: Download the Expensify Package
+1. In Expensify, go to Settings > Workspaces
+1. Click into the workspace where you'd like to connect to Sage Intacct
+ - If you already use Expensify, you can optionally create a test workspace by clicking **New Workspace** at the top-right of the Workspaces page. A test workspace allows you to have a sandbox environment for testing before implementing the integration live.
+1. Go to **Connections > Sage Intacct > Connect to Sage Intacct**
+1. Select **Download Package** (You only need to download the file; we’ll upload it from your Downloads folder later)
+
+## Step 7: Upload Package in Sage Intacct
+If you use Customization Services:
+
+1. Go to **Customization Services > Custom Packages > New Package**
+1. Click on **Choose File** and select the Package file from your downloads folder
+1. Click **Import**
+
+If you use Platform Services:
+
+1. Go to **Platform Services > Custom Packages > New Package**
+1. Click on **Choose File** and select the Package file from your downloads folder
+1. Click **Import**
+
+
+## Step 8: Add Web Services Authorization
+1. Go to **Company > Company Info > Security** in Sage Intacct and click **Edit**
+2. Scroll down to **Web Services Authorizations** and add “expensify” (all lower case) as a Sender ID
+
+## Step 9: Enter Credentials and Connect Expensify and Sage Intacct
+1. In Expensify, go to **Settings > Workspaces > [Workspace Name] > Accounting**
+1. Click **Set up** next to Sage Intacct
+1. Enter the credentials you set for your web services user in Step 1
+1. Click **Confirm**
+
+
+
+# Configure Sage Intacct integration
+
+## Step 1: Select entity (multi-entity setups only)
+If you have a multi-entity setup in Sage Intacct, you will be able to select in Expensify which Sage Intacct entity to connect each workspace to. Each Expensify workspace can either be connected to a single entity or connected at the Top Level.
+
+To select or change the Sage Intacct entity that your Expensify workspace is connected to, navigate to the Accounting settings for your workspace and click **Entity** under the Sage Intacct connection.
+
+## Step 2: Configure import settings
+The following section will help you determine how data will be imported from Sage Intacct into Expensify. To change your import settings, navigate to the Accounting settings for your workspace, then click **Import** under the Sage Intacct connection.
+
+### Expense Types / Chart of Accounts
+The categories in Expensify depend on how you choose to export out-of-pocket expenses:
+
+- If you choose to export out-of-pocket expenses as Expense Reports, your categories in Expensify will be imported from your Sage Intacct Expense Types
+- If you choose to export out-of-pocket expenses as Vendor Bills, your categories will be imported directly from your Chart of Accounts (also known as GL Codes or Account Codes).
+
+You can disable unnecessary categories in Expensify by going to **Settings > Workspaces > [Workspace Name] > Categories**. Note that every expense must be coded with a Category, or it will fail to export.
+
+### Billable Expenses
+Enabling billable expenses allows you to map your expense types or accounts to items in Sage Intacct. To do this, you’ll need to enable the correct permissions on your Sage Intacct user or role. This may vary based on the modules you use in Sage Intacct, so you should enable read-only permissions for relevant modules such as Projects, Purchasing, Inventory Control, and Order Entry.
+
+Once permissions are set, you can map categories to specific items, which will then export to Sage Intacct. When an expense is marked as Billable in Expensify, users must select the correct billable Category (Item), or there will be an error during export.
+
+
+### Standard dimensions: Departments, Classes, and Locations
+The Sage Intacct integration allows you to import standard dimensions into Expensify as tags, report fields, or using the Sage Intacct employee default.
+
+- **Sage Intacct Employee default:** This option is only available when exporting as expense reports. When this option is selected, nothing will be imported into Expensify - instead, the employee default will be applied to each expense upon export.
+- **Tags:** Employees can select the department, class, or location on each individual expense. If the employee's Sage Intacct employee record has a default value, then each expense will default to that tag, with the option for the employee to select a different value on each expense.
+- **Report Fields:** Employees can select one department/class/location for each expense report.
+
+New departments, classes, and locations must be added in Sage Intacct. Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**.
+
+Please note that when importing departments as tags, expense reports may show the tag name as "Tag" instead of "Department."
+
+### Customers and Projects
+The Sage Intacct integration allows you to import customers and projects into Expensify as Tags or Report Fields.
+
+- **Tags:** Employees can select the customer or project on each individual expense.
+- **Report Fields:** Employees can select one department/class/location for each expense report.
+
+New customers and projects must be added in Sage Intacct. Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**.
+
+
+### Tax
+The Sage Intacct integration supports native VAT and GST tax. To enable this feature, go to **Settings > Workspaces > [Workspace Name] > Accounting**, click **Import** under Sage Intacct, and enable Tax. Enabling this option will import your native tax rates from Sage Intacct into Expensify. From there, you can select default rates for each category under **Settings > Workspaces > [Workspace Name] > Categories**.
+
+For older Sage Intacct connections that don't show the Tax option, simply resync the connection by going to **Settings > Workspaces > [Workspace Name] > Accounting** and clicking the three dots next to Sage Intacct, and the tax toggle will appear.
+
+### User-Defined Dimensions
+You can add User-Defined Dimensions (UDDs) to your workspace by locating the “Integration Name” in Sage Intacct. Please note that you must be logged in as an administrator in Sage Intacct to find the required fields.
+
+To find the Integration Name in Sage Intacct:
+
+1. Go to **Platform Services > Objects > List**
+1. Set “filter by application” to “user-defined dimensions”
+1. In Expensify, go to **Settings > Workspaces > [Workspace Name] > Accounting** and click **Import** under Sage Intacct
+1. Enable User Defined Dimensions
+1. Enter the “Integration name” and choose whether to import it into Expensify as expense-level tags or as report fields
+1. Click **Save**
+
+Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**.
+
+
+## Step 5: Configure export settings
+To access export settings, head to **Settings > Workspaces > [Workspace name] > Accounting** and click **Export** under Sage Intacct.
+
+### Preferred exporter
+Any workspace admin can export reports to Sage Intacct. For auto-export, Concierge will export on behalf of the preferred exporter. The preferred exporter will also be notified of any expense reports that fail to export to Sage Intacct due to an error.
+
+### Export date
+You can choose which date to use for the records created in Sage Intacct. There are three date options:
+
+1. **Date of last expense:** This will use the date of the previous expense on the report
+1. **Export date:** The date you export the report to Sage Intacct
+1. **Submitted date:** The date the employee submitted the report
+
+### Export out-of-pocket expenses as
+Out-of-pocket expenses can be exported to Sage Intacct as **expense reports** or as **vendor bills**. If you choose to export as expense reports, you can optionally select a **default vendor**, which will apply to reimbursable expenses that don't have a matching vendor in Sage Intacct.
+
+### Export company card expenses as
+Company Card expenses are exported separately from out-of-pocket expenses, and can be exported to Sage Intacct as credit card charges** or as **vendor bills**.
+
+- **Credit card charges:** When exporting as credit card charges, you must select a credit card account. You can optionally select a default vendor, which will apply to company card expenses that don't have a matching vendor in Sage Intacct.
+ - Credit card charges cannot be exported to Sage Intacct at the top-level if you have multi-currency enabled, so you will need to select an individual entity if you have this setup.
+- **Vendor bills:** When exporting as vendor bills, you can select a default vendor, which will apply to company card expenses that don't have a matching vendor in Sage Intacct.
+
+If you centrally manage your company cards through Domains in Expensify Classic, you can export expenses from each individual card to a specific account in Sage Intacct in the Expensify Company Card settings.
+
+### 6. Configure advanced settings
+To access the advanced settings of the Sage Intacct integration, head to **Settings > Workspaces > [Workspace name] > Accounting** and click **Advanced** under Sage Intacct.
+
+
+Let’s review the different advanced settings and how they interact with the integration.
+
+### Auto-sync
+We strongly recommend enabling auto-sync to ensure that the information in Sage Intacct and Expensify is always in sync. The following will occur when auto-sync is enabled:
+
+**Daily sync from Sage Intacct to Expensify:** Once a day, Expensify will sync any changes from Sage Intacct into Expensify. This includes any changes or additions to your Sage Intacct dimensions.
+
+**Auto-export:** When an expense report reaches its final state in Expensify, it will be automatically exported to Sage Intacct. The final state will either be reimbursement (if you reimburse members through Expensify) or final approval (if you reimburse members outside of Expensify).
+
+**Reimbursement-sync:** If Sync Reimbursed Reports (more details below) is enabled, then we will sync the reimbursement status of reports between Expensify and Sage Intacct.
+
+### Invite employees
+Enabling this feature will invite all employees from the connected Sage Intacct entity to your Expensify workspace. Once imported, each employee who has not already been invited to that Expensify workspace will receive an email letting them know they’ve been added to the workspace.
+
+In addition to inviting employees, this feature enables a custom set of approval workflow options, which you can manage in Expensify Classic:
+
+- **Basic Approval:** A single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page.
+- **Manager Approval (default):** Each user submits to their manager, who is imported from Sage Intacct. You can optionally select a final approver who each manager forwards to for second-level approval.
+- **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured in Expensify. If you enable this setting, you can configure approvals by going to **Settings > Workspaces > [Workspace Name] > People**.
+
+
+### Sync reimbursed reports
+When Sync reimbursed reports is enabled, the reimbursement status will be synced between Expensify and Sage Intacct.
+
+**If you reimburse employees through Expensify:** Reimbursing an expense report will trigger auto-export to Sage Intacct. When the expense report is exported to Sage Intacct, a corresponding bill payment will also be created in Sage Intacct in the selected Cash and Cash Equivalents account. If you don't see the account you'd like to select in the dropdown list, please confirm that the account type is Cash and Cash Equivalents.
+
+**If you reimburse employees outside of Expensify:** Expense reports will be exported to Sage Intacct at time of final approval. After you mark the report as paid in Sage Intacct, the reimbursed status will be synced back to Expensify the next time the integration syncs.
+
+To ensure this feature works properly for expense reports, make sure that the account you choose within the settings matches the default account for Bill Payments in NetSuite. When exporting invoices, once marked as Paid, the payment is marked against the account selected after enabling the Collection Account setting.
+## FAQ
+
+### Why wasn't my report automatically exported to Sage Intacct?
+There are a number of factors that can cause auto-export to fail. If this happens, you will find the specific export error in the report comments for the report that failed to export. Once you’ve resolved any errors, you can manually export the report to Sage Intacct.
+
+### Will enabling auto-sync affect existing approved and reimbursed reports?
+Auto-sync will only export newly approved reports to Sage Intacct. Any reports that were approved or reimbursed before enabling auto-sync will need to be manually exported in order to sync them to Sage Intacct.
+
+
+### Can I export negative expenses to Sage Intacct?
+Yes, you can export negative expenses to Sage Intacct. If you are exporting out-of-pocket expenses as expense reports, then the total of each exported report cannot be negative.
diff --git a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md
index 155512866a8f..79d5b17055f7 100644
--- a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md
+++ b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md
@@ -52,6 +52,12 @@ Log in to QuickBooks Online and ensure all of your employees are setup as either
![The QuickBooks Online Connect button]({{site.url}}/assets/images/ExpensifyHelp-QBO-3.png){:width="100%"}
+![The QuickBooks Online Connect Accounting button]({{site.url}}/assets/images/ExpensifyHelp-QBO-4.png){:width="100%"}
+
+![The QuickBooks Online Connect Connect button]({{site.url}}/assets/images/ExpensifyHelp-QBO-5.png){:width="100%"}
+
+
+
# Step 3: Configure import settings
The following steps help you determine how data will be imported from QuickBooks Online to Expensify.
diff --git a/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md
rename to docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md
diff --git a/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md
rename to docs/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account.md
diff --git a/docs/articles/new-expensify/expenses/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Create-an-expense.md
rename to docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md
diff --git a/docs/articles/new-expensify/expenses/Distance-Requests.md b/docs/articles/new-expensify/expenses-&-payments/Distance-Requests.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Distance-Requests.md
rename to docs/articles/new-expensify/expenses-&-payments/Distance-Requests.md
diff --git a/docs/articles/new-expensify/expenses-&-payments/Duplicate-detection.md b/docs/articles/new-expensify/expenses-&-payments/Duplicate-detection.md
new file mode 100644
index 000000000000..d7fb0b1bfa71
--- /dev/null
+++ b/docs/articles/new-expensify/expenses-&-payments/Duplicate-detection.md
@@ -0,0 +1,120 @@
+---
+title: Duplicate Detection
+description: Identify and manage duplicate expense requests
+---
+
+
+
+Duplicate Detection helps prevent duplicate expense requests within a member’s account. By identifying and flagging potential duplicates, it ensures better oversight and control over expenses, enhances fraud prevention, and eases the approval process. This feature is available exclusively for paid plans (Collect & Control).
+
+# What is a Duplicate?
+
+A duplicate is an expense request with the same date and amount as another request in an individual member's account. When detected, duplicates are flagged as with a violation and put on “hold”.
+
+# Surfacing Potential Duplicates
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Identify the red dot indicator in the Left-Hand Navigation (LHN) or workspace chat, which signifies a potential duplicate.
+2. Click on the flagged request to open it.
+3. Review the system message indicating the request is on hold due to a potential duplicate.
+4. Click the green **Review duplicates** button in the request header to navigate to the resolve duplicates page.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Identify the red dot indicator in the LHN or workspace chat, which signifies a potential duplicate.
+2. Tap on the flagged request to open it.
+3. Review the system message indicating the request is on hold due to a potential duplicate.
+4. Tap the green **Review duplicates** button in the request header to navigate to the resolve duplicates page.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Resolving Duplicates
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. On the resolve duplicates page, review the chronological list of potential duplicates.
+2. Choose an action:
+ - **Keep all**: Dismiss the duplicates and remove the hold.
+ - **Keep this one**: Merge duplicates, keeping one request and discarding the rest.
+3. If discrepancies exist (e.g., category, tags), choose which details to keep using the one-by-one flow.
+4. Confirm your selection to merge requests or keep all.
+5. The hold is removed, and system messages are updated accordingly.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. On the resolve duplicates page, review the chronological list of potential duplicates.
+2. Choose an action:
+ - **Keep all**: Dismiss the duplicates and remove the hold.
+ - **Keep this one**: Merge duplicates, keeping one request and discarding the rest.
+3. If discrepancies exist (e.g., category, tags), choose which details to keep using the one-by-one flow.
+4. Confirm your selection to merge requests or keep all.
+5. The hold is removed, and system messages are updated accordingly.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Approver Review
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+1. Identify the red dot and "Duplicate" indicators in the expense report.
+2. Click the request with the duplicate indicator.
+3. Click the green **Review duplicates** button to navigate to the review duplicates page.
+4. Choose an action:
+ - **Keep all**: Confirm to keep all requests and resolve duplicates.
+ - **Keep this one**: Navigate through the one-by-one flow to choose the details to keep, followed by confirmation.
+5. Confirm your choices to finalize the action and update system messages accordingly.
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+1. Identify the red dot and "Duplicate" indicators in the expense report.
+2. Tap the request with the duplicate indicator.
+3. Tap the green **Review duplicates** button to navigate to the review duplicates page.
+4. Choose an action:
+ - **Keep all**: Confirm to keep all requests and resolve duplicates.
+ - **Keep this one**: Navigate through the one-by-one flow to choose the details to keep, followed by confirmation.
+5. Confirm your choices to finalize the action and update system messages accordingly.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# Next Steps
+
+- **For members**: Once resolved, the request is automatically unheld, and a system message indicates the resolution.
+- **For approvers**: After confirming the resolution, system messages are updated, and the request status is appropriately adjusted.
+
+{% include faq-begin.md %}
+**Can I review a dismissed duplicate later?**
+
+Yes, approvers can review dismissed duplicates to ensure accuracy and prevent fraud.
+
+**What happens if I choose to keep all duplicates?**
+
+Choosing to keep all duplicates will remove the hold from the requests, and system messages will be updated to reflect this action.
+
+**Can I edit a duplicate request once resolved?**
+
+Yes, you can edit the details of a duplicate request once it has been resolved, but the hold must be removed first.
+
+**What if there are discrepancies in the duplicate requests?**
+
+You will be guided through a one-by-one flow to choose which details to keep from each request.
+
+**If two expenses are SmartScanned on the same day for the same amount, will they be flagged as duplicates?**
+
+Yes, they will be flagged as duplicates unless:
+- The expenses were split from a single expense,
+- The expenses were imported from a credit card, or
+- Matching email receipts sent to receipts@expensify.com were received with different timestamps.
+
+**What happens if Concierge flags a receipt as a duplicate?**
+
+If Concierge lets you know it has flagged a receipt as a duplicate, scanning the receipt again will trigger the same duplicate flagging.
+{% include faq-end.md %}
+
+
diff --git a/docs/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Resolve-Errors-Adding-a-Bank-Account.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account.md
rename to docs/articles/new-expensify/expenses-&-payments/Resolve-Errors-Adding-a-Bank-Account.md
diff --git a/docs/articles/new-expensify/expenses/Send-an-invoice.md b/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md
similarity index 95%
rename from docs/articles/new-expensify/expenses/Send-an-invoice.md
rename to docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md
index 588f0da20154..6546f57073ee 100644
--- a/docs/articles/new-expensify/expenses/Send-an-invoice.md
+++ b/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md
@@ -38,6 +38,9 @@ Once an invoice is sent, the customer receives an automated email or text messag
4. Click **Pay Elsewhere**, which will mark the invoice as Paid.
Currently, invoices must be paid outside of Expensify. However, the ability to make payments through Expensify is coming soon.
+
+![A photo of the pay button]({{site.url}}/assets/images/ExpensifyHelp-Invoice-1.png){:width="100%"}
+
# FAQs
diff --git a/docs/articles/new-expensify/expenses/Set-up-your-wallet.md b/docs/articles/new-expensify/expenses-&-payments/Set-up-your-wallet.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Set-up-your-wallet.md
rename to docs/articles/new-expensify/expenses-&-payments/Set-up-your-wallet.md
diff --git a/docs/articles/new-expensify/expenses/Split-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Split-an-expense.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Split-an-expense.md
rename to docs/articles/new-expensify/expenses-&-payments/Split-an-expense.md
diff --git a/docs/articles/new-expensify/expenses/Track-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Track-expenses.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Track-expenses.md
rename to docs/articles/new-expensify/expenses-&-payments/Track-expenses.md
diff --git a/docs/articles/new-expensify/expenses/Unlock-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Unlock-a-Business-Bank-Account.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Unlock-a-Business-Bank-Account.md
rename to docs/articles/new-expensify/expenses-&-payments/Unlock-a-Business-Bank-Account.md
diff --git a/docs/articles/new-expensify/expenses/Validate-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Validate-a-Business-Bank-Account.md
similarity index 100%
rename from docs/articles/new-expensify/expenses/Validate-a-Business-Bank-Account.md
rename to docs/articles/new-expensify/expenses-&-payments/Validate-a-Business-Bank-Account.md
diff --git a/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md b/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md
new file mode 100644
index 000000000000..ceb40254c607
--- /dev/null
+++ b/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md
@@ -0,0 +1,8 @@
+---
+title: Expensify Travel demo video
+description: Check out a demo of Expensify Travel
+---
+
+Check out a video of how Expensify Travel works below:
+
+
diff --git a/docs/assets/images/ExpensifyHelp-Invoice-1.png b/docs/assets/images/ExpensifyHelp-Invoice-1.png
index e4a042afef82..a6dda9fdca92 100644
Binary files a/docs/assets/images/ExpensifyHelp-Invoice-1.png and b/docs/assets/images/ExpensifyHelp-Invoice-1.png differ
diff --git a/docs/assets/images/ExpensifyHelp-QBO-1.png b/docs/assets/images/ExpensifyHelp-QBO-1.png
index 2aa80e954f1b..e20a5e4222d0 100644
Binary files a/docs/assets/images/ExpensifyHelp-QBO-1.png and b/docs/assets/images/ExpensifyHelp-QBO-1.png differ
diff --git a/docs/assets/images/ExpensifyHelp-QBO-2.png b/docs/assets/images/ExpensifyHelp-QBO-2.png
index 23419b86b6aa..66b71b8d8ec8 100644
Binary files a/docs/assets/images/ExpensifyHelp-QBO-2.png and b/docs/assets/images/ExpensifyHelp-QBO-2.png differ
diff --git a/docs/assets/images/ExpensifyHelp-QBO-3.png b/docs/assets/images/ExpensifyHelp-QBO-3.png
index c612cb760d58..f96550868bbd 100644
Binary files a/docs/assets/images/ExpensifyHelp-QBO-3.png and b/docs/assets/images/ExpensifyHelp-QBO-3.png differ
diff --git a/docs/assets/images/ExpensifyHelp-QBO-4.png b/docs/assets/images/ExpensifyHelp-QBO-4.png
index 7fbc99503f2e..c7b85a93b04b 100644
Binary files a/docs/assets/images/ExpensifyHelp-QBO-4.png and b/docs/assets/images/ExpensifyHelp-QBO-4.png differ
diff --git a/docs/assets/images/ExpensifyHelp-QBO-5.png b/docs/assets/images/ExpensifyHelp-QBO-5.png
index 600a5903c05f..99b83b8be2d1 100644
Binary files a/docs/assets/images/ExpensifyHelp-QBO-5.png and b/docs/assets/images/ExpensifyHelp-QBO-5.png differ
diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_01.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_01.png
new file mode 100644
index 000000000000..8d8222bc76b5
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_01.png differ
diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_02.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_02.png
new file mode 100644
index 000000000000..7fb0afc5d8ff
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_02.png differ
diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_03.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_03.png
new file mode 100644
index 000000000000..68a1f3e65be6
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_03.png differ
diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_04.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_04.png
new file mode 100644
index 000000000000..c62146e2ed43
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_04.png differ
diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_05.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_05.png
new file mode 100644
index 000000000000..d3cfafa4e6a7
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_05.png differ
diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_06.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_06.png
new file mode 100644
index 000000000000..347551a85c59
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_06.png differ
diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_07.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_07.png
new file mode 100644
index 000000000000..83071f380e6e
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_07.png differ
diff --git a/docs/expensify-classic/hubs/connect-credit-cards/business-bank-accounts.html b/docs/expensify-classic/hubs/bank-accounts-and-payments/payments.html
similarity index 100%
rename from docs/expensify-classic/hubs/connect-credit-cards/business-bank-accounts.html
rename to docs/expensify-classic/hubs/bank-accounts-and-payments/payments.html
diff --git a/docs/expensify-classic/hubs/connect-credit-cards/deposit-accounts.html b/docs/expensify-classic/hubs/connections/accelo.html
similarity index 100%
rename from docs/expensify-classic/hubs/connect-credit-cards/deposit-accounts.html
rename to docs/expensify-classic/hubs/connections/accelo.html
diff --git a/docs/expensify-classic/hubs/expenses/expenses.html b/docs/expensify-classic/hubs/connections/certinia.html
similarity index 100%
rename from docs/expensify-classic/hubs/expenses/expenses.html
rename to docs/expensify-classic/hubs/connections/certinia.html
diff --git a/docs/expensify-classic/hubs/integrations/index.html b/docs/expensify-classic/hubs/connections/index.html
similarity index 100%
rename from docs/expensify-classic/hubs/integrations/index.html
rename to docs/expensify-classic/hubs/connections/index.html
diff --git a/docs/expensify-classic/hubs/expenses/reports.html b/docs/expensify-classic/hubs/connections/netsuite.html
similarity index 100%
rename from docs/expensify-classic/hubs/expenses/reports.html
rename to docs/expensify-classic/hubs/connections/netsuite.html
diff --git a/docs/expensify-classic/hubs/getting-started/approved-accountants.html b/docs/expensify-classic/hubs/connections/quickbooks-desktop.html
similarity index 100%
rename from docs/expensify-classic/hubs/getting-started/approved-accountants.html
rename to docs/expensify-classic/hubs/connections/quickbooks-desktop.html
diff --git a/docs/expensify-classic/hubs/getting-started/support.html b/docs/expensify-classic/hubs/connections/quickbooks-online.html
similarity index 100%
rename from docs/expensify-classic/hubs/getting-started/support.html
rename to docs/expensify-classic/hubs/connections/quickbooks-online.html
diff --git a/docs/expensify-classic/hubs/getting-started/tips-and-tricks.html b/docs/expensify-classic/hubs/connections/sage-intacct.html
similarity index 100%
rename from docs/expensify-classic/hubs/getting-started/tips-and-tricks.html
rename to docs/expensify-classic/hubs/connections/sage-intacct.html
diff --git a/docs/expensify-classic/hubs/integrations/HR-integrations.html b/docs/expensify-classic/hubs/connections/xero.html
similarity index 100%
rename from docs/expensify-classic/hubs/integrations/HR-integrations.html
rename to docs/expensify-classic/hubs/connections/xero.html
diff --git a/docs/expensify-classic/hubs/integrations/accounting-integrations.html b/docs/expensify-classic/hubs/integrations/accounting-integrations.html
deleted file mode 100644
index 86641ee60b7d..000000000000
--- a/docs/expensify-classic/hubs/integrations/accounting-integrations.html
+++ /dev/null
@@ -1,5 +0,0 @@
----
-layout: default
----
-
-{% include section.html %}
diff --git a/docs/expensify-classic/hubs/integrations/other-integrations.html b/docs/expensify-classic/hubs/integrations/other-integrations.html
deleted file mode 100644
index 86641ee60b7d..000000000000
--- a/docs/expensify-classic/hubs/integrations/other-integrations.html
+++ /dev/null
@@ -1,5 +0,0 @@
----
-layout: default
----
-
-{% include section.html %}
diff --git a/docs/expensify-classic/hubs/integrations/travel-integrations.html b/docs/expensify-classic/hubs/integrations/travel-integrations.html
deleted file mode 100644
index 86641ee60b7d..000000000000
--- a/docs/expensify-classic/hubs/integrations/travel-integrations.html
+++ /dev/null
@@ -1,5 +0,0 @@
----
-layout: default
----
-
-{% include section.html %}
diff --git a/docs/expensify-classic/hubs/workspaces/reports.html b/docs/expensify-classic/hubs/workspaces/reports.html
deleted file mode 100644
index 86641ee60b7d..000000000000
--- a/docs/expensify-classic/hubs/workspaces/reports.html
+++ /dev/null
@@ -1,5 +0,0 @@
----
-layout: default
----
-
-{% include section.html %}
diff --git a/docs/new-expensify/hubs/expenses/index.html b/docs/new-expensify/hubs/expenses-&-payments/index.html
similarity index 100%
rename from docs/new-expensify/hubs/expenses/index.html
rename to docs/new-expensify/hubs/expenses-&-payments/index.html
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 1c849e0aabdc..9e87328e6733 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -203,3 +203,50 @@ https://help.expensify.com/articles/new-expensify/chat/Expensify-Chat-For-Admins
https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.html,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account
https://help.expensify.com/articles/expensify-classic/travel/Coming-Soon,https://help.expensify.com/expensify-classic/hubs/travel/
https://help.expensify.com/articles/new-expensify/expenses/Manually-submit-reports-for-approval,https://help.expensify.com/new-expensify/hubs/expenses/
+https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Reimbursements.html,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments
+https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.html,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills
+https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.html,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills
+https://help.expensify.com/articles/expensify-classic/expensify-card/Auto-Reconciliation,https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation.md
+https://help.expensify.com/articles/new-expensify/expenses/Approve-and-pay-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses
+https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account
+https://help.expensify.com/articles/new-expensify/expenses/Create-an-expense,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Create-an-expense
+https://help.expensify.com/articles/new-expensify/expenses/Distance-Requests,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Distance-Requests
+https://help.expensify.com/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Resolve-Errors-Adding-a-Bank-Account
+https://help.expensify.com/articles/new-expensify/expenses/Send-an-invoice,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Send-an-invoice
+https://help.expensify.com/articles/new-expensify/expenses/Set-up-your-wallet,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Set-up-your-wallet
+https://help.expensify.com/articles/new-expensify/expenses/Split-an-expense,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Split-an-expense
+https://help.expensify.com/articles/new-expensify/expenses/Track-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Track-expenses
+https://help.expensify.com/articles/new-expensify/expenses/Unlock-a-Business-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Unlock-a-Business-Bank-Account
+https://help.expensify.com/expensify-classic/hubs/integrations/HR-integrations,https://help.expensify.com/expensify-classic/hubs/connections
+https://help.expensify.com/expensify-classic/hubs/integrations/accounting-integrations,https://help.expensify.com/expensify-classic/hubs/connections
+https://help.expensify.com/expensify-classic/hubs/integrations/other-integrations,https://help.expensify.com/expensify-classic/hubs/connections
+https://help.expensify.com/expensify-classic/hubs/integrations/travel-integrations,https://help.expensify.com/expensify-classic/hubs/connections
+https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP,https://help.expensify.com/articles/expensify-classic/connections/ADP
+https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Accelo,https://help.expensify.com/expensify-classic/hubs/connections/accelo
+https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations,https://help.expensify.com/articles/expensify-classic/connections/Additional-Travel-Integrations
+https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Certinia,https://help.expensify.com/expensify-classic/hubs/connections/certinia
+https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Egencia,https://help.expensify.com/articles/expensify-classic/connections/Egencia
+https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Global-VaTax,https://help.expensify.com/articles/expensify-classic/connections/Global-VaTax
+https://help.expensify.com/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO,https://help.expensify.com/articles/expensify-classic/connections/Google-Apps-SSO
+https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Greenhouse,https://help.expensify.com/articles/expensify-classic/connections/Greenhouse
+https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Gusto,https://help.expensify.com/articles/expensify-classic/connections/Gusto
+https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations,https://help.expensify.com/articles/expensify-classic/connections/Indirect-Accounting-Integrations
+https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Lyft,https://help.expensify.com/articles/expensify-classic/connections/Lyft
+https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Navan,https://help.expensify.com/articles/expensify-classic/connections/Navan
+https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite,https://help.expensify.com/expensify-classic/hubs/connections/netsuite
+https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time,https://help.expensify.com/articles/expensify-classic/connections/QuickBooks-Time
+https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop,https://help.expensify.com/expensify-classic/hubs/connections/quickbooks-desktop
+https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online,https://help.expensify.com/expensify-classic/hubs/connections/quickbooks-online
+https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Rippling,https://help.expensify.com/articles/expensify-classic/connections/Rippling
+https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct,https://help.expensify.com/expensify-classic/hubs/connections/sage-intacct
+https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/TravelPerk,https://help.expensify.com/articles/expensify-classic/connections/TravelPerk
+https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Uber,https://help.expensify.com/articles/expensify-classic/connections/Uber
+https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Workday,https://help.expensify.com/articles/expensify-classic/connections/Workday
+https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Xero,https://help.expensify.com/expensify-classic/hubs/connections/xero
+https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Zenefits,https://help.expensify.com/articles/expensify-classic/connections/Zenefits
+https://help.expensify.com/articles/expensify-classic/workspaces/tax-tracking,https://help.expensify.com/articles/expensify-classic/workspaces/Tax-Tracking
+https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows,https://help.expensify.com/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees
+https://help.expensify.com/articles/expensify-classic/settings/Notification-Troubleshooting,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications
+https://help.expensify.com/articles/new-expensify/expenses/Validate-a-Business-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Validate-a-Business-Bank-Account
+https://help.expensify.com/articles/expensify-classic/workspaces/Expenses,https://help.expensify.com/articles/expensify-classic/workspaces/Expense-Settings
+https://help.expensify.com/articles/expensify-classic/workspaces/Reimbursement,https://help.expensify.com/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index af9e798d2343..2560e48728c5 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -10,6 +10,8 @@
# https://docs.fastlane.tools/plugins/available-plugins
#
+require 'ostruct'
+
skip_docs
opt_out_usage
diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg
index 32ed6ba30059..d5f3c582327d 100644
Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ
diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg
index 5712b0d86b19..8300bd34ef76 100644
Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ
diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj
index f86952ca7aca..164f8485129b 100644
--- a/ios/NewExpensify.xcodeproj/project.pbxproj
+++ b/ios/NewExpensify.xcodeproj/project.pbxproj
@@ -388,6 +388,7 @@
FBC7D704E4E9CC08E91D7919 /* [CP] Copy Pods Resources */,
9FF963998EFF771D82D473D2 /* [CP-User] [RNFB] Core Configuration */,
A2BE84E8C8EFD6C81A2B41F1 /* [CP-User] [RNFB] Crashlytics Configuration */,
+ 498240F82C49553900C15857 /* Run Fullstory Asset Uploader */,
);
buildRules = (
);
@@ -620,6 +621,24 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-resources.sh\"\n";
showEnvVarsInLog = 0;
};
+ 498240F82C49553900C15857 /* Run Fullstory Asset Uploader */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = "Run Fullstory Asset Uploader";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/FullStory/tools/FullStoryCommandLine\" \"${CONFIGURATION_BUILD_DIR}/${WRAPPER_NAME}\"\n";
+ };
5CF45ABA52C0BB0D7B9D139A /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 57a582d2a22b..8ef960cc349c 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
-
9.0.3
+
9.0.10
CFBundleSignature
????
CFBundleURLTypes
@@ -40,11 +40,13 @@
CFBundleVersion
-
9.0.3.2
+
9.0.10.2
FullStory
OrgId
o-1WN56P-na1
+ RecordOnStart
+
ITSAppUsesNonExemptEncryption
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 1ba368709984..95022fd78c43 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
-
9.0.3
+
9.0.10
CFBundleSignature
????
CFBundleVersion
-
9.0.3.2
+
9.0.10.2
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index bb9a344b3314..8b09b94feb3f 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
-
9.0.3
+
9.0.10
CFBundleVersion
-
9.0.3.2
+
9.0.10.2
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 35dccc2de393..54fcd1e83993 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
@@ -303,7 +303,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - Plaid (5.2.1)
+ - Plaid (5.6.0)
- PromisesObjC (2.4.0)
- RCT-Folly (2022.05.16.00):
- boost
@@ -1243,7 +1243,13 @@ PODS:
- react-native-config (1.5.0):
- react-native-config/App (= 1.5.0)
- react-native-config/App (1.5.0):
- - React-Core
+ - RCT-Folly
+ - RCTRequired
+ - RCTTypeSafety
+ - React
+ - React-Codegen
+ - React-RCTFabric
+ - ReactCommon/turbomodule/core
- react-native-document-picker (9.1.1):
- RCT-Folly
- RCTRequired
@@ -1251,7 +1257,7 @@ PODS:
- React-Codegen
- React-Core
- ReactCommon/turbomodule/core
- - react-native-geolocation (3.2.1):
+ - react-native-geolocation (3.3.0):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1394,10 +1400,10 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - react-native-plaid-link-sdk (11.5.0):
+ - react-native-plaid-link-sdk (11.11.0):
- glog
- hermes-engine
- - Plaid (~> 5.2.0)
+ - Plaid (~> 5.6.0)
- RCT-Folly (= 2022.05.16.00)
- RCTRequired
- RCTTypeSafety
@@ -1433,7 +1439,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - react-native-release-profiler (0.1.6):
+ - react-native-release-profiler (0.2.1):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1865,7 +1871,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.88):
+ - RNLiveMarkdown (0.1.105):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1883,9 +1889,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNLiveMarkdown/common (= 0.1.88)
+ - RNLiveMarkdown/common (= 0.1.105)
- Yoga
- - RNLiveMarkdown/common (0.1.88):
+ - RNLiveMarkdown/common (0.1.105):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1929,7 +1935,7 @@ PODS:
- ReactCommon/turbomodule/core
- Turf
- Yoga
- - RNPermissions (3.9.3):
+ - RNPermissions (3.10.1):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1974,7 +1980,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - RNScreens (3.30.1):
+ - RNScreens (3.32.0):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1988,13 +1994,14 @@ PODS:
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
+ - React-RCTImage
- React-rendererdebug
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNScreens/common (= 3.30.1)
+ - RNScreens/common (= 3.32.0)
- Yoga
- - RNScreens/common (3.30.1):
+ - RNScreens/common (3.32.0):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2008,6 +2015,7 @@ PODS:
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
+ - React-RCTImage
- React-rendererdebug
- React-utils
- ReactCommon/turbomodule/bridging
@@ -2026,7 +2034,7 @@ PODS:
- RNSound/Core (= 0.11.2)
- RNSound/Core (0.11.2):
- React-Core
- - RNSVG (14.1.0):
+ - RNSVG (15.4.0):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2044,9 +2052,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNSVG/common (= 14.1.0)
+ - RNSVG/common (= 15.4.0)
- Yoga
- - RNSVG/common (14.1.0):
+ - RNSVG/common (15.4.0):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2523,9 +2531,9 @@ SPEC CHECKSUMS:
MapboxMaps: 87ef0003e6db46e45e7a16939f29ae87e38e7ce2
MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
- Onfido: 342cbecd7a4383e98dfe7f9c35e98aaece599062
+ Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af
onfido-react-native-sdk: 3e3b0dd70afa97410fb318d54c6a415137968ef2
- Plaid: 7829e84db6d766a751c91a402702946d2977ddcb
+ Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
RCTRequired: ab7f915c15569f04a49669e573e6e319a53f9faa
@@ -2552,9 +2560,9 @@ SPEC CHECKSUMS:
react-native-airship: 38e2596999242b68c933959d6145512e77937ac0
react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b
react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957
- react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727
+ react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c
react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809
- react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3
+ react-native-geolocation: 580c86eb531c0aaf7a14bc76fd2983ce47ca58aa
react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440
react-native-key-command: 28ccfa09520e7d7e30739480dea4df003493bfe8
react-native-keyboard-controller: 47c01b0741ae5fc84e53cf282e61cfa5c2edb19b
@@ -2563,9 +2571,9 @@ SPEC CHECKSUMS:
react-native-pager-view: ccd4bbf9fc7effaf8f91f8dae43389844d9ef9fa
react-native-pdf: 762369633665ec02ac227aefe2f4558b92475c23
react-native-performance: fb21ff0c9bd7a10789c69d948f25b0067d29f7a9
- react-native-plaid-link-sdk: 2a91ef7e257ae16d180a1ca14ba3041ae0836fbf
+ react-native-plaid-link-sdk: ba40d1b13cca4b946974fafd9ae278e0fb697d87
react-native-quick-sqlite: e3ab3e0a29d8c705f47a60aaa6ceaa42eb6a9ec1
- react-native-release-profiler: 14ccdc0eeb03bedf625cf68d53d80275a81b19dd
+ react-native-release-profiler: a77d4f291b92e48d3d4a574deed19bd1b513ac98
react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c
react-native-safe-area-context: 9d79895b60b8be151fdf6faef9d2d0591eeecc63
react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688
@@ -2606,16 +2614,16 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: e33d2c97863d5480f8f4b45f8b25f801cc43c7f5
+ RNLiveMarkdown: bae86068a0b80cda3d4875ac2a5a398955a94922
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c
- RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216
+ RNPermissions: d2392b754e67bc14491f5b12588bef2864e783f3
RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37
RNReanimated: 323436b1a5364dca3b5f8b1a13458455e0de9efe
- RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1
+ RNScreens: abd354e98519ed267600b7ee64fdcb8e060b1218
RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58
RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852
- RNSVG: 18f1381e046be2f1c30b4724db8d0c966238089f
+ RNSVG: 8c067e7203053d4c82f456cbeab1fe509ac797dd
SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
SDWebImageAVIFCoder: 8348fef6d0ec69e129c66c9fe4d74fbfbf366112
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
@@ -2623,7 +2631,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055
- Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70
+ Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312
PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14
diff --git a/jest/setup.ts b/jest/setup.ts
index f11a8a4ed631..c1a737c5def8 100644
--- a/jest/setup.ts
+++ b/jest/setup.ts
@@ -1,6 +1,8 @@
import '@shopify/flash-list/jestSetup';
import 'react-native-gesture-handler/jestSetup';
+import type * as RNKeyboardController from 'react-native-keyboard-controller';
import mockStorage from 'react-native-onyx/dist/storage/__mocks__';
+import type Animated from 'react-native-reanimated';
import 'setimmediate';
import mockFSLibrary from './setupMockFullstoryLib';
import setupMockImages from './setupMockImages';
@@ -20,6 +22,16 @@ jest.mock('react-native-onyx/dist/storage', () => mockStorage);
// Mock NativeEventEmitter as it is needed to provide mocks of libraries which include it
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter');
+// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest
+jest.mock('react-native/Libraries/LogBox/LogBox', () => ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ __esModule: true,
+ default: {
+ ignoreLogs: jest.fn(),
+ ignoreAllLogs: jest.fn(),
+ },
+}));
+
// Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise
jest.spyOn(console, 'debug').mockImplementation((...params: string[]) => {
if (params[0].startsWith('Timing:')) {
@@ -54,5 +66,10 @@ jest.mock('react-native-share', () => ({
default: jest.fn(),
}));
-// eslint-disable-next-line @typescript-eslint/no-unsafe-return
-jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest'));
+jest.mock('react-native-reanimated', () => ({
+ ...jest.requireActual('react-native-reanimated/mock'),
+ createAnimatedPropAdapter: jest.fn,
+ useReducedMotion: jest.fn,
+}));
+
+jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest'));
diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts
index 9edfccab9441..eae3ea1f51bd 100644
--- a/jest/setupMockFullstoryLib.ts
+++ b/jest/setupMockFullstoryLib.ts
@@ -15,7 +15,7 @@ export default function mockFSLibrary() {
return {
FSPage(): FSPageInterface {
return {
- start: jest.fn(),
+ start: jest.fn(() => {}),
};
},
default: Fullstory,
diff --git a/package-lock.json b/package-lock.json
index 40805e7a2fa2..a5f41540d008 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "new.expensify",
- "version": "9.0.3-2",
+ "version": "9.0.10-2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.3-2",
+ "version": "9.0.10-2",
"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.105",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -26,12 +26,12 @@
"@fullstory/react-native": "^1.4.2",
"@gorhom/portal": "^1.0.14",
"@invertase/react-native-apple-authentication": "^2.2.2",
- "@kie/act-js": "^2.6.0",
+ "@kie/act-js": "^2.6.2",
"@kie/mock-github": "2.0.1",
"@onfido/react-native-sdk": "10.6.0",
"@react-native-camera-roll/camera-roll": "7.4.0",
"@react-native-clipboard/clipboard": "^1.13.2",
- "@react-native-community/geolocation": "3.2.1",
+ "@react-native-community/geolocation": "3.3.0",
"@react-native-community/netinfo": "11.2.1",
"@react-native-firebase/analytics": "^12.3.0",
"@react-native-firebase/app": "^12.3.0",
@@ -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.49",
"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",
@@ -102,23 +102,23 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "2.0.54",
+ "react-native-onyx": "2.0.56",
"react-native-pager-view": "6.2.3",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
- "react-native-permissions": "^3.9.3",
+ "react-native-permissions": "^3.10.0",
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf",
- "react-native-plaid-link-sdk": "11.5.0",
- "react-native-qrcode-svg": "^6.2.0",
+ "react-native-plaid-link-sdk": "11.11.0",
+ "react-native-qrcode-svg": "git+https://github.com/Expensify/react-native-qrcode-svg",
"react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316",
"react-native-reanimated": "^3.8.0",
- "react-native-release-profiler": "^0.1.6",
+ "react-native-release-profiler": "^0.2.1",
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.8.2",
- "react-native-screens": "3.30.1",
+ "react-native-screens": "3.32.0",
"react-native-share": "^10.0.2",
"react-native-sound": "^0.11.2",
- "react-native-svg": "14.1.0",
+ "react-native-svg": "15.4.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "3.8.0",
@@ -201,7 +201,7 @@
"babel-jest": "29.4.1",
"babel-loader": "^9.1.3",
"babel-plugin-module-resolver": "^5.0.0",
- "babel-plugin-react-compiler": "^0.0.0-experimental-c23de8d-20240515",
+ "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625",
"babel-plugin-react-native-web": "^0.18.7",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-remove-console": "^6.9.4",
@@ -213,14 +213,14 @@
"diff-so-fancy": "^1.3.0",
"dotenv": "^16.0.3",
"electron": "^29.4.1",
- "electron-builder": "24.13.2",
+ "electron-builder": "25.0.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-expensify": "^2.0.52",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jsdoc": "^46.2.6",
- "eslint-plugin-react-compiler": "^0.0.0-experimental-53bb89e-20240515",
+ "eslint-plugin-react-compiler": "0.0.0-experimental-0998c1e-20240625",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^6.2.2",
@@ -235,9 +235,11 @@
"onchange": "^7.1.0",
"openai": "^4.47.2",
"patch-package": "^8.0.0",
+ "peggy": "^4.0.3",
"portfinder": "^1.0.28",
"prettier": "^2.8.8",
"pusher-js-mock": "^0.3.3",
+ "react-compiler-healthcheck": "^0.0.0-experimental-b130d5f-20240625",
"react-is": "^18.3.1",
"react-native-clean-project": "^4.0.0-alpha4.0",
"react-test-renderer": "18.2.0",
@@ -250,7 +252,7 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
- "type-fest": "^4.10.2",
+ "type-fest": "4.20.0",
"typescript": "^5.4.5",
"wait-port": "^0.2.9",
"webpack": "^5.76.0",
@@ -2982,8 +2984,9 @@
},
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
+ "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==",
"dev": true,
- "license": "MIT",
"dependencies": {
"ajv": "^6.12.0",
"ajv-keywords": "^3.4.1"
@@ -2998,8 +3001,9 @@
},
"node_modules/@develar/schema-utils/node_modules/ajv": {
"version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -3013,16 +3017,18 @@
},
"node_modules/@develar/schema-utils/node_modules/ajv-keywords": {
"version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
- "license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/@develar/schema-utils/node_modules/json-schema-traverse": {
"version": "0.4.1",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
@@ -3107,9 +3113,10 @@
}
},
"node_modules/@electron/asar": {
- "version": "3.2.8",
+ "version": "3.2.10",
+ "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.10.tgz",
+ "integrity": "sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"commander": "^5.0.0",
"glob": "^7.1.6",
@@ -3124,8 +3131,9 @@
},
"node_modules/@electron/asar/node_modules/commander": {
"version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+ "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 6"
}
@@ -3188,9 +3196,10 @@
}
},
"node_modules/@electron/notarize": {
- "version": "2.2.1",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.3.2.tgz",
+ "integrity": "sha512-zfayxCe19euNwRycCty1C7lF7snk9YwfRpB5M8GLr1a4ICH63znxaPNAubrMvj0yDvVozqfgsdYpXVUnpWBDpg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"fs-extra": "^9.0.1",
@@ -3201,9 +3210,10 @@
}
},
"node_modules/@electron/osx-sign": {
- "version": "1.0.5",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.0.tgz",
+ "integrity": "sha512-TEXhxlYSDRr9JWK5nWdOv5MtuUdaZ412uxIIEQ0hLt80o0HYWtQJBlW5QmrQDMtebzATaOjKG9UfCzLyA90zWQ==",
"dev": true,
- "license": "BSD-2-Clause",
"dependencies": {
"compare-version": "^0.1.2",
"debug": "^4.3.4",
@@ -3222,8 +3232,9 @@
},
"node_modules/@electron/osx-sign/node_modules/fs-extra": {
"version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -3235,8 +3246,9 @@
},
"node_modules/@electron/osx-sign/node_modules/isbinaryfile": {
"version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
+ "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 8.0.0"
},
@@ -3244,21 +3256,172 @@
"url": "https://github.com/sponsors/gjtorikian/"
}
},
+ "node_modules/@electron/rebuild": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.0.tgz",
+ "integrity": "sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw==",
+ "dev": true,
+ "dependencies": {
+ "@malept/cross-spawn-promise": "^2.0.0",
+ "chalk": "^4.0.0",
+ "debug": "^4.1.1",
+ "detect-libc": "^2.0.1",
+ "fs-extra": "^10.0.0",
+ "got": "^11.7.0",
+ "node-abi": "^3.45.0",
+ "node-api-version": "^0.2.0",
+ "node-gyp": "^9.0.0",
+ "ora": "^5.1.0",
+ "read-binary-file-arch": "^1.0.6",
+ "semver": "^7.3.5",
+ "tar": "^6.0.5",
+ "yargs": "^17.0.1"
+ },
+ "bin": {
+ "electron-rebuild": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/@electron/rebuild/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/@electron/universal": {
- "version": "1.5.1",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
+ "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@electron/asar": "^3.2.1",
- "@malept/cross-spawn-promise": "^1.1.0",
+ "@electron/asar": "^3.2.7",
+ "@malept/cross-spawn-promise": "^2.0.0",
"debug": "^4.3.1",
- "dir-compare": "^3.0.0",
- "fs-extra": "^9.0.1",
- "minimatch": "^3.0.4",
- "plist": "^3.0.4"
+ "dir-compare": "^4.2.0",
+ "fs-extra": "^11.1.1",
+ "minimatch": "^9.0.3",
+ "plist": "^3.1.0"
},
"engines": {
- "node": ">=8.6"
+ "node": ">=16.4"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/fs-extra": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
+ "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
@@ -3785,9 +3948,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.105",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.105.tgz",
+ "integrity": "sha512-4cecVQFQQjdszUHtkHVT7sMwKntGCfxH62DJvbpa6H3eTuz3RCHrzWOZBQGRDVpn0aM3nwVgcWZSvNsDvT8Ziw==",
"workspaces": [
"parser",
"example",
@@ -6124,28 +6287,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@jest/console/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/console/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/@jest/console/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -6249,28 +6390,6 @@
}
}
},
- "node_modules/@jest/core/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/core/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/@jest/core/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -6339,86 +6458,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/create-cache-key-function/node_modules/@types/yargs": {
- "version": "17.0.31",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/@jest/create-cache-key-function/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@jest/create-cache-key-function/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/@jest/create-cache-key-function/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@jest/create-cache-key-function/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
- "node_modules/@jest/create-cache-key-function/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jest/create-cache-key-function/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@jest/environment": {
"version": "29.7.0",
"license": "MIT",
@@ -6432,86 +6471,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@jest/environment/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/environment/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/@jest/environment/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@jest/environment/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/@jest/environment/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@jest/environment/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
- "node_modules/@jest/environment/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jest/environment/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@jest/expect": {
"version": "29.6.2",
"license": "MIT",
@@ -6548,86 +6507,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@jest/fake-timers/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/fake-timers/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/@jest/fake-timers/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@jest/fake-timers/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/@jest/fake-timers/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@jest/fake-timers/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
- "node_modules/@jest/fake-timers/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jest/fake-timers/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@jest/globals": {
"version": "29.5.0",
"license": "MIT",
@@ -6641,86 +6520,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@jest/globals/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/globals/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/@jest/globals/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@jest/globals/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/@jest/globals/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@jest/globals/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
- "node_modules/@jest/globals/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jest/globals/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@jest/reporters": {
"version": "29.4.1",
"license": "MIT",
@@ -6762,28 +6561,6 @@
}
}
},
- "node_modules/@jest/reporters/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/reporters/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/@jest/reporters/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -6903,86 +6680,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@jest/test-result/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/test-result/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/@jest/test-result/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@jest/test-result/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/@jest/test-result/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@jest/test-result/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
- "node_modules/@jest/test-result/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jest/test-result/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@jest/test-sequencer": {
"version": "29.4.1",
"license": "MIT",
@@ -7020,28 +6717,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@jest/transform/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/transform/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/@jest/transform/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -7119,10 +6794,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
- "dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
@@ -7137,10 +6809,7 @@
},
"node_modules/@jest/types/node_modules/ansi-styles": {
"version": "4.3.0",
- "dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -7153,10 +6822,7 @@
},
"node_modules/@jest/types/node_modules/chalk": {
"version": "4.1.2",
- "dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -7170,10 +6836,7 @@
},
"node_modules/@jest/types/node_modules/color-convert": {
"version": "2.0.1",
- "dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -7183,27 +6846,18 @@
},
"node_modules/@jest/types/node_modules/color-name": {
"version": "1.1.4",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
+ "license": "MIT"
},
"node_modules/@jest/types/node_modules/has-flag": {
"version": "4.0.0",
- "dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@jest/types/node_modules/supports-color": {
"version": "7.2.0",
- "dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -7285,9 +6939,10 @@
}
},
"node_modules/@kie/act-js": {
- "version": "2.6.0",
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@kie/act-js/-/act-js-2.6.2.tgz",
+ "integrity": "sha512-i366cfWluUi55rPZ6e9/aWH4tnw3Q6W1CKh9Gz6QjTvbAtS4KnUUy33I9aMXS6uwa0haw6MSahMM37vmuFCVpQ==",
"hasInstallScript": true,
- "license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@kie/mock-github": "^2.0.0",
"adm-zip": "^0.5.10",
@@ -7417,7 +7072,9 @@
}
},
"node_modules/@malept/cross-spawn-promise": {
- "version": "1.1.1",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
+ "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==",
"dev": true,
"funding": [
{
@@ -7429,18 +7086,18 @@
"url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
}
],
- "license": "Apache-2.0",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"engines": {
- "node": ">= 10"
+ "node": ">= 12.13.0"
}
},
"node_modules/@malept/flatpak-bundler": {
"version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz",
+ "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"fs-extra": "^9.0.0",
@@ -7886,14 +7543,41 @@
"react-native": ">=0.70.0 <1.0.x"
}
},
+ "node_modules/@peggyjs/from-mem": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@peggyjs/from-mem/-/from-mem-1.3.0.tgz",
+ "integrity": "sha512-kzGoIRJjkg3KuGI4bopz9UvF3KguzfxalHRDEIdqEZUe45xezsQ6cx30e0RKuxPUexojQRBfu89Okn7f4/QXsw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "7.6.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@peggyjs/from-mem/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@perf-profiler/android": {
- "version": "0.12.1",
- "resolved": "https://registry.npmjs.org/@perf-profiler/android/-/android-0.12.1.tgz",
- "integrity": "sha512-t4E2tfj9UdJw5JjhFPLMzrsu3NkKSyiZyeIyd70HX9d3anWqNK47XuQV+qkDPMjWaoU+CTlj1SuNnIOqEkCpSA==",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@perf-profiler/android/-/android-0.13.0.tgz",
+ "integrity": "sha512-4lUQjJNHFAYB5npts5JLrPaPNpIOEAAjfpeTQOOgBNLT1NW50WWSGuvV2pAdnMi7T28cXs3aUziJJ30cNrSvNg==",
"dev": true,
"dependencies": {
"@perf-profiler/logger": "^0.3.3",
- "@perf-profiler/profiler": "^0.10.10",
+ "@perf-profiler/profiler": "^0.10.11",
"@perf-profiler/types": "^0.8.0",
"commander": "^12.0.0",
"lodash": "^4.17.21"
@@ -7912,24 +7596,24 @@
}
},
"node_modules/@perf-profiler/ios": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@perf-profiler/ios/-/ios-0.3.2.tgz",
- "integrity": "sha512-2jYyHXFO3xe5BdvU1Ttt+Uw2nAf10B3/mcx4FauJwSdJ+nlOAKIvxmZDvMcipCZZ63uc+HWsYndhziJZVQ7VUw==",
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@perf-profiler/ios/-/ios-0.3.3.tgz",
+ "integrity": "sha512-dbb9lVKOyj1VjinuxrnbfI3FT0+uhH3xclqLQH7rQFA4d93dusjC/s3RzlnCXRNPFPDy5TTmkiIg3xM/6P3/2g==",
"dev": true,
"dependencies": {
- "@perf-profiler/ios-instruments": "^0.3.2",
+ "@perf-profiler/ios-instruments": "^0.3.3",
"@perf-profiler/logger": "^0.3.3",
"@perf-profiler/types": "^0.8.0"
}
},
"node_modules/@perf-profiler/ios-instruments": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@perf-profiler/ios-instruments/-/ios-instruments-0.3.2.tgz",
- "integrity": "sha512-uox5arQscpRuGWfzBrTpsn6eJq0ErdjPlU0FMbN4Cv5akQC11ejKWmgV6y4FR/0YIET9uiiXMtnwyEBgUunYGQ==",
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@perf-profiler/ios-instruments/-/ios-instruments-0.3.3.tgz",
+ "integrity": "sha512-e3UmlWuNUOuNbJPWg6aLOVd9wRKe3RYCqwwUgxMNIAwa5QBxaVYBf2pt3+HRsYReh2qm1yvqW7LU9zP+AJ7/7g==",
"dev": true,
"dependencies": {
"@perf-profiler/logger": "^0.3.3",
- "@perf-profiler/profiler": "^0.10.10",
+ "@perf-profiler/profiler": "^0.10.11",
"@perf-profiler/types": "^0.8.0",
"commander": "^12.0.0",
"fast-xml-parser": "^4.2.7"
@@ -7970,13 +7654,13 @@
}
},
"node_modules/@perf-profiler/profiler": {
- "version": "0.10.10",
- "resolved": "https://registry.npmjs.org/@perf-profiler/profiler/-/profiler-0.10.10.tgz",
- "integrity": "sha512-kvVC6VQ7pBdthcWEcLTua+iDj0ZkcmYYL9gXHa9Dl7jYkZI4cOeslJZ1vuGfIcC168JwAVrB8UYhgoSgss/MWQ==",
+ "version": "0.10.11",
+ "resolved": "https://registry.npmjs.org/@perf-profiler/profiler/-/profiler-0.10.11.tgz",
+ "integrity": "sha512-nu/zakhG5wRi0tCw4SjTCZJh9e/x9YABAOChh3lGI6CESsFzc1Gi2Vrr+2sytN8dpiTDYCCbECC2EalD7ZKvtg==",
"dev": true,
"dependencies": {
- "@perf-profiler/android": "^0.12.1",
- "@perf-profiler/ios": "^0.3.2",
+ "@perf-profiler/android": "^0.13.0",
+ "@perf-profiler/ios": "^0.3.3",
"@perf-profiler/types": "^0.8.0"
}
},
@@ -9051,9 +8735,9 @@
}
},
"node_modules/@react-native-community/cli-server-api/node_modules/ws": {
- "version": "7.5.9",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
- "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"engines": {
"node": ">=8.3.0"
},
@@ -9561,9 +9245,9 @@
"license": "MIT"
},
"node_modules/@react-native-community/geolocation": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.2.1.tgz",
- "integrity": "sha512-/+HNzuRl4UCMma7KK+KYL8k2nxAGuW+DGxqmqfpiqKBlCkCUbuFHaZZdqVD6jpsn9r/ghe583ECLmd9SV9I4Bw==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.3.0.tgz",
+ "integrity": "sha512-7DFeuotH7m7ImoXffN3TmlGSFn1XjvsaphPort0XZKipssYbdHiKhVVWG+jzisvDhcXikUc6nbUJgddVBL6RDg==",
"engines": {
"node": ">=18.0.0"
},
@@ -17633,8 +17317,9 @@
},
"node_modules/@types/debug": {
"version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/ms": "*"
}
@@ -17721,8 +17406,9 @@
},
"node_modules/@types/fs-extra": {
"version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
+ "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -17909,8 +17595,9 @@
},
"node_modules/@types/ms": {
"version": "0.7.34",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
+ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
+ "dev": true
},
"node_modules/@types/node": {
"version": "20.11.5",
@@ -17965,8 +17652,9 @@
},
"node_modules/@types/plist": {
"version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
+ "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*",
@@ -18174,9 +17862,10 @@
"dev": true
},
"node_modules/@types/verror": {
- "version": "1.10.9",
+ "version": "1.10.10",
+ "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz",
+ "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==",
"dev": true,
- "license": "MIT",
"optional": true
},
"node_modules/@types/webpack": {
@@ -18212,10 +17901,7 @@
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
"integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
- "dev": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"@types/yargs-parser": "*"
}
@@ -18949,8 +18635,9 @@
},
"node_modules/7zip-bin": {
"version": "5.2.0",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
+ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
+ "dev": true
},
"node_modules/abab": {
"version": "2.0.6",
@@ -18958,8 +18645,8 @@
},
"node_modules/abbrev": {
"version": "1.1.1",
- "license": "ISC",
- "optional": true
+ "devOptional": true,
+ "license": "ISC"
},
"node_modules/abort-controller": {
"version": "3.0.0",
@@ -19309,29 +18996,32 @@
}
},
"node_modules/app-builder-bin": {
- "version": "4.0.0",
- "dev": true,
- "license": "MIT"
+ "version": "5.0.0-alpha.4",
+ "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.4.tgz",
+ "integrity": "sha512-4MitKmOtfTdMONrtRoiaqJ6HtlVZXgrNX1PNdEzEHSAoXU85x7s+mo0IhAS9K9qgjyTVuLrM1E/HAMp5qGyoOA==",
+ "dev": true
},
"node_modules/app-builder-lib": {
- "version": "24.13.2",
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.0.0.tgz",
+ "integrity": "sha512-GIx0n/QvbeObY8rQTTp08UPn4pS9xSGZLq6cPRy/CyX/mTNN9pO/uU28MWgqjnYXk0bf/595vzDdAijuDyz5Zw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@develar/schema-utils": "~2.6.5",
- "@electron/notarize": "2.2.1",
- "@electron/osx-sign": "1.0.5",
- "@electron/universal": "1.5.1",
+ "@electron/notarize": "2.3.2",
+ "@electron/osx-sign": "1.3.0",
+ "@electron/rebuild": "3.6.0",
+ "@electron/universal": "2.0.1",
"@malept/flatpak-bundler": "^0.4.0",
"@types/fs-extra": "9.0.13",
"async-exit-hook": "^2.0.1",
"bluebird-lst": "^1.0.9",
- "builder-util": "24.13.1",
- "builder-util-runtime": "9.2.4",
+ "builder-util": "25.0.0",
+ "builder-util-runtime": "9.2.5",
"chromium-pickle-js": "^0.2.0",
"debug": "^4.3.4",
"ejs": "^3.1.8",
- "electron-publish": "24.13.1",
+ "electron-publish": "25.0.0",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"hosted-git-info": "^4.1.0",
@@ -19339,8 +19029,9 @@
"isbinaryfile": "^5.0.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
- "minimatch": "^5.1.1",
- "read-config-file": "6.3.2",
+ "minimatch": "^10.0.0",
+ "read-config-file": "6.4.0",
+ "resedit": "^1.7.0",
"sanitize-filename": "^1.6.3",
"semver": "^7.3.8",
"tar": "^6.1.12",
@@ -19350,27 +19041,30 @@
"node": ">=14.0.0"
},
"peerDependencies": {
- "dmg-builder": "24.13.2",
- "electron-builder-squirrel-windows": "24.13.2"
+ "dmg-builder": "25.0.0",
+ "electron-builder-squirrel-windows": "25.0.0"
}
},
"node_modules/app-builder-lib/node_modules/argparse": {
"version": "2.0.1",
- "dev": true,
- "license": "Python-2.0"
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
},
"node_modules/app-builder-lib/node_modules/brace-expansion": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/app-builder-lib/node_modules/form-data": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
- "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -19382,8 +19076,9 @@
},
"node_modules/app-builder-lib/node_modules/fs-extra": {
"version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -19395,8 +19090,9 @@
},
"node_modules/app-builder-lib/node_modules/js-yaml": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -19405,14 +19101,18 @@
}
},
"node_modules/app-builder-lib/node_modules/minimatch": {
- "version": "5.1.6",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
+ "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
- "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
- "node": ">=10"
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/app-root-dir": {
@@ -19432,13 +19132,14 @@
},
"node_modules/aproba": {
"version": "1.2.0",
- "license": "ISC",
- "optional": true
+ "devOptional": true,
+ "license": "ISC"
},
"node_modules/archiver": {
"version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
+ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^2.1.0",
@@ -19455,8 +19156,9 @@
},
"node_modules/archiver-utils": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.1.4",
@@ -19476,8 +19178,9 @@
},
"node_modules/archiver/node_modules/readable-stream": {
"version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
@@ -19749,8 +19452,9 @@
},
"node_modules/assert-plus": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"dev": true,
- "license": "MIT",
"optional": true,
"engines": {
"node": ">=0.8"
@@ -19793,8 +19497,9 @@
},
"node_modules/astral-regex": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true,
- "license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
@@ -19814,8 +19519,9 @@
},
"node_modules/async-exit-hook": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
+ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -20375,9 +20081,9 @@
}
},
"node_modules/babel-plugin-react-compiler": {
- "version": "0.0.0-experimental-c23de8d-20240515",
- "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-c23de8d-20240515.tgz",
- "integrity": "sha512-0XN2gmpT55QtAz5n7d5g91y1AuO9tRhWBaLgCRyc4ExHrlr7+LfxW+YTb3mOwxngkkiggwM8HyYsaEK9MqhnlQ==",
+ "version": "0.0.0-experimental-696af53-20240625",
+ "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-696af53-20240625.tgz",
+ "integrity": "sha512-OUDKms8qmcm5bX0D+sJWC1YcKcd7AZ2aJ7eY6gkR+Xr7PDfkXLbqAld4Qs9B0ntjVbUMEtW/PjlQrxDtY4raHg==",
"dev": true,
"dependencies": {
"@babel/generator": "7.2.0",
@@ -20956,13 +20662,15 @@
},
"node_modules/bluebird": {
"version": "3.7.2",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true
},
"node_modules/bluebird-lst": {
"version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz",
+ "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"bluebird": "^3.5.5"
}
@@ -21282,17 +20990,6 @@
"node": "*"
}
},
- "node_modules/buffer-equal": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/buffer-fill": {
"version": "1.0.0",
"license": "MIT"
@@ -21306,15 +21003,16 @@
"license": "MIT"
},
"node_modules/builder-util": {
- "version": "24.13.1",
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.0.0.tgz",
+ "integrity": "sha512-cI8zIsipo/gciZ5jGEA1qYL5Em1N6cWoNMpeJWZAfOs3H9s5zQWKnAS7rTdlJpsJ88gEmL5/32yeXUF2Uzxw6w==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/debug": "^4.1.6",
"7zip-bin": "~5.2.0",
- "app-builder-bin": "4.0.0",
+ "app-builder-bin": "v5.0.0-alpha.4",
"bluebird-lst": "^1.0.9",
- "builder-util-runtime": "9.2.4",
+ "builder-util-runtime": "9.2.5",
"chalk": "^4.1.2",
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
@@ -21329,9 +21027,10 @@
}
},
"node_modules/builder-util-runtime": {
- "version": "9.2.4",
+ "version": "9.2.5",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz",
+ "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
@@ -21342,8 +21041,9 @@
},
"node_modules/builder-util/node_modules/ansi-styles": {
"version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -21356,13 +21056,15 @@
},
"node_modules/builder-util/node_modules/argparse": {
"version": "2.0.1",
- "dev": true,
- "license": "Python-2.0"
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
},
"node_modules/builder-util/node_modules/chalk": {
"version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -21376,8 +21078,9 @@
},
"node_modules/builder-util/node_modules/color-convert": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
@@ -21387,13 +21090,15 @@
},
"node_modules/builder-util/node_modules/color-name": {
"version": "1.1.4",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"node_modules/builder-util/node_modules/fs-extra": {
"version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -21405,16 +21110,18 @@
},
"node_modules/builder-util/node_modules/has-flag": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/builder-util/node_modules/js-yaml": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -21424,8 +21131,9 @@
},
"node_modules/builder-util/node_modules/supports-color": {
"version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -21818,8 +21526,9 @@
},
"node_modules/chromium-pickle-js": {
"version": "0.2.0",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
+ "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==",
+ "dev": true
},
"node_modules/ci-info": {
"version": "3.8.0",
@@ -22025,8 +21734,9 @@
},
"node_modules/cli-truncate": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"slice-ansi": "^3.0.0",
@@ -22179,8 +21889,8 @@
},
"node_modules/color-support": {
"version": "1.1.3",
+ "devOptional": true,
"license": "ISC",
- "optional": true,
"bin": {
"color-support": "bin.js"
}
@@ -22245,8 +21955,9 @@
},
"node_modules/compare-version": {
"version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
+ "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -22277,8 +21988,9 @@
},
"node_modules/compress-commons": {
"version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
+ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
@@ -22292,8 +22004,9 @@
},
"node_modules/compress-commons/node_modules/readable-stream": {
"version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
@@ -22458,47 +22171,64 @@
}
},
"node_modules/config-file-ts": {
- "version": "0.2.6",
+ "version": "0.2.8-rc1",
+ "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz",
+ "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "glob": "^10.3.10",
- "typescript": "^5.3.3"
+ "glob": "^10.3.12",
+ "typescript": "^5.4.3"
}
},
"node_modules/config-file-ts/node_modules/brace-expansion": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/config-file-ts/node_modules/glob": {
- "version": "10.3.10",
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
- "license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
- "jackspeak": "^2.3.5",
- "minimatch": "^9.0.1",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
- "path-scurry": "^1.10.1"
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
- "engines": {
- "node": ">=16 || 14 >=14.17"
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/config-file-ts/node_modules/minimatch": {
- "version": "9.0.3",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
- "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -22510,9 +22240,10 @@
}
},
"node_modules/config-file-ts/node_modules/minipass": {
- "version": "7.0.4",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
- "license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -22601,8 +22332,8 @@
},
"node_modules/console-control-strings": {
"version": "1.1.0",
- "license": "ISC",
- "optional": true
+ "devOptional": true,
+ "license": "ISC"
},
"node_modules/constants-browserify": {
"version": "1.0.0",
@@ -22820,8 +22551,9 @@
},
"node_modules/crc": {
"version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
+ "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"buffer": "^5.1.0"
@@ -22829,8 +22561,9 @@
},
"node_modules/crc-32": {
"version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true,
- "license": "Apache-2.0",
"peer": true,
"bin": {
"crc32": "bin/crc32.njs"
@@ -22841,8 +22574,9 @@
},
"node_modules/crc32-stream": {
"version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
+ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"crc-32": "^1.2.0",
@@ -22854,8 +22588,9 @@
},
"node_modules/crc32-stream/node_modules/readable-stream": {
"version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
@@ -23595,8 +23330,8 @@
},
"node_modules/delegates": {
"version": "1.0.0",
- "license": "MIT",
- "optional": true
+ "devOptional": true,
+ "license": "MIT"
},
"node_modules/denodeify": {
"version": "1.2.1",
@@ -23657,8 +23392,8 @@
},
"node_modules/detect-libc": {
"version": "2.0.1",
+ "devOptional": true,
"license": "Apache-2.0",
- "optional": true,
"engines": {
"node": ">=8"
}
@@ -23751,12 +23486,13 @@
"license": "MIT"
},
"node_modules/dir-compare": {
- "version": "3.3.0",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
+ "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "buffer-equal": "^1.0.0",
- "minimatch": "^3.0.4"
+ "minimatch": "^3.0.5",
+ "p-limit": "^3.1.0 "
}
},
"node_modules/dir-glob": {
@@ -23770,13 +23506,14 @@
}
},
"node_modules/dmg-builder": {
- "version": "24.13.2",
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.0.0.tgz",
+ "integrity": "sha512-kXETWCy/JIXS8PHYc8Y0EdSWO02gpf4jleW74hkIp6o9WWTjAdBRw2fAcRBNIEBUJtVHFrgCYsEWh0wKFUB0+A==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "app-builder-lib": "24.13.2",
- "builder-util": "24.13.1",
- "builder-util-runtime": "9.2.4",
+ "app-builder-lib": "25.0.0",
+ "builder-util": "25.0.0",
+ "builder-util-runtime": "9.2.5",
"fs-extra": "^10.1.0",
"iconv-lite": "^0.6.2",
"js-yaml": "^4.1.0"
@@ -23787,13 +23524,15 @@
},
"node_modules/dmg-builder/node_modules/argparse": {
"version": "2.0.1",
- "dev": true,
- "license": "Python-2.0"
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
},
"node_modules/dmg-builder/node_modules/fs-extra": {
"version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -23805,8 +23544,9 @@
},
"node_modules/dmg-builder/node_modules/js-yaml": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -23816,8 +23556,9 @@
},
"node_modules/dmg-license": {
"version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
+ "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -23841,8 +23582,9 @@
},
"node_modules/dmg-license/node_modules/ajv": {
"version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -23857,8 +23599,9 @@
},
"node_modules/dmg-license/node_modules/json-schema-traverse": {
"version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
- "license": "MIT",
"optional": true
},
"node_modules/dns-packet": {
@@ -23973,20 +23716,31 @@
}
},
"node_modules/dotenv": {
- "version": "16.3.1",
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"dev": true,
- "license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
- "url": "https://github.com/motdotla/dotenv?sponsor=1"
+ "url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand": {
- "version": "5.1.0",
+ "version": "11.0.6",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz",
+ "integrity": "sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==",
"dev": true,
- "license": "BSD-2-Clause"
+ "dependencies": {
+ "dotenv": "^16.4.4"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
},
"node_modules/duplexer": {
"version": "0.1.2",
@@ -24054,19 +23808,20 @@
}
},
"node_modules/electron-builder": {
- "version": "24.13.2",
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.0.0.tgz",
+ "integrity": "sha512-3nEqF6KnoM206mLz1C70VXWCzXmH2boL82wkpgLB1GXgK3dly6ay/cepI+2BmQT4iWkIHeG8qH9bPjPj0hn+1A==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "app-builder-lib": "24.13.2",
- "builder-util": "24.13.1",
- "builder-util-runtime": "9.2.4",
+ "app-builder-lib": "25.0.0",
+ "builder-util": "25.0.0",
+ "builder-util-runtime": "9.2.5",
"chalk": "^4.1.2",
- "dmg-builder": "24.13.2",
+ "dmg-builder": "25.0.0",
"fs-extra": "^10.1.0",
"is-ci": "^3.0.0",
"lazy-val": "^1.0.5",
- "read-config-file": "6.3.2",
+ "read-config-file": "6.4.0",
"simple-update-notifier": "2.0.0",
"yargs": "^17.6.2"
},
@@ -24079,21 +23834,23 @@
}
},
"node_modules/electron-builder-squirrel-windows": {
- "version": "24.13.2",
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.0.0.tgz",
+ "integrity": "sha512-bfARwAdye1UkFQZ7NedHZBcOek2lvDDeg/pCaXT4Nrki7gdwrvVY/Be/QJm7Smc6IR/mviozbL9ykUHQ/FSsbw==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
- "app-builder-lib": "24.13.2",
+ "app-builder-lib": "25.0.0",
"archiver": "^5.3.1",
- "builder-util": "24.13.1",
+ "builder-util": "25.0.0",
"fs-extra": "^10.1.0"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
"version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
@@ -24182,13 +23939,14 @@
}
},
"node_modules/electron-publish": {
- "version": "24.13.1",
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.0.0.tgz",
+ "integrity": "sha512-8wq3pVLq9bpd/jNKJGIXbeL8B8AovLojtCDkVSuSgrLtxEndqy5JfuadUKPAgbmh1zjholNAHsfHH9FS5yeYAg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/fs-extra": "^9.0.11",
- "builder-util": "24.13.1",
- "builder-util-runtime": "9.2.4",
+ "builder-util": "25.0.0",
+ "builder-util-runtime": "9.2.5",
"chalk": "^4.1.2",
"fs-extra": "^10.1.0",
"lazy-val": "^1.0.5",
@@ -24197,8 +23955,9 @@
},
"node_modules/electron-publish/node_modules/ansi-styles": {
"version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -24211,8 +23970,9 @@
},
"node_modules/electron-publish/node_modules/chalk": {
"version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -24226,8 +23986,9 @@
},
"node_modules/electron-publish/node_modules/color-convert": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
@@ -24237,13 +23998,15 @@
},
"node_modules/electron-publish/node_modules/color-name": {
"version": "1.1.4",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"node_modules/electron-publish/node_modules/fs-extra": {
"version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -24255,16 +24018,18 @@
},
"node_modules/electron-publish/node_modules/has-flag": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/electron-publish/node_modules/supports-color": {
"version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -24327,6 +24092,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
"node_modules/end-of-stream": {
"version": "1.4.4",
"license": "MIT",
@@ -25341,9 +25115,9 @@
}
},
"node_modules/eslint-plugin-react-compiler": {
- "version": "0.0.0-experimental-53bb89e-20240515",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-53bb89e-20240515.tgz",
- "integrity": "sha512-L3HV9qja1dnClRlR9aaWEJeJoGPH9cgjKq0sYqIOOH9uyWdVMH9CudsFr6yLva7dj05FpLZkiIaRSZJ3P/v6yQ==",
+ "version": "0.0.0-experimental-0998c1e-20240625",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-0998c1e-20240625.tgz",
+ "integrity": "sha512-npq2RomExoQI3jETs4OrifaygyJYgOcX/q74Q9OC7GmffLh5zSJaQpzjs2fi61NMNkJyIvTBD0C6sKTGGcetOw==",
"dev": true,
"dependencies": {
"@babel/core": "^7.24.4",
@@ -26017,9 +25791,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.49",
+ "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.49.tgz",
+ "integrity": "sha512-67QbRuR2XEl2RoNLSbyqGWATIbOXPV42azAfs2sqNT6iyWKcOgHUqRkWPhxA0GmSW35lwq66bvgPVsQUfMGCow==",
"dependencies": {
"awesome-phonenumber": "^5.4.0",
"classnames": "2.5.0",
@@ -26031,9 +25805,9 @@
"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"
+ "ua-parser-js": "^1.0.38"
}
},
"node_modules/expensify-common/node_modules/react": {
@@ -26070,9 +25844,9 @@
}
},
"node_modules/expensify-common/node_modules/ua-parser-js": {
- "version": "1.0.37",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
- "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
+ "version": "1.0.38",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz",
+ "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==",
"funding": [
{
"type": "opencollective",
@@ -26395,6 +26169,12 @@
"node": ">=8"
}
},
+ "node_modules/exponential-backoff": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz",
+ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==",
+ "dev": true
+ },
"node_modules/express": {
"version": "4.18.1",
"license": "MIT",
@@ -26558,11 +26338,12 @@
},
"node_modules/extsprintf": {
"version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
+ "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
"dev": true,
"engines": [
"node >=0.6.0"
],
- "license": "MIT",
"optional": true
},
"node_modules/fast-deep-equal": {
@@ -27839,8 +27620,8 @@
},
"node_modules/has-unicode": {
"version": "2.0.1",
- "license": "ISC",
- "optional": true
+ "devOptional": true,
+ "license": "ISC"
},
"node_modules/has-value": {
"version": "1.0.0",
@@ -28066,8 +27847,9 @@
},
"node_modules/hosted-git-info": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
"dev": true,
- "license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -28611,8 +28393,9 @@
},
"node_modules/iconv-corefoundation": {
"version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
+ "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -28981,6 +28764,25 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ip-address": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+ "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "dev": true,
+ "dependencies": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ip-address/node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "dev": true
+ },
"node_modules/ip-regex": {
"version": "2.1.0",
"license": "MIT",
@@ -29128,8 +28930,9 @@
},
"node_modules/is-ci": {
"version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+ "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"ci-info": "^3.2.0"
},
@@ -29364,6 +29167,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-lambda": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+ "dev": true
+ },
"node_modules/is-map": {
"version": "2.0.2",
"dev": true,
@@ -29640,8 +29449,9 @@
},
"node_modules/isbinaryfile": {
"version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz",
+ "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 18.0.0"
},
@@ -29938,28 +29748,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-circus/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-circus/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-circus/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -30050,28 +29838,6 @@
}
}
},
- "node_modules/jest-cli/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-cli/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-cli/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -30173,28 +29939,6 @@
}
}
},
- "node_modules/jest-config/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-config/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-config/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -30348,28 +30092,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-each/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-each/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-each/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -30453,28 +30175,6 @@
}
}
},
- "node_modules/jest-environment-jsdom/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-environment-jsdom/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-environment-jsdom/node_modules/acorn": {
"version": "8.11.3",
"license": "MIT",
@@ -30485,47 +30185,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/jest-environment-jsdom/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/jest-environment-jsdom/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/jest-environment-jsdom/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/jest-environment-jsdom/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
"node_modules/jest-environment-jsdom/node_modules/cssstyle": {
"version": "2.3.0",
"license": "MIT",
@@ -30574,13 +30233,6 @@
"node": ">= 6"
}
},
- "node_modules/jest-environment-jsdom/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"license": "MIT",
@@ -30644,16 +30296,6 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/jest-environment-jsdom/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/jest-environment-jsdom/node_modules/tr46": {
"version": "3.0.0",
"license": "MIT",
@@ -30724,86 +30366,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-environment-node/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-environment-node/node_modules/@types/yargs": {
- "version": "17.0.31",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/jest-environment-node/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/jest-environment-node/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/jest-environment-node/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/jest-environment-node/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
- "node_modules/jest-environment-node/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/jest-environment-node/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/jest-expo": {
"version": "50.0.1",
"license": "MIT",
@@ -30871,69 +30433,6 @@
"fsevents": "^2.3.2"
}
},
- "node_modules/jest-haste-map/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-haste-map/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/jest-haste-map/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/jest-haste-map/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/jest-haste-map/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/jest-haste-map/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
"node_modules/jest-haste-map/node_modules/has-flag": {
"version": "4.0.0",
"license": "MIT",
@@ -30967,16 +30466,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
- "node_modules/jest-haste-map/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/jest-leak-detector": {
"version": "29.4.1",
"license": "MIT",
@@ -31077,28 +30566,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-message-util/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-message-util/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-message-util/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -31169,86 +30636,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-mock/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-mock/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/jest-mock/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/jest-mock/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/jest-mock/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/jest-mock/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
- "node_modules/jest-mock/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/jest-mock/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/jest-pnp-resolver": {
"version": "1.2.3",
"license": "MIT",
@@ -31388,28 +30775,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-runner/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-runner/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-runner/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -31534,28 +30899,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-runtime/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-runtime/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-runtime/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -31643,28 +30986,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-snapshot/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-snapshot/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-snapshot/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -31747,28 +31068,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-util/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-util/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-util/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -31842,28 +31141,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-validate/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-validate/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-validate/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -32157,28 +31434,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-watcher/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-watcher/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
"node_modules/jest-watcher/node_modules/ansi-styles": {
"version": "4.3.0",
"license": "MIT",
@@ -32279,86 +31534,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
- "node_modules/jest/node_modules/@jest/types": {
- "version": "29.6.3",
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest/node_modules/@types/yargs": {
- "version": "17.0.24",
- "license": "MIT",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/jest/node_modules/ansi-styles": {
- "version": "4.3.0",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/jest/node_modules/chalk": {
- "version": "4.1.2",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/jest/node_modules/color-convert": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/jest/node_modules/color-name": {
- "version": "1.1.4",
- "license": "MIT"
- },
- "node_modules/jest/node_modules/has-flag": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/jest/node_modules/supports-color": {
- "version": "7.2.0",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/jimp-compact": {
"version": "0.16.1",
"license": "MIT"
@@ -32410,6 +31585,12 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
+ "dev": true
+ },
"node_modules/jsc-android": {
"version": "250231.0.0",
"license": "BSD-2-Clause"
@@ -32758,13 +31939,15 @@
},
"node_modules/lazy-val": {
"version": "1.0.5",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
+ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
+ "dev": true
},
"node_modules/lazystream": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"readable-stream": "^2.0.5"
@@ -32983,20 +32166,23 @@
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true,
- "license": "MIT",
"peer": true
},
"node_modules/lodash.difference": {
"version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true,
- "license": "MIT",
"peer": true
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true,
- "license": "MIT",
"peer": true
},
"node_modules/lodash.isequal": {
@@ -33005,8 +32191,9 @@
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
- "license": "MIT",
"peer": true
},
"node_modules/lodash.memoize": {
@@ -33026,8 +32213,9 @@
},
"node_modules/lodash.union": {
"version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true,
- "license": "MIT",
"peer": true
},
"node_modules/log-symbols": {
@@ -33395,6 +32583,175 @@
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
}
},
+ "node_modules/make-fetch-happen": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
+ "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==",
+ "dev": true,
+ "dependencies": {
+ "agentkeepalive": "^4.2.1",
+ "cacache": "^16.1.0",
+ "http-cache-semantics": "^4.1.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-lambda": "^1.0.1",
+ "lru-cache": "^7.7.1",
+ "minipass": "^3.1.6",
+ "minipass-collect": "^1.0.2",
+ "minipass-fetch": "^2.0.3",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.3",
+ "promise-retry": "^2.0.1",
+ "socks-proxy-agent": "^7.0.0",
+ "ssri": "^9.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/@npmcli/fs": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz",
+ "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==",
+ "dev": true,
+ "dependencies": {
+ "@gar/promisify": "^1.1.3",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/@npmcli/move-file": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz",
+ "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==",
+ "deprecated": "This functionality has been moved to @npmcli/fs",
+ "dev": true,
+ "dependencies": {
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/cacache": {
+ "version": "16.1.3",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz",
+ "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/fs": "^2.1.0",
+ "@npmcli/move-file": "^2.0.0",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.1.0",
+ "glob": "^8.0.1",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^7.7.1",
+ "minipass": "^3.1.6",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "mkdirp": "^1.0.4",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^9.0.0",
+ "tar": "^6.1.11",
+ "unique-filename": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/ssri": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz",
+ "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/unique-filename": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
+ "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==",
+ "dev": true,
+ "dependencies": {
+ "unique-slug": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/unique-slug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz",
+ "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
"node_modules/makeerror": {
"version": "1.0.12",
"license": "BSD-3-Clause",
@@ -34152,8 +33509,9 @@
}
},
"node_modules/metro/node_modules/ws": {
- "version": "7.5.9",
- "license": "MIT",
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"engines": {
"node": ">=8.3.0"
},
@@ -34291,6 +33649,23 @@
"node": ">= 8"
}
},
+ "node_modules/minipass-fetch": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz",
+ "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.1.6",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.1.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
"node_modules/minipass-flush": {
"version": "1.0.5",
"license": "ISC",
@@ -34311,6 +33686,18 @@
"node": ">=8"
}
},
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/minizlib": {
"version": "2.1.2",
"license": "MIT",
@@ -34567,16 +33954,38 @@
"node": ">= 10.13"
}
},
+ "node_modules/node-abi": {
+ "version": "3.65.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz",
+ "integrity": "sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/node-abort-controller": {
"version": "3.1.1",
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
+ "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
"dev": true,
- "license": "MIT",
"optional": true
},
+ "node_modules/node-api-version": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.0.tgz",
+ "integrity": "sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.3.5"
+ }
+ },
"node_modules/node-dir": {
"version": "0.1.17",
"license": "MIT",
@@ -34653,6 +34062,95 @@
"node": ">= 6.13.0"
}
},
+ "node_modules/node-gyp": {
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz",
+ "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==",
+ "dev": true,
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^10.0.3",
+ "nopt": "^6.0.0",
+ "npmlog": "^6.0.0",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^12.13 || ^14.13 || >=16"
+ }
+ },
+ "node_modules/node-gyp/node_modules/are-we-there-yet": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+ "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/gauge": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+ "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.3",
+ "console-control-strings": "^1.1.0",
+ "has-unicode": "^2.0.1",
+ "signal-exit": "^3.0.7",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/npmlog": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+ "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "dependencies": {
+ "are-we-there-yet": "^3.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^4.0.3",
+ "set-blocking": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/node-int64": {
"version": "0.4.0",
"license": "MIT"
@@ -34720,6 +34218,21 @@
"url": "https://github.com/sponsors/antelle"
}
},
+ "node_modules/nopt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
+ "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "^1.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
"node_modules/normalize-package-data": {
"version": "2.5.0",
"license": "BSD-2-Clause",
@@ -35974,6 +35487,16 @@
"path2d-polyfill": "^2.0.1"
}
},
+ "node_modules/pe-library": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.0.tgz",
+ "integrity": "sha512-JAmVv2jGxmczplhHO7UoFGJ+pM/yMBpny3vNjwNFuaeQfzKlekQidZ8Ss8EJ0qee8wEQN4lY2IwtWx2oRfMsag==",
+ "dev": true,
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
"node_modules/peek-stream": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
@@ -35985,6 +35508,32 @@
"through2": "^2.0.3"
}
},
+ "node_modules/peggy": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/peggy/-/peggy-4.0.3.tgz",
+ "integrity": "sha512-v7/Pt6kGYsfXsCrfb52q7/yg5jaAwiVaUMAPLPvy4DJJU6Wwr72t6nDIqIDkGfzd1B4zeVuTnQT0RGeOhe/uSA==",
+ "dev": true,
+ "dependencies": {
+ "@peggyjs/from-mem": "1.3.0",
+ "commander": "^12.1.0",
+ "source-map-generator": "0.8.0"
+ },
+ "bin": {
+ "peggy": "bin/peggy.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/peggy/node_modules/commander": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/pend": {
"version": "1.2.0",
"dev": true,
@@ -36117,14 +35666,24 @@
}
},
"node_modules/plist": {
- "version": "3.0.6",
- "license": "MIT",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
+ "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
"dependencies": {
+ "@xmldom/xmldom": "^0.8.8",
"base64-js": "^1.5.1",
"xmlbuilder": "^15.1.1"
},
"engines": {
- "node": ">=6"
+ "node": ">=10.4.0"
+ }
+ },
+ "node_modules/plist/node_modules/@xmldom/xmldom": {
+ "version": "0.8.10",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
+ "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
+ "engines": {
+ "node": ">=10.0.0"
}
},
"node_modules/plist/node_modules/xmlbuilder": {
@@ -36886,6 +36445,98 @@
"react-dom": ">=16.8.0"
}
},
+ "node_modules/react-compiler-healthcheck": {
+ "version": "0.0.0-experimental-b130d5f-20240625",
+ "resolved": "https://registry.npmjs.org/react-compiler-healthcheck/-/react-compiler-healthcheck-0.0.0-experimental-b130d5f-20240625.tgz",
+ "integrity": "sha512-vf3Ipg+f19yOYQeRP938e5jWNEpwR6EX5pwBZdJUF9rt11vJ3ckgUVcF5qGWUU/7DB0N9MH1koBqwqMYabrBiQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "chalk": "4",
+ "fast-glob": "^3.3.2",
+ "ora": "5.4.1",
+ "yargs": "^17.7.2",
+ "zod": "^3.22.4",
+ "zod-validation-error": "^3.0.3"
+ },
+ "bin": {
+ "react-compiler-healthcheck": "dist/index.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
+ }
+ },
+ "node_modules/react-compiler-healthcheck/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/react-compiler-healthcheck/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/react-compiler-healthcheck/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/react-compiler-healthcheck/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/react-compiler-healthcheck/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/react-compiler-healthcheck/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/react-compiler-runtime": {
"resolved": "lib/react-compiler-runtime",
"link": true
@@ -37022,9 +36673,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"
@@ -37405,9 +37056,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "2.0.54",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.54.tgz",
- "integrity": "sha512-cANbs0KuiwHAIUC0HY7DGNXbFMHH4ZWbTci+qhHhuNNf4aNIP0/ncJ4W8a3VCgFVtfobIFAX5ouT40dEcgBOIQ==",
+ "version": "2.0.56",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.56.tgz",
+ "integrity": "sha512-3rn1+J4tli9zPS9w5x6tOAUz01wVHkiTFgtHoIwjD7HdLUO/9nk6H8JX6Oqb9Vzq2XQOSavUFRepIHnGvzNtgg==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -37470,8 +37121,9 @@
}
},
"node_modules/react-native-permissions": {
- "version": "3.9.3",
- "license": "MIT",
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.10.1.tgz",
+ "integrity": "sha512-Gc5BxxpjZn4QNUDiVeHOO0vXh3AH7ToolmwTJozqC6DsxV7NAf3ttap+8BSmzDR8WxuAM3Cror+YNiBhHJx7/w==",
"peerDependencies": {
"react": ">=16.13.1",
"react-native": ">=0.63.3",
@@ -37496,17 +37148,17 @@
}
},
"node_modules/react-native-plaid-link-sdk": {
- "version": "11.5.0",
- "resolved": "https://registry.npmjs.org/react-native-plaid-link-sdk/-/react-native-plaid-link-sdk-11.5.0.tgz",
- "integrity": "sha512-B3fwujxBS9nZwadXFcseU3nrYG7Ptob6p9eG/gXde65cqwErMaq2k1rVv3R17s/rpKnmU5Cx5pKOMmkxPUn08w==",
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/react-native-plaid-link-sdk/-/react-native-plaid-link-sdk-11.11.0.tgz",
+ "integrity": "sha512-Kmimhr6iOwCtIzsW7gygz48TzaZsdjnpgstJ2PM1q+THulOnx+BnkFu8UpLIGGkVe19E4wkxOAYL8kJ8vefNSQ==",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-qrcode-svg": {
- "version": "6.2.0",
- "license": "MIT",
+ "version": "6.3.0",
+ "resolved": "git+ssh://git@github.com/Expensify/react-native-qrcode-svg.git#295f87d45c0f10d9b50838ad28fa70e47d054c3b",
"dependencies": {
"prop-types": "^15.8.0",
"qrcode": "^1.5.1"
@@ -37514,7 +37166,7 @@
"peerDependencies": {
"react": "*",
"react-native": ">=0.63.4",
- "react-native-svg": "^13.2.0"
+ "react-native-svg": ">=13.2.0"
}
},
"node_modules/react-native-quick-sqlite": {
@@ -37557,14 +37209,15 @@
"license": "MIT"
},
"node_modules/react-native-release-profiler": {
- "version": "0.1.6",
- "license": "MIT",
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/react-native-release-profiler/-/react-native-release-profiler-0.2.1.tgz",
+ "integrity": "sha512-gDOwEXypd4Gu++nlKyaVLHPfwrVkkdBrsjMrQORYTTDqcrD/OfuNZ8YK7p+u5LUNjnPD4WmBF88C5dEW7iM1lg==",
"workspaces": [
"example"
],
"dependencies": {
- "@react-native-community/cli": "^12.2.1",
- "commander": "^11.1.0"
+ "commander": "^11.1.0",
+ "hermes-profile-transformer": "^0.0.9"
},
"bin": {
"react-native-release-profiler": "lib/commonjs/cli.js"
@@ -37584,6 +37237,25 @@
"node": ">=16"
}
},
+ "node_modules/react-native-release-profiler/node_modules/hermes-profile-transformer": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/hermes-profile-transformer/-/hermes-profile-transformer-0.0.9.tgz",
+ "integrity": "sha512-JYPUE9zA+W/hpTIGBV+t2ODvntataLLMfntoEcpEpKFDwdR6+Quk9SwLnWX9y2A3ZII6N4T8w3TUBC2ejsEGBw==",
+ "dependencies": {
+ "source-map": "^0.7.3"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/react-native-release-profiler/node_modules/source-map": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/react-native-render-html": {
"version": "6.3.1",
"license": "BSD-2-Clause",
@@ -37634,9 +37306,9 @@
}
},
"node_modules/react-native-screens": {
- "version": "3.30.1",
- "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.30.1.tgz",
- "integrity": "sha512-/muEvjocCtFb+j5J3YmLvB25+f4rIU8hnnxgGTkXcAf2omPBY8uhPjJaaFUlvj64VEoEzJcRpugbXWsjfPPIFg==",
+ "version": "3.32.0",
+ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.32.0.tgz",
+ "integrity": "sha512-wybqZAHX7v8ipOXhh90CqGLkBHw5JYqKNRBX7R/b0c2WQisTOgu0M0yGwBMM6LyXRBT+4k3NTGHdDbpJVpq0yQ==",
"dependencies": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"
@@ -37661,11 +37333,13 @@
}
},
"node_modules/react-native-svg": {
- "version": "14.1.0",
- "license": "MIT",
+ "version": "15.4.0",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.4.0.tgz",
+ "integrity": "sha512-zkBEbme/Dba4yqreg/oI2P6/6LrLywWY7HhaSwpU7Pb5COpTd2fV6/ShsgZz8GRFFdidUPwWmx01FITUsjhkmw==",
"dependencies": {
"css-select": "^5.1.0",
- "css-tree": "^1.1.3"
+ "css-tree": "^1.1.3",
+ "warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
@@ -37937,8 +37611,9 @@
}
},
"node_modules/react-native/node_modules/ws": {
- "version": "6.2.2",
- "license": "MIT",
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
+ "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
"dependencies": {
"async-limiter": "~1.0.0"
}
@@ -38112,10 +37787,6 @@
"react": "^18.2.0"
}
},
- "node_modules/react-test-renderer/node_modules/react-is": {
- "version": "18.2.0",
- "license": "MIT"
- },
"node_modules/react-test-renderer/node_modules/scheduler": {
"version": "0.23.0",
"license": "MIT",
@@ -38544,6 +38215,18 @@
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/read-binary-file-arch": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
+ "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "bin": {
+ "read-binary-file-arch": "cli.js"
+ }
+ },
"node_modules/read-cmd-shim": {
"version": "4.0.0",
"license": "ISC",
@@ -38552,16 +38235,17 @@
}
},
"node_modules/read-config-file": {
- "version": "6.3.2",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.4.0.tgz",
+ "integrity": "sha512-uB5QOBeF84PT61GlV11OTV4jUGHAO3iDEOP6v9ygxhG6Bs9PLg7WsjNT6mtIX2G+x8lJTr4ZWNeG6LDTKkNf2Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "config-file-ts": "^0.2.4",
- "dotenv": "^9.0.2",
- "dotenv-expand": "^5.1.0",
+ "config-file-ts": "0.2.8-rc1",
+ "dotenv": "^16.4.5",
+ "dotenv-expand": "^11.0.6",
"js-yaml": "^4.1.0",
- "json5": "^2.2.0",
- "lazy-val": "^1.0.4"
+ "json5": "^2.2.3",
+ "lazy-val": "^1.0.5"
},
"engines": {
"node": ">=12.0.0"
@@ -38569,21 +38253,15 @@
},
"node_modules/read-config-file/node_modules/argparse": {
"version": "2.0.1",
- "dev": true,
- "license": "Python-2.0"
- },
- "node_modules/read-config-file/node_modules/dotenv": {
- "version": "9.0.2",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=10"
- }
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
},
"node_modules/read-config-file/node_modules/js-yaml": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -38754,8 +38432,9 @@
},
"node_modules/readdir-glob": {
"version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true,
- "license": "Apache-2.0",
"peer": true,
"dependencies": {
"minimatch": "^5.1.0"
@@ -38763,8 +38442,9 @@
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0"
@@ -38772,8 +38452,9 @@
},
"node_modules/readdir-glob/node_modules/minimatch": {
"version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
- "license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -39139,6 +38820,19 @@
"version": "1.0.0",
"license": "MIT"
},
+ "node_modules/resedit": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.0.tgz",
+ "integrity": "sha512-dbsZ0gk5opWPFlKMqvxCrLCuMZUVmsW3yTPT0tT4mYwo5fjQM8c4HMN9ZJt6dRDqDV/78m9SU4rv24PN4NiYaA==",
+ "dev": true,
+ "dependencies": {
+ "pe-library": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
"node_modules/reselect": {
"version": "4.1.7",
"dev": true,
@@ -39411,8 +39105,9 @@
},
"node_modules/sanitize-filename": {
"version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
+ "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
"dev": true,
- "license": "WTFPL OR ISC",
"dependencies": {
"truncate-utf8-bytes": "^1.0.0"
}
@@ -39508,11 +39203,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"
},
@@ -39954,8 +39647,9 @@
},
"node_modules/slice-ansi": {
"version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^4.0.0",
@@ -39968,8 +39662,9 @@
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"color-convert": "^2.0.1"
@@ -39983,8 +39678,9 @@
},
"node_modules/slice-ansi/node_modules/color-convert": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"color-name": "~1.1.4"
@@ -39995,8 +39691,9 @@
},
"node_modules/slice-ansi/node_modules/color-name": {
"version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
- "license": "MIT",
"optional": true
},
"node_modules/slugify": {
@@ -40008,9 +39705,9 @@
},
"node_modules/smart-buffer": {
"version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"dev": true,
- "license": "MIT",
- "optional": true,
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
@@ -40216,6 +39913,34 @@
"websocket-driver": "^0.7.4"
}
},
+ "node_modules/socks": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
+ "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
+ "dev": true,
+ "dependencies": {
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
+ "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.3",
+ "socks": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/sort-asc": {
"version": "0.2.0",
"license": "MIT",
@@ -40263,6 +39988,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-generator": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/source-map-generator/-/source-map-generator-0.8.0.tgz",
+ "integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.0.2",
"license": "BSD-3-Clause",
@@ -40493,8 +40227,9 @@
},
"node_modules/stat-mode": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
+ "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 6"
}
@@ -41235,8 +40970,9 @@
},
"node_modules/temp-file": {
"version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
+ "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"async-exit-hook": "^2.0.1",
"fs-extra": "^10.0.0"
@@ -41244,8 +40980,9 @@
},
"node_modules/temp-file/node_modules/fs-extra": {
"version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -41587,16 +41324,18 @@
},
"node_modules/tmp": {
"version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
+ "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=14.14"
}
},
"node_modules/tmp-promise": {
"version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
+ "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"tmp": "^0.2.0"
}
@@ -41737,8 +41476,9 @@
},
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+ "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
"dev": true,
- "license": "WTFPL",
"dependencies": {
"utf8-byte-length": "^1.0.1"
}
@@ -41968,9 +41708,10 @@
}
},
"node_modules/type-fest": {
- "version": "4.10.3",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz",
+ "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==",
"dev": true,
- "license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
@@ -42542,9 +42283,10 @@
"license": "MIT"
},
"node_modules/utf8-byte-length": {
- "version": "1.0.4",
- "dev": true,
- "license": "WTFPL"
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
+ "dev": true
},
"node_modules/util": {
"version": "0.11.1",
@@ -42630,8 +42372,9 @@
},
"node_modules/verror": {
"version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
+ "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"assert-plus": "^1.0.0",
@@ -43143,9 +42886,10 @@
}
},
"node_modules/webpack-bundle-analyzer/node_modules/ws": {
- "version": "7.5.9",
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8.3.0"
},
@@ -43768,8 +43512,8 @@
},
"node_modules/wide-align": {
"version": "1.1.5",
+ "devOptional": true,
"license": "ISC",
- "optional": true,
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
@@ -43907,8 +43651,9 @@
}
},
"node_modules/ws": {
- "version": "8.16.0",
- "license": "MIT",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
@@ -44056,8 +43801,9 @@
},
"node_modules/zip-stream": {
"version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
+ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^3.0.4",
@@ -44070,8 +43816,9 @@
},
"node_modules/zip-stream/node_modules/archiver-utils": {
"version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
+ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.2.3",
@@ -44091,8 +43838,10 @@
},
"node_modules/zip-stream/node_modules/glob": {
"version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
- "license": "ISC",
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -44111,8 +43860,9 @@
},
"node_modules/zip-stream/node_modules/readable-stream": {
"version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
- "license": "MIT",
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
diff --git a/package.json b/package.json
index 966feed06fb8..5efe9951de9f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.3-2",
+ "version": "9.0.10-2",
"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.",
@@ -60,13 +60,15 @@
"workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh",
"workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.ts",
"setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1",
- "e2e-test-runner-build": "ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/"
+ "e2e-test-runner-build": "ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/",
+ "react-compiler-healthcheck": "react-compiler-healthcheck --verbose",
+ "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy "
},
"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.105",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -79,12 +81,12 @@
"@fullstory/react-native": "^1.4.2",
"@gorhom/portal": "^1.0.14",
"@invertase/react-native-apple-authentication": "^2.2.2",
- "@kie/act-js": "^2.6.0",
+ "@kie/act-js": "^2.6.2",
"@kie/mock-github": "2.0.1",
"@onfido/react-native-sdk": "10.6.0",
"@react-native-camera-roll/camera-roll": "7.4.0",
"@react-native-clipboard/clipboard": "^1.13.2",
- "@react-native-community/geolocation": "3.2.1",
+ "@react-native-community/geolocation": "3.3.0",
"@react-native-community/netinfo": "11.2.1",
"@react-native-firebase/analytics": "^12.3.0",
"@react-native-firebase/app": "^12.3.0",
@@ -108,7 +110,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.49",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -132,7 +134,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",
@@ -155,23 +157,23 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "2.0.54",
+ "react-native-onyx": "2.0.56",
"react-native-pager-view": "6.2.3",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
- "react-native-permissions": "^3.9.3",
+ "react-native-permissions": "^3.10.0",
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf",
- "react-native-plaid-link-sdk": "11.5.0",
- "react-native-qrcode-svg": "^6.2.0",
+ "react-native-plaid-link-sdk": "11.11.0",
+ "react-native-qrcode-svg": "git+https://github.com/Expensify/react-native-qrcode-svg",
"react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316",
"react-native-reanimated": "^3.8.0",
- "react-native-release-profiler": "^0.1.6",
+ "react-native-release-profiler": "^0.2.1",
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.8.2",
- "react-native-screens": "3.30.1",
+ "react-native-screens": "3.32.0",
"react-native-share": "^10.0.2",
"react-native-sound": "^0.11.2",
- "react-native-svg": "14.1.0",
+ "react-native-svg": "15.4.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "3.8.0",
@@ -254,7 +256,7 @@
"babel-jest": "29.4.1",
"babel-loader": "^9.1.3",
"babel-plugin-module-resolver": "^5.0.0",
- "babel-plugin-react-compiler": "^0.0.0-experimental-c23de8d-20240515",
+ "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625",
"babel-plugin-react-native-web": "^0.18.7",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-remove-console": "^6.9.4",
@@ -266,14 +268,14 @@
"diff-so-fancy": "^1.3.0",
"dotenv": "^16.0.3",
"electron": "^29.4.1",
- "electron-builder": "24.13.2",
+ "electron-builder": "25.0.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-expensify": "^2.0.52",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jsdoc": "^46.2.6",
- "eslint-plugin-react-compiler": "^0.0.0-experimental-53bb89e-20240515",
+ "eslint-plugin-react-compiler": "0.0.0-experimental-0998c1e-20240625",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^6.2.2",
@@ -288,9 +290,11 @@
"onchange": "^7.1.0",
"openai": "^4.47.2",
"patch-package": "^8.0.0",
+ "peggy": "^4.0.3",
"portfinder": "^1.0.28",
"prettier": "^2.8.8",
"pusher-js-mock": "^0.3.3",
+ "react-compiler-healthcheck": "^0.0.0-experimental-b130d5f-20240625",
"react-is": "^18.3.1",
"react-native-clean-project": "^4.0.0-alpha4.0",
"react-test-renderer": "18.2.0",
@@ -303,7 +307,7 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
- "type-fest": "^4.10.2",
+ "type-fest": "4.20.0",
"typescript": "^5.4.5",
"wait-port": "^0.2.9",
"webpack": "^5.76.0",
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/@perf-profiler+android+0.12.1.patch b/patches/@perf-profiler+android+0.12.1.patch
deleted file mode 100644
index e6e4a90d6ab4..000000000000
--- a/patches/@perf-profiler+android+0.12.1.patch
+++ /dev/null
@@ -1,26 +0,0 @@
-diff --git a/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js b/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js
-index 59aeed9..ee1d8a6 100644
---- a/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js
-+++ b/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js
-@@ -28,7 +28,7 @@ exports.CppProfilerName = `BAMPerfProfiler`;
- // into the Flipper plugin directory
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error
--const binaryFolder = global.Flipper
-+const binaryFolder = (global.Flipper || process.env.AWS)
- ? `${__dirname}/bin`
- : `${__dirname}/../../..${__dirname.includes("dist") ? "/.." : ""}/cpp-profiler/bin`;
- class UnixProfiler {
-diff --git a/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts b/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts
-index ccacf09..1eea659 100644
---- a/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts
-+++ b/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts
-@@ -26,7 +26,7 @@ export const CppProfilerName = `BAMPerfProfiler`;
- // into the Flipper plugin directory
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error
--const binaryFolder = global.Flipper
-+const binaryFolder = (global.Flipper || process.env.AWS)
- ? `${__dirname}/bin`
- : `${__dirname}/../../..${__dirname.includes("dist") ? "/.." : ""}/cpp-profiler/bin`;
-
diff --git a/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch b/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch
index f8e171008e14..c679bdbf73b9 100644
--- a/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch
+++ b/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch
@@ -1,5 +1,5 @@
diff --git a/node_modules/@react-native-community/netinfo/android/build.gradle b/node_modules/@react-native-community/netinfo/android/build.gradle
-index 0d617ed..e93d64a 100644
+index 0d617ed..97439e6 100644
--- a/node_modules/@react-native-community/netinfo/android/build.gradle
+++ b/node_modules/@react-native-community/netinfo/android/build.gradle
@@ -3,9 +3,10 @@ buildscript {
@@ -105,7 +105,6 @@ index 0d617ed..e93d64a 100644
+ implementation 'com.facebook.react:react-native:+'
+ }
}
-\ No newline at end of file
diff --git a/node_modules/@react-native-community/netinfo/android/src/main/java/com/reactnativecommunity/netinfo/NetInfoModuleImpl.java b/node_modules/@react-native-community/netinfo/android/src/main/java/com/reactnativecommunity/netinfo/NetInfoModuleImpl.java
index 2c3280b..296bbfd 100644
--- a/node_modules/@react-native-community/netinfo/android/src/main/java/com/reactnativecommunity/netinfo/NetInfoModuleImpl.java
@@ -1609,10 +1608,10 @@ index 095dd3b..596ace1 100644
+{"version":3,"names":["NetInfoStateType","exports","NetInfoCellularGeneration"],"sources":["types.ts"],"sourcesContent":["/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @format\n */\n\nexport enum NetInfoStateType {\n unknown = 'unknown',\n none = 'none',\n cellular = 'cellular',\n wifi = 'wifi',\n bluetooth = 'bluetooth',\n ethernet = 'ethernet',\n wimax = 'wimax',\n vpn = 'vpn',\n other = 'other',\n}\n\nexport type NetInfoMethodType = 'HEAD' | 'GET';\n\nexport enum NetInfoCellularGeneration {\n '2g' = '2g',\n '3g' = '3g',\n '4g' = '4g',\n '5g' = '5g',\n}\n\nexport interface NetInfoConnectedDetails {\n isConnectionExpensive: boolean;\n}\n\ninterface NetInfoConnectedState<\n T extends NetInfoStateType,\n D extends Record = Record,\n> {\n type: T;\n isConnected: true;\n isInternetReachable: boolean | null;\n details: D & NetInfoConnectedDetails;\n isWifiEnabled?: boolean;\n}\n\ninterface NetInfoDisconnectedState {\n type: T;\n isConnected: false;\n isInternetReachable: false;\n details: null;\n isWifiEnabled?: boolean;\n}\n\nexport interface NetInfoUnknownState {\n type: NetInfoStateType.unknown;\n isConnected: boolean | null;\n isInternetReachable: null;\n details: null;\n isWifiEnabled?: boolean;\n}\n\nexport type NetInfoNoConnectionState =\n NetInfoDisconnectedState;\nexport type NetInfoDisconnectedStates =\n | NetInfoUnknownState\n | NetInfoNoConnectionState;\n\nexport type NetInfoCellularState = NetInfoConnectedState<\n NetInfoStateType.cellular,\n {\n cellularGeneration: NetInfoCellularGeneration | null;\n carrier: string | null;\n }\n>;\nexport type NetInfoWifiState = NetInfoConnectedState<\n NetInfoStateType.wifi,\n {\n ssid: string | null;\n bssid: string | null;\n strength: number | null;\n ipAddress: string | null;\n subnet: string | null;\n frequency: number | null;\n linkSpeed: number | null;\n rxLinkSpeed: number | null;\n txLinkSpeed: number | null;\n }\n>;\nexport type NetInfoBluetoothState =\n NetInfoConnectedState;\nexport type NetInfoEthernetState = NetInfoConnectedState<\n NetInfoStateType.ethernet,\n {\n ipAddress: string | null;\n subnet: string | null;\n }\n>;\nexport type NetInfoWimaxState = NetInfoConnectedState;\nexport type NetInfoVpnState = NetInfoConnectedState;\nexport type NetInfoOtherState = NetInfoConnectedState;\nexport type NetInfoConnectedStates =\n | NetInfoCellularState\n | NetInfoWifiState\n | NetInfoBluetoothState\n | NetInfoEthernetState\n | NetInfoWimaxState\n | NetInfoVpnState\n | NetInfoOtherState;\n\nexport type NetInfoState = NetInfoDisconnectedStates | NetInfoConnectedStates;\n\nexport type NetInfoChangeHandler = (state: NetInfoState) => void;\nexport type NetInfoSubscription = () => void;\n\nexport interface NetInfoConfiguration {\n reachabilityUrl: string;\n reachabilityMethod?: NetInfoMethodType;\n reachabilityHeaders?: Record;\n reachabilityTest: (response: Response) => Promise;\n reachabilityLongTimeout: number;\n reachabilityShortTimeout: number;\n reachabilityRequestTimeout: number;\n reachabilityShouldRun: () => boolean;\n shouldFetchWiFiSSID: boolean;\n useNativeReachability: boolean;\n}\n"],"mappings":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAPA,IASYA,gBAAgB,GAAAC,OAAA,CAAAD,gBAAA,0BAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAA,OAAhBA,gBAAgB;AAAA;AAAA,IAchBE,yBAAyB,GAAAD,OAAA,CAAAC,yBAAA,0BAAzBA,yBAAyB;EAAzBA,yBAAyB;EAAzBA,yBAAyB;EAAzBA,yBAAyB;EAAzBA,yBAAyB;EAAA,OAAzBA,yBAAyB;AAAA"}
\ No newline at end of file
diff --git a/node_modules/@react-native-community/netinfo/lib/module/index.js b/node_modules/@react-native-community/netinfo/lib/module/index.js
-index 147c72e..02aa0db 100644
+index 147c72e..5de4e7c 100644
--- a/node_modules/@react-native-community/netinfo/lib/module/index.js
+++ b/node_modules/@react-native-community/netinfo/lib/module/index.js
-@@ -6,20 +6,23 @@
+@@ -6,20 +6,26 @@
*
* @format
*/
@@ -1635,11 +1634,14 @@ index 147c72e..02aa0db 100644
const createState = () => {
return new State(_configuration);
};
++
++// Track ongoing requests
++let isRequestInProgress = false;
+
/**
* Configures the library with the given configuration. Note that calling this will stop all
* previously added listeners from being called again. It is best to call this right when your
-@@ -27,23 +30,20 @@ const createState = () => {
+@@ -27,23 +33,20 @@ const createState = () => {
*
* @param configuration The new configuration to set.
*/
@@ -1666,7 +1668,7 @@ index 147c72e..02aa0db 100644
/**
* Returns a `Promise` that resolves to a `NetInfoState` object.
* This function operates on the global singleton instance configured using `configure()`
-@@ -52,27 +52,25 @@ export function configure(configuration) {
+@@ -52,27 +55,33 @@ export function configure(configuration) {
*
* @returns A Promise which contains the current connection state.
*/
@@ -1689,14 +1691,22 @@ index 147c72e..02aa0db 100644
if (!_state) {
_state = createState();
}
--
- return _state._fetchCurrentState();
+
+- return _state._fetchCurrentState();
++ if (isRequestInProgress) {
++ return _state.latest(); // Return the latest state if a request is already in progress
++ }
++
++ isRequestInProgress = true;
++ return _state._fetchCurrentState().finally(() => {
++ isRequestInProgress = false;
++ });
}
+
/**
* Subscribe to the global singleton's connection information. The callback is called with a parameter of type
* [`NetInfoState`](README.md#netinfostate) whenever the connection state changes. Your listener
-@@ -84,18 +82,16 @@ export function refresh() {
+@@ -84,18 +93,16 @@ export function refresh() {
*
* @returns A function which can be called to unsubscribe.
*/
@@ -1716,7 +1726,7 @@ index 147c72e..02aa0db 100644
/**
* A React Hook into this library's singleton which updates when the connection state changes.
*
-@@ -103,12 +99,10 @@ export function addEventListener(listener) {
+@@ -103,12 +110,10 @@ export function addEventListener(listener) {
*
* @returns The connection state.
*/
@@ -1729,7 +1739,7 @@ index 147c72e..02aa0db 100644
const [netInfo, setNetInfo] = useState({
type: Types.NetInfoStateType.unknown,
isConnected: null,
-@@ -120,6 +114,7 @@ export function useNetInfo(configuration) {
+@@ -120,6 +125,7 @@ export function useNetInfo(configuration) {
}, []);
return netInfo;
}
@@ -1737,7 +1747,7 @@ index 147c72e..02aa0db 100644
/**
* A React Hook which manages an isolated instance of the network info manager.
* This is not a hook into a singleton shared state. NetInfo.configure, NetInfo.addEventListener,
-@@ -129,7 +124,6 @@ export function useNetInfo(configuration) {
+@@ -129,7 +135,6 @@ export function useNetInfo(configuration) {
*
* @returns the netInfo state and a refresh function
*/
@@ -1745,7 +1755,7 @@ index 147c72e..02aa0db 100644
export function useNetInfoInstance(isPaused = false, configuration) {
const [networkInfoManager, setNetworkInfoManager] = useState();
const [netInfo, setNetInfo] = useState({
-@@ -142,8 +136,8 @@ export function useNetInfoInstance(isPaused = false, configuration) {
+@@ -142,8 +147,8 @@ export function useNetInfoInstance(isPaused = false, configuration) {
if (isPaused) {
return;
}
@@ -2610,22 +2620,13 @@ index 6982220..b515270 100644
};
export default _default;
diff --git a/node_modules/@react-native-community/netinfo/package.json b/node_modules/@react-native-community/netinfo/package.json
-index 3c80db2..61e6564 100644
+index 3c80db2..15d214d 100644
--- a/node_modules/@react-native-community/netinfo/package.json
+++ b/node_modules/@react-native-community/netinfo/package.json
-@@ -48,6 +48,7 @@
- "network info"
- ],
- "peerDependencies": {
-+ "react": "*",
- "react-native": ">=0.59"
+@@ -97,6 +97,14 @@
+ "webpack-cli": "^3.3.10",
+ "webpack-dev-server": "^3.11.3"
},
- "dependencies": {},
-@@ -121,5 +122,13 @@
- "yarn eslint --fix",
- "git add"
- ]
-+ },
+ "codegenConfig": {
+ "name": "RNCNetInfoSpec",
+ "type": "modules",
@@ -2633,8 +2634,10 @@ index 3c80db2..61e6564 100644
+ "android": {
+ "javaPackageName": "com.reactnativecommunity.netinfo"
+ }
- }
- }
++ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/react-native-netinfo/react-native-netinfo.git"
diff --git a/node_modules/@react-native-community/netinfo/react-native-netinfo.podspec b/node_modules/@react-native-community/netinfo/react-native-netinfo.podspec
index e34e728..9090eb1 100644
--- a/node_modules/@react-native-community/netinfo/react-native-netinfo.podspec
@@ -3062,4 +3065,4 @@ index 878f7ba..0000000
-
-#Files generated by the VS build
-**/Generated Files/**
--
+-
\ No newline at end of file
diff --git a/patches/@react-navigation+core+6.4.11+001+fix-react-strictmode.patch b/patches/@react-navigation+core+6.4.11+001+fix-react-strictmode.patch
index 8941bb380a79..f68cd6fe9ca4 100644
--- a/patches/@react-navigation+core+6.4.11+001+fix-react-strictmode.patch
+++ b/patches/@react-navigation+core+6.4.11+001+fix-react-strictmode.patch
@@ -42,3 +42,48 @@ index 051520b..6fb49e0 100644
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+diff --git a/node_modules/@react-navigation/core/src/useNavigationBuilder.tsx b/node_modules/@react-navigation/core/src/useNavigationBuilder.tsx
+index b1971ba..7d550e0 100644
+--- a/node_modules/@react-navigation/core/src/useNavigationBuilder.tsx
++++ b/node_modules/@react-navigation/core/src/useNavigationBuilder.tsx
+@@ -362,11 +362,6 @@ export default function useNavigationBuilder<
+
+ const stateCleanedUp = React.useRef(false);
+
+- const cleanUpState = React.useCallback(() => {
+- setCurrentState(undefined);
+- stateCleanedUp.current = true;
+- }, [setCurrentState]);
+-
+ const setState = React.useCallback(
+ (state: NavigationState | PartialState | undefined) => {
+ if (stateCleanedUp.current) {
+@@ -540,6 +535,9 @@ export default function useNavigationBuilder<
+ state = nextState;
+
+ React.useEffect(() => {
++ // In strict mode, React will double-invoke effects.
++ // So we need to reset the flag if component was not unmounted
++ stateCleanedUp.current = false;
+ setKey(navigatorKey);
+
+ if (!getIsInitial()) {
+@@ -551,14 +549,10 @@ export default function useNavigationBuilder<
+
+ return () => {
+ // We need to clean up state for this navigator on unmount
+- // We do it in a timeout because we need to detect if another navigator mounted in the meantime
+- // For example, if another navigator has started rendering, we should skip cleanup
+- // Otherwise, our cleanup step will cleanup state for the other navigator and re-initialize it
+- setTimeout(() => {
+- if (getCurrentState() !== undefined && getKey() === navigatorKey) {
+- cleanUpState();
+- }
+- }, 0);
++ if (getCurrentState() !== undefined && getKey() === navigatorKey) {
++ setCurrentState(undefined);
++ stateCleanedUp.current = true;
++ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
diff --git a/patches/eslint-plugin-react-compiler+0.0.0-experimental-53bb89e-20240515.patch b/patches/eslint-plugin-react-compiler+0.0.0-experimental-53bb89e-20240515.patch
deleted file mode 100644
index f81f70944dd2..000000000000
--- a/patches/eslint-plugin-react-compiler+0.0.0-experimental-53bb89e-20240515.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/node_modules/eslint-plugin-react-compiler/dist/index.js b/node_modules/eslint-plugin-react-compiler/dist/index.js
-index a0f47a7..f649250 100644
---- a/node_modules/eslint-plugin-react-compiler/dist/index.js
-+++ b/node_modules/eslint-plugin-react-compiler/dist/index.js
-@@ -69108,7 +69108,7 @@ const rule = {
- return false;
- }
- let babelAST;
-- if (context.filename.endsWith(".tsx") || context.filename.endsWith(".ts")) {
-+ if (filename.endsWith(".tsx") || filename.endsWith(".ts")) {
- try {
- const { parse: babelParse } = require("@babel/parser");
- babelAST = babelParse(sourceCode, {
diff --git a/patches/focus-trap+7.5.4.patch b/patches/focus-trap+7.5.4.patch
new file mode 100644
index 000000000000..c7b2aef2b51f
--- /dev/null
+++ b/patches/focus-trap+7.5.4.patch
@@ -0,0 +1,106 @@
+diff --git a/node_modules/focus-trap/dist/focus-trap.esm.js b/node_modules/focus-trap/dist/focus-trap.esm.js
+index 10d56db..a6d76d8 100644
+--- a/node_modules/focus-trap/dist/focus-trap.esm.js
++++ b/node_modules/focus-trap/dist/focus-trap.esm.js
+@@ -100,8 +100,8 @@ var isKeyForward = function isKeyForward(e) {
+ var isKeyBackward = function isKeyBackward(e) {
+ return isTabEvent(e) && e.shiftKey;
+ };
+-var delay = function delay(fn) {
+- return setTimeout(fn, 0);
++var delay = function delay(fn, delayTime = 0) {
++ return setTimeout(() => setTimeout(fn, delayTime), 0);
+ };
+
+ // Array.find/findIndex() are not supported on IE; this replicates enough
+@@ -283,7 +283,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
+ return node;
+ };
+ var getInitialFocusNode = function getInitialFocusNode() {
+- var node = getNodeForOption('initialFocus');
++ var node = getNodeForOption('initialFocus', state.containers);
+
+ // false explicitly indicates we want no initialFocus at all
+ if (node === false) {
+@@ -744,7 +744,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
+ // that caused the focus trap activation.
+ state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () {
+ tryFocus(getInitialFocusNode());
+- }) : tryFocus(getInitialFocusNode());
++ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined) : tryFocus(getInitialFocusNode());
+ doc.addEventListener('focusin', checkFocusIn, true);
+ doc.addEventListener('mousedown', checkPointerDown, {
+ capture: true,
+@@ -880,7 +880,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
+ tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
+ }
+ onPostDeactivate === null || onPostDeactivate === void 0 || onPostDeactivate();
+- });
++ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined);
+ };
+ if (returnFocus && checkCanReturnFocus) {
+ checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation);
+diff --git a/node_modules/focus-trap/index.d.ts b/node_modules/focus-trap/index.d.ts
+index 400db1b..69f4b94 100644
+--- a/node_modules/focus-trap/index.d.ts
++++ b/node_modules/focus-trap/index.d.ts
+@@ -16,7 +16,7 @@ declare module 'focus-trap' {
+ * `document.querySelector()` to find the DOM node), `false` to explicitly indicate
+ * an opt-out, or a function that returns a DOM node or `false`.
+ */
+- export type FocusTargetOrFalse = FocusTargetValueOrFalse | (() => FocusTargetValueOrFalse);
++ export type FocusTargetOrFalse = FocusTargetValueOrFalse | ((containers?: HTMLElement[]) => FocusTargetValueOrFalse | undefined);
+
+ type MouseEventToBoolean = (event: MouseEvent | TouchEvent) => boolean;
+ type KeyboardEventToBoolean = (event: KeyboardEvent) => boolean;
+@@ -185,7 +185,7 @@ declare module 'focus-trap' {
+ * This prevents elements within the focusable element from capturing
+ * the event that triggered the focus trap activation.
+ */
+- delayInitialFocus?: boolean;
++ delayInitialFocus?: boolean | number;
+ /**
+ * Default: `window.document`. Document where the focus trap will be active.
+ * This allows to use FocusTrap in an iFrame context.
+diff --git a/node_modules/focus-trap/index.js b/node_modules/focus-trap/index.js
+index de8e46a..bfc8b63 100644
+--- a/node_modules/focus-trap/index.js
++++ b/node_modules/focus-trap/index.js
+@@ -63,8 +63,8 @@ const isKeyBackward = function (e) {
+ return isTabEvent(e) && e.shiftKey;
+ };
+
+-const delay = function (fn) {
+- return setTimeout(fn, 0);
++const delay = function (fn, delayTime = 0) {
++ return setTimeout(() => setTimeout(fn, delayTime), 0);
+ };
+
+ // Array.find/findIndex() are not supported on IE; this replicates enough
+@@ -267,7 +267,7 @@ const createFocusTrap = function (elements, userOptions) {
+ };
+
+ const getInitialFocusNode = function () {
+- let node = getNodeForOption('initialFocus');
++ let node = getNodeForOption('initialFocus', state.containers);
+
+ // false explicitly indicates we want no initialFocus at all
+ if (node === false) {
+@@ -817,7 +817,7 @@ const createFocusTrap = function (elements, userOptions) {
+ state.delayInitialFocusTimer = config.delayInitialFocus
+ ? delay(function () {
+ tryFocus(getInitialFocusNode());
+- })
++ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined)
+ : tryFocus(getInitialFocusNode());
+
+ doc.addEventListener('focusin', checkFocusIn, true);
+@@ -989,7 +989,7 @@ const createFocusTrap = function (elements, userOptions) {
+ tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
+ }
+ onPostDeactivate?.();
+- });
++ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined);
+ };
+
+ if (returnFocus && checkCanReturnFocus) {
diff --git a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625.patch b/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625.patch
new file mode 100644
index 000000000000..d7c02701a636
--- /dev/null
+++ b/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625.patch
@@ -0,0 +1,90 @@
+diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js
+index b427385..4bf23db 100755
+--- a/node_modules/react-compiler-healthcheck/dist/index.js
++++ b/node_modules/react-compiler-healthcheck/dist/index.js
+@@ -69154,7 +69154,7 @@ var reactCompilerCheck = {
+ compile(source, path);
+ }
+ },
+- report() {
++ report(verbose) {
+ const totalComponents =
+ SucessfulCompilation.length +
+ countUniqueLocInEvents(OtherFailures) +
+@@ -69164,6 +69164,50 @@ var reactCompilerCheck = {
+ `Successfully compiled ${SucessfulCompilation.length} out of ${totalComponents} components.`
+ )
+ );
++
++ if (verbose) {
++ for (const compilation of [...SucessfulCompilation, ...ActionableFailures, ...OtherFailures]) {
++ const filename = compilation.fnLoc?.filename;
++
++ if (compilation.kind === "CompileSuccess") {
++ const name = compilation.fnName;
++ const isHook = name?.startsWith('use');
++
++ if (name) {
++ console.log(
++ chalk.green(
++ `Successfully compiled ${isHook ? "hook" : "component" } [${name}](${filename})`
++ )
++ );
++ } else {
++ console.log(chalk.green(`Successfully compiled ${compilation.fnLoc?.filename}`));
++ }
++ }
++
++ if (compilation.kind === "CompileError") {
++ const { reason, severity, loc } = compilation.detail;
++
++ const lnNo = loc.start?.line;
++ const colNo = loc.start?.column;
++
++ const isTodo = severity === ErrorSeverity.Todo;
++
++ console.log(
++ chalk[isTodo ? 'yellow' : 'red'](
++ `Failed to compile ${
++ filename
++ }${
++ lnNo !== undefined ? `:${lnNo}${
++ colNo !== undefined ? `:${colNo}` : ""
++ }.` : ""
++ }`
++ ),
++ chalk[isTodo ? 'yellow' : 'red'](reason? `Reason: ${reason}` : "")
++ );
++ console.log("\n");
++ }
++ }
++ }
+ },
+ };
+ const JsFileExtensionRE = /(js|ts|jsx|tsx)$/;
+@@ -69200,9 +69244,16 @@ function main() {
+ type: "string",
+ default: "**/+(*.{js,mjs,jsx,ts,tsx}|package.json)",
+ })
++ .option('verbose', {
++ description: 'run with verbose logging',
++ type: 'boolean',
++ default: false,
++ alias: 'v',
++ })
+ .parseSync();
+ const spinner = ora("Checking").start();
+ let src = argv.src;
++ let verbose = argv.verbose;
+ const globOptions = {
+ onlyFiles: true,
+ ignore: [
+@@ -69222,7 +69273,7 @@ function main() {
+ libraryCompatCheck.run(source, path);
+ }
+ spinner.stop();
+- reactCompilerCheck.report();
++ reactCompilerCheck.report(verbose);
+ strictModeCheck.report();
+ libraryCompatCheck.report();
+ });
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-plaid-link-sdk+11.5.0+001+initial.patch b/patches/react-native-plaid-link-sdk+11.5.0+001+initial.patch
deleted file mode 100644
index 6035477256b7..000000000000
--- a/patches/react-native-plaid-link-sdk+11.5.0+001+initial.patch
+++ /dev/null
@@ -1,4 +0,0 @@
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.m b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm
-similarity index 100%
-rename from node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.m
-rename to node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm
diff --git a/patches/react-native-plaid-link-sdk+11.5.0+002+turbomodule.patch b/patches/react-native-plaid-link-sdk+11.5.0+002+turbomodule.patch
deleted file mode 100644
index 7d5aab6c84cf..000000000000
--- a/patches/react-native-plaid-link-sdk+11.5.0+002+turbomodule.patch
+++ /dev/null
@@ -1,3287 +0,0 @@
-diff --git a/node_modules/react-native-plaid-link-sdk/README.md b/node_modules/react-native-plaid-link-sdk/README.md
-index 93ebca6..7bea608 100644
---- a/node_modules/react-native-plaid-link-sdk/README.md
-+++ b/node_modules/react-native-plaid-link-sdk/README.md
-@@ -49,7 +49,6 @@ cd ios && bundle install && bundle exec pod install
-
- AutoLinking should handle all of the Android setup.
-
--
- ### React Native Setup
-
- - To initialize `PlaidLink`, you will need to first create a `link_token` at [/link/token/create](https://plaid.com/docs/#create-link-token). Check out our [QuickStart guide](https://plaid.com/docs/quickstart/#introduction) for additional API information.
-@@ -58,7 +57,13 @@ AutoLinking should handle all of the Android setup.
-
- ```javascript
- import { Text } from 'react-native';
--import { PlaidLink, LinkSuccess, LinkExit, LinkLogLevel, LinkIOSPresentationStyle } from 'react-native-plaid-link-sdk';
-+import {
-+ PlaidLink,
-+ LinkSuccess,
-+ LinkExit,
-+ LinkLogLevel,
-+ LinkIOSPresentationStyle,
-+} from 'react-native-plaid-link-sdk';
-
- const MyPlaidComponent = () => {
- return (
-@@ -77,7 +82,7 @@ const MyPlaidComponent = () => {
- // UI is always presented in full screen on Android.
- iOSPresentationStyle={LinkIOSPresentationStyle.MODAL}
- >
-- Add Account
-+ Add Account
-
- );
- };
-@@ -92,6 +97,7 @@ const MyPlaidComponent = () => {
- ##### Android OAuth Requirements
-
- ###### Register your app package name
-+
- 1. Log into your [Plaid Dashboard](https://dashboard.plaid.com/developers/api) and navigate to the API page under the Developers tab.
- 2. Next to Allowed Android package names click "Configure" then "Add New Android Package Name".
- 3. Enter your package name, for example `com.plaid.example`.
-@@ -100,17 +106,16 @@ const MyPlaidComponent = () => {
- ##### iOS OAuth Requirements
-
- For iOS OAuth to work, specific requirements must be met.
-+
- 1. Redirect URIs must be [registered](https://plaid.com/docs/link/ios/#register-your-redirect-uri), and set up as [universal links](https://developer.apple.com/documentation/xcode/supporting-associated-domains).
- 2. Your native iOS application, must be configured with your associated domain. See your iOS [set up universal links](https://plaid.com/docs/link/ios/#set-up-universal-links) for more information.
-
--
- ##### Link Token OAuth Requirements
-
- - On iOS you must configure your `link_token` with a [redirect_uri](https://plaid.com/docs/api/tokens/#link-token-create-request-redirect-uri) to support OAuth. When creating a `link_token` for initializing Link on Android, `android_package_name` must be specified and `redirect_uri` must be left blank.
-
- - On Android you must configure your `link_token` with an [android_package_name](https://plaid.com/docs/api/tokens/#link-token-create-request-android-package-name) to support OAuth. When creating a `link_token` for initializing Link on iOS, `android_package_name` must be left blank and `redirect_uri` should be used instead.
-
--
- #### To receive onEvent callbacks:
-
- The React Native Plaid module emits `onEvent` events throughout the account linking process — see [details here](https://plaid.com/docs/link/react-native/#onevent). To receive these events in your React Native app, wrap the `PlaidLink` react component with the following in order to listen for those events:
-@@ -139,9 +144,9 @@ class PlaidEventContainer extends React.Component {
- You can also use the `usePlaidEmitter` hook in react functional components:
-
- ```javascript
-- usePlaidEmitter((event: LinkEvent) => {
-- console.log(event)
-- })
-+usePlaidEmitter((event: LinkEvent) => {
-+ console.log(event);
-+});
- ```
-
- ## Upgrading
-@@ -165,6 +170,8 @@ While these older versions are expected to continue to work without disruption,
- | 11.0.2 | * | [4.0.0+] | 21 | 33 | >=5.0.0 | 14.0 | Active, supports Xcode 15.0.1 |
- | 11.0.1 | * | [4.0.0+] | 21 | 33 | >=5.0.0 | 14.0 | Active, supports Xcode 15.0.1 |
- | 11.0.0 | * | [4.0.0+] | 21 | 33 | >=5.0.0 | 14.0 | Active, supports Xcode 15.0.1 |
-+| 10.13.0 | >= 0.66.0 | [3.14.3+] | 21 | 33 | >=4.7.2 | 11.0 | Active, supports Xcode 14 |
-+| 10.12.0 | >= 0.66.0 | [3.14.3+] | 21 | 33 | >=4.7.1 | 11.0 | Active, supports Xcode 14 |
- | 10.11.0 | >= 0.66.0 | [3.14.1+] | 21 | 33 | >=4.7.1 | 11.0 | Active, supports Xcode 14 |
- | ~10.10.0~ | >= 0.66.0 | [3.14.2+] | 21 | 33 | >=4.7.1 | 11.0 | **Deprecated** |
- | 10.9.1 | >= 0.66.0 | [3.14.1+] | 21 | 33 | >=4.7.0 | 11.0 | Active, supports Xcode 14 |
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/checksums.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/checksums.lock
-deleted file mode 100644
-index b5da584..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/checksums.lock and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/md5-checksums.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/md5-checksums.bin
-deleted file mode 100644
-index ef608b4..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/md5-checksums.bin and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/sha1-checksums.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/sha1-checksums.bin
-deleted file mode 100644
-index 0856ae4..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/sha1-checksums.bin and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/dependencies-accessors.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/dependencies-accessors.lock
-deleted file mode 100644
-index 12aea68..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/dependencies-accessors.lock and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/gc.properties b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/gc.properties
-deleted file mode 100644
-index e69de29..0000000
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileChanges/last-build.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileChanges/last-build.bin
-deleted file mode 100644
-index f76dd23..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileChanges/last-build.bin and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileHashes/fileHashes.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileHashes/fileHashes.lock
-deleted file mode 100644
-index 752a252..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileHashes/fileHashes.lock and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/gc.properties b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/gc.properties
-deleted file mode 100644
-index e69de29..0000000
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
-deleted file mode 100644
-index 470ca89..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/cache.properties b/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/cache.properties
-deleted file mode 100644
-index 1439672..0000000
---- a/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/cache.properties
-+++ /dev/null
-@@ -1,2 +0,0 @@
--#Thu Nov 09 09:41:17 PST 2023
--gradle.version=7.4.2
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/checksums.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/checksums.lock
-deleted file mode 100644
-index 34602e1..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/checksums.lock and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/md5-checksums.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/md5-checksums.bin
-deleted file mode 100644
-index 2420123..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/md5-checksums.bin and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/sha1-checksums.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/sha1-checksums.bin
-deleted file mode 100644
-index 1081852..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/sha1-checksums.bin and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/vcs-1/gc.properties b/node_modules/react-native-plaid-link-sdk/android/.gradle/vcs-1/gc.properties
-deleted file mode 100644
-index e69de29..0000000
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.idea/gradle.xml b/node_modules/react-native-plaid-link-sdk/android/.idea/gradle.xml
-deleted file mode 100644
-index 0364d75..0000000
---- a/node_modules/react-native-plaid-link-sdk/android/.idea/gradle.xml
-+++ /dev/null
-@@ -1,14 +0,0 @@
--
--
--
--
--
--
--
--
--
--
--
--
--
--
-\ No newline at end of file
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.idea/misc.xml b/node_modules/react-native-plaid-link-sdk/android/.idea/misc.xml
-deleted file mode 100644
-index a318cae..0000000
---- a/node_modules/react-native-plaid-link-sdk/android/.idea/misc.xml
-+++ /dev/null
-@@ -1,9 +0,0 @@
--
--
--
--
--
--
--
--
--
-\ No newline at end of file
-diff --git a/node_modules/react-native-plaid-link-sdk/android/.idea/vcs.xml b/node_modules/react-native-plaid-link-sdk/android/.idea/vcs.xml
-deleted file mode 100644
-index 6c0b863..0000000
---- a/node_modules/react-native-plaid-link-sdk/android/.idea/vcs.xml
-+++ /dev/null
-@@ -1,6 +0,0 @@
--
--
--
--
--
--
-\ No newline at end of file
-diff --git a/node_modules/react-native-plaid-link-sdk/android/build.gradle b/node_modules/react-native-plaid-link-sdk/android/build.gradle
-index 2d9e2ce..e88208b 100644
---- a/node_modules/react-native-plaid-link-sdk/android/build.gradle
-+++ b/node_modules/react-native-plaid-link-sdk/android/build.gradle
-@@ -12,7 +12,12 @@ allprojects {
-
-
- buildscript {
-- ext.kotlin_version = '1.8.22'
-+ ext {
-+ kotlin_version = '1.8.22'
-+ }
-+ ext.safeExtGet = {prop, fallback ->
-+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
-+ }
- repositories {
- google()
- mavenCentral()
-@@ -25,10 +30,32 @@ buildscript {
- }
- }
-
-+def isNewArchitectureEnabled() {
-+ // To opt-in for the New Architecture, you can either:
-+ // - Set `newArchEnabled` to true inside the `gradle.properties` file
-+ // - Invoke gradle with `-newArchEnabled=true`
-+ // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
-+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
-+}
-+
-+if (isNewArchitectureEnabled()) {
-+ apply plugin: "com.facebook.react"
-+}
-+
- apply plugin: 'com.android.library'
- apply plugin: "kotlin-android"
-
- android {
-+
-+ // Used to override the NDK path/version on internal CI or by allowing
-+ // users to customize the NDK path/version from their root project (e.g. for M1 support)
-+ if (rootProject.hasProperty("ndkPath")) {
-+ ndkPath rootProject.ext.ndkPath
-+ }
-+ if (rootProject.hasProperty("ndkVersion")) {
-+ ndkVersion rootProject.ext.ndkVersion
-+ }
-+
- def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
- if (agpVersion >= 7) {
- namespace 'com.plaid'
-@@ -52,6 +79,14 @@ android {
- }
- }
-
-+ sourceSets.main {
-+ java {
-+ if (!isNewArchitectureEnabled()) {
-+ srcDirs += 'src/paper/java'
-+ }
-+ }
-+ }
-+
- buildTypes {
- release {
- debuggable = false
-diff --git a/node_modules/react-native-plaid-link-sdk/android/local.properties b/node_modules/react-native-plaid-link-sdk/android/local.properties
-deleted file mode 100644
-index 0b4e321..0000000
---- a/node_modules/react-native-plaid-link-sdk/android/local.properties
-+++ /dev/null
-@@ -1,8 +0,0 @@
--## This file must *NOT* be checked into Version Control Systems,
--# as it contains information specific to your local configuration.
--#
--# Location of the SDK. This is only used by Gradle.
--# For customization when using a Version Control System, please read the
--# header note.
--#Fri Aug 11 13:58:32 PDT 2023
--sdk.dir=/Users/dtroupe/Library/Android/sdk
-diff --git a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PLKEmbeddedViewManager.kt b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PLKEmbeddedViewManager.kt
-index c73011f..66fd266 100644
---- a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PLKEmbeddedViewManager.kt
-+++ b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PLKEmbeddedViewManager.kt
-@@ -19,9 +19,9 @@ class PLKEmbeddedViewManager : SimpleViewManager() {
- }
-
- override fun getExportedCustomBubblingEventTypeConstants(): Map {
-- return mapOf(
-- EVENT_NAME to mapOf(
-- "phasedRegistrationNames" to mapOf(
-+ return mutableMapOf(
-+ EVENT_NAME to mutableMapOf(
-+ "phasedRegistrationNames" to mutableMapOf(
- "bubbled" to EVENT_NAME
- )
- ))
-diff --git a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidModule.kt b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidModule.kt
-index 293374a..b79352e 100644
---- a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidModule.kt
-+++ b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidModule.kt
-@@ -24,9 +24,9 @@ import org.json.JSONException
- import org.json.JSONObject
- import java.util.ArrayList
-
--@ReactModule(name = PlaidModule.TAG)
-+@ReactModule(name = PlaidModule.NAME)
- class PlaidModule internal constructor(reactContext: ReactApplicationContext) :
-- ReactContextBaseJavaModule(reactContext), ActivityEventListener {
-+ NativePlaidLinkModuleAndroidSpec(reactContext), ActivityEventListener {
-
- val mActivityResultManager by lazy { ActivityResultManager() }
-
-@@ -38,11 +38,11 @@ class PlaidModule internal constructor(reactContext: ReactApplicationContext) :
- companion object {
- private const val LINK_TOKEN_PREFIX = "link"
-
-- const val TAG = "PlaidAndroid"
-+ const val NAME = "PlaidAndroid"
- }
-
- override fun getName(): String {
-- return PlaidModule.TAG
-+ return NAME
- }
-
- override fun initialize() {
-@@ -78,7 +78,7 @@ class PlaidModule internal constructor(reactContext: ReactApplicationContext) :
-
- @ReactMethod
- @Suppress("unused")
-- fun startLinkActivityForResult(
-+ override fun startLinkActivityForResult(
- token: String,
- noLoadingState: Boolean,
- logLevel: String,
-@@ -113,6 +113,10 @@ class PlaidModule internal constructor(reactContext: ReactApplicationContext) :
- }
- }
-
-+ override fun addListener(eventName: String?) = Unit
-+
-+ override fun removeListeners(count: Double) = Unit
-+
- private fun maybeGetStringField(obj: JSONObject, fieldName: String): String? {
- if (obj.has(fieldName) && !TextUtils.isEmpty(obj.getString(fieldName))) {
- return obj.getString(fieldName)
-diff --git a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidPackage.java b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidPackage.java
-index c59299e..d6b310e 100644
---- a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidPackage.java
-+++ b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidPackage.java
-@@ -6,19 +6,54 @@ import java.util.List;
- import java.util.Map;
-
- import com.facebook.react.TurboReactPackage;
-+import com.facebook.react.ViewManagerOnDemandReactPackage;
-+import com.facebook.react.bridge.ModuleSpec;
- import com.facebook.react.bridge.NativeModule;
- import com.facebook.react.bridge.ReactApplicationContext;
-+import com.facebook.react.module.annotations.ReactModule;
-+import com.facebook.react.module.annotations.ReactModuleList;
- import com.facebook.react.module.model.ReactModuleInfo;
- import com.facebook.react.module.model.ReactModuleInfoProvider;
-+import com.facebook.react.turbomodule.core.interfaces.TurboModule;
- import com.facebook.react.uimanager.ViewManager;
-
--@SuppressWarnings("unused")
--public class PlaidPackage extends TurboReactPackage {
-+import javax.annotation.Nonnull;
-+import javax.annotation.Nullable;
-
-+@ReactModuleList(nativeModules = {PlaidModule.class})
-+public class PlaidPackage extends TurboReactPackage implements ViewManagerOnDemandReactPackage {
-+
-+ /**
-+ * {@inheritDoc}
-+ */
-+ @Override
-+ public List getViewManagerNames(ReactApplicationContext reactContext) {
-+ return null;
-+ }
-+
-+ @Override
-+ protected List getViewManagers(ReactApplicationContext reactContext) {
-+ return null;
-+ }
-+
-+ /**
-+ * {@inheritDoc}
-+ */
- @Override
-- public NativeModule getModule(
-- String name, ReactApplicationContext reactContext) {
-- return new PlaidModule(reactContext);
-+ public @Nullable
-+ ViewManager createViewManager(
-+ ReactApplicationContext reactContext, String viewManagerName) {
-+ return null;
-+ }
-+
-+ @Override
-+ public NativeModule getModule(String name, @Nonnull ReactApplicationContext reactContext) {
-+ switch (name) {
-+ case PlaidModule.NAME:
-+ return new PlaidModule(reactContext);
-+ default:
-+ return null;
-+ }
- }
-
- @Override
-@@ -28,19 +63,44 @@ public class PlaidPackage extends TurboReactPackage {
-
- @Override
- public ReactModuleInfoProvider getReactModuleInfoProvider() {
-- return () -> {
-- Map map = new HashMap<>();
-- map.put(
-- "PlaidAndroid",
-- new ReactModuleInfo(
-- "PlaidAndroid",
-- "com.reactlibrary.PlaidModule",
-- false,
-- false,
-- true,
-- false,
-- false));
-- return map;
-- };
-+ try {
-+ Class> reactModuleInfoProviderClass =
-+ Class.forName("com.plaid.PlaidPackage$$ReactModuleInfoProvider");
-+ return (ReactModuleInfoProvider) reactModuleInfoProviderClass.newInstance();
-+ } catch (ClassNotFoundException e) {
-+ // ReactModuleSpecProcessor does not run at build-time. Create this ReactModuleInfoProvider by
-+ // hand.
-+ return new ReactModuleInfoProvider() {
-+ @Override
-+ public Map getReactModuleInfos() {
-+ final Map reactModuleInfoMap = new HashMap<>();
-+
-+ Class extends NativeModule>[] moduleList =
-+ new Class[]{
-+ PlaidModule.class,
-+ };
-+
-+ for (Class extends NativeModule> moduleClass : moduleList) {
-+ ReactModule reactModule = moduleClass.getAnnotation(ReactModule.class);
-+
-+ reactModuleInfoMap.put(
-+ reactModule.name(),
-+ new ReactModuleInfo(
-+ reactModule.name(),
-+ moduleClass.getName(),
-+ reactModule.canOverrideExistingModule(),
-+ reactModule.needsEagerInit(),
-+ reactModule.hasConstants(),
-+ reactModule.isCxxModule(),
-+ TurboModule.class.isAssignableFrom(moduleClass)));
-+ }
-+
-+ return reactModuleInfoMap;
-+ }
-+ };
-+ } catch (InstantiationException | IllegalAccessException e) {
-+ throw new RuntimeException(
-+ "No ReactModuleInfoProvider for com.plaid.PlaidPackage$$ReactModuleInfoProvider", e);
-+ }
- }
- }
-diff --git a/node_modules/react-native-plaid-link-sdk/android/src/paper/java/com/plaid/NativePlaidLinkModuleAndroidSpec.java b/node_modules/react-native-plaid-link-sdk/android/src/paper/java/com/plaid/NativePlaidLinkModuleAndroidSpec.java
-new file mode 100644
-index 0000000..fee5a11
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/android/src/paper/java/com/plaid/NativePlaidLinkModuleAndroidSpec.java
-@@ -0,0 +1,46 @@
-+
-+/**
-+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
-+ *
-+ * Do not edit this file as changes may cause incorrect behavior and will be lost
-+ * once the code is regenerated.
-+ *
-+ * @generated by codegen project: GenerateModuleJavaSpec.js
-+ *
-+ * @nolint
-+ */
-+
-+package com.plaid;
-+
-+import com.facebook.proguard.annotations.DoNotStrip;
-+import com.facebook.react.bridge.Callback;
-+import com.facebook.react.bridge.ReactApplicationContext;
-+import com.facebook.react.bridge.ReactContextBaseJavaModule;
-+import com.facebook.react.bridge.ReactMethod;
-+import com.facebook.react.turbomodule.core.interfaces.TurboModule;
-+import javax.annotation.Nonnull;
-+
-+public abstract class NativePlaidLinkModuleAndroidSpec extends ReactContextBaseJavaModule implements TurboModule {
-+ public static final String NAME = "PlaidAndroid";
-+
-+ public NativePlaidLinkModuleAndroidSpec(ReactApplicationContext reactContext) {
-+ super(reactContext);
-+ }
-+
-+ @Override
-+ public @Nonnull String getName() {
-+ return NAME;
-+ }
-+
-+ @ReactMethod
-+ @DoNotStrip
-+ public abstract void startLinkActivityForResult(String token, boolean noLoadingState, String logLevel, Callback onSuccessCallback, Callback onExitCallback);
-+
-+ @ReactMethod
-+ @DoNotStrip
-+ public abstract void addListener(String eventName);
-+
-+ @ReactMethod
-+ @DoNotStrip
-+ public abstract void removeListeners(double count);
-+}
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/EmbeddedLink/EmbeddedLinkView.js b/node_modules/react-native-plaid-link-sdk/dist/EmbeddedLink/EmbeddedLinkView.js
-index c7b1e96..c429da7 100644
---- a/node_modules/react-native-plaid-link-sdk/dist/EmbeddedLink/EmbeddedLinkView.js
-+++ b/node_modules/react-native-plaid-link-sdk/dist/EmbeddedLink/EmbeddedLinkView.js
-@@ -1,55 +1,69 @@
- import React from 'react';
- import NativeEmbeddedLinkView from './NativeEmbeddedLinkView';
- class EmbeddedEvent {
-- constructor(event) {
-- this.eventName = event.eventName;
-- this.metadata = event.metadata;
-- }
-+ constructor(event) {
-+ this.eventName = event.eventName;
-+ this.metadata = event.metadata;
-+ }
- }
- class EmbeddedExit {
-- constructor(event) {
-- this.error = event.error;
-- this.metadata = event.metadata;
-- }
-+ constructor(event) {
-+ this.error = event.error;
-+ this.metadata = event.metadata;
-+ }
- }
- class EmbeddedSuccess {
-- constructor(event) {
-- this.publicToken = event.publicToken;
-- this.metadata = event.metadata;
-- }
-+ constructor(event) {
-+ this.publicToken = event.publicToken;
-+ this.metadata = event.metadata;
-+ }
- }
--export const EmbeddedLinkView = (props) => {
-- const { token, iOSPresentationStyle, onEvent, onSuccess, onExit, style } = props;
-- const onEmbeddedEvent = (event) => {
-- switch (event.nativeEvent.embeddedEventName) {
-- case 'onSuccess': {
-- if (!onSuccess) {
-- return;
-- }
-- const embeddedSuccess = new EmbeddedSuccess(event.nativeEvent);
-- onSuccess(embeddedSuccess);
-- break;
-- }
-- case 'onExit': {
-- if (!onExit) {
-- return;
-- }
-- const embeddedExit = new EmbeddedExit(event.nativeEvent);
-- onExit(embeddedExit);
-- break;
-- }
-- case 'onEvent': {
-- if (!onEvent) {
-- return;
-- }
-- const embeddedEvent = new EmbeddedEvent(event.nativeEvent);
-- onEvent(embeddedEvent);
-- break;
-- }
-- default: {
-- return;
-- }
-+export const EmbeddedLinkView = props => {
-+ const {
-+ token,
-+ iOSPresentationStyle,
-+ onEvent,
-+ onSuccess,
-+ onExit,
-+ style,
-+ } = props;
-+ const onEmbeddedEvent = event => {
-+ switch (event.nativeEvent.embeddedEventName) {
-+ case 'onSuccess': {
-+ if (!onSuccess) {
-+ return;
- }
-- };
-- return ;
-+ const embeddedSuccess = new EmbeddedSuccess(event.nativeEvent);
-+ onSuccess(embeddedSuccess);
-+ break;
-+ }
-+ case 'onExit': {
-+ if (!onExit) {
-+ return;
-+ }
-+ const embeddedExit = new EmbeddedExit(event.nativeEvent);
-+ onExit(embeddedExit);
-+ break;
-+ }
-+ case 'onEvent': {
-+ if (!onEvent) {
-+ return;
-+ }
-+ const embeddedEvent = new EmbeddedEvent(event.nativeEvent);
-+ onEvent(embeddedEvent);
-+ break;
-+ }
-+ default: {
-+ return;
-+ }
-+ }
-+ };
-+ return (
-+
-+ );
- };
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.d.ts b/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.d.ts
-index a48b319..43205dd 100644
---- a/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.d.ts
-+++ b/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.d.ts
-@@ -4,9 +4,9 @@ import { LinkEventListener, PlaidLinkComponentProps, PlaidLinkProps } from './Ty
- * A hook that registers a listener on the Plaid emitter for the 'onEvent' type.
- * The listener is cleaned up when this view is unmounted
- *
-- * @param LinkEventListener the listener to call
-+ * @param linkEventListener the listener to call
- */
--export declare const usePlaidEmitter: (LinkEventListener: LinkEventListener) => void;
-+export declare const usePlaidEmitter: (linkEventListener: LinkEventListener) => void;
- export declare const openLink: (props: PlaidLinkProps) => Promise;
- export declare const dismissLink: () => void;
- export declare const PlaidLink: (props: PlaidLinkComponentProps) => React.JSX.Element;
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.js b/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.js
-index 21da2bc..6c43633 100644
---- a/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.js
-+++ b/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.js
-@@ -1,83 +1,146 @@
--var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
-- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
-- return new (P || (P = Promise))(function (resolve, reject) {
-- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
-- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
-- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
-- step((generator = generator.apply(thisArg, _arguments || [])).next());
-+var __awaiter =
-+ (this && this.__awaiter) ||
-+ function(thisArg, _arguments, P, generator) {
-+ function adopt(value) {
-+ return value instanceof P
-+ ? value
-+ : new P(function(resolve) {
-+ resolve(value);
-+ });
-+ }
-+ return new (P || (P = Promise))(function(resolve, reject) {
-+ function fulfilled(value) {
-+ try {
-+ step(generator.next(value));
-+ } catch (e) {
-+ reject(e);
-+ }
-+ }
-+ function rejected(value) {
-+ try {
-+ step(generator['throw'](value));
-+ } catch (e) {
-+ reject(e);
-+ }
-+ }
-+ function step(result) {
-+ result.done
-+ ? resolve(result.value)
-+ : adopt(result.value).then(fulfilled, rejected);
-+ }
-+ step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
--};
-+ };
-+var _a;
- import React, { useEffect } from 'react';
--import { NativeEventEmitter, NativeModules, Platform, TouchableOpacity, } from 'react-native';
--import { LinkIOSPresentationStyle, LinkLogLevel, } from './Types';
-+import { NativeEventEmitter, Platform, TouchableOpacity } from 'react-native';
-+import { LinkIOSPresentationStyle, LinkLogLevel } from './Types';
-+import RNLinksdkAndroid from './fabric/NativePlaidLinkModuleAndroid';
-+import RNLinksdkiOS from './fabric/NativePlaidLinkModuleiOS';
-+const RNLinksdk =
-+ (_a = Platform.OS === 'android' ? RNLinksdkAndroid : RNLinksdkiOS) !== null &&
-+ _a !== void 0
-+ ? _a
-+ : undefined;
- /**
- * A hook that registers a listener on the Plaid emitter for the 'onEvent' type.
- * The listener is cleaned up when this view is unmounted
- *
-- * @param LinkEventListener the listener to call
-+ * @param linkEventListener the listener to call
- */
--export const usePlaidEmitter = (LinkEventListener) => {
-- useEffect(() => {
-- const emitter = new NativeEventEmitter(Platform.OS === 'ios'
-- ? NativeModules.RNLinksdk
-- : NativeModules.PlaidAndroid);
-- const listener = emitter.addListener('onEvent', LinkEventListener);
-- // Clean up after this effect:
-- return function cleanup() {
-- listener.remove();
-- };
-- }, []);
-+export const usePlaidEmitter = linkEventListener => {
-+ useEffect(() => {
-+ const emitter = new NativeEventEmitter(RNLinksdk);
-+ const listener = emitter.addListener('onEvent', linkEventListener);
-+ // Clean up after this effect:
-+ return function cleanup() {
-+ listener.remove();
-+ };
-+ }, []);
- };
--export const openLink = (props) => __awaiter(void 0, void 0, void 0, function* () {
-- var _a, _b;
-+export const openLink = props =>
-+ __awaiter(void 0, void 0, void 0, function*() {
-+ var _b, _c;
- let config = props.tokenConfig;
-- let noLoadingState = (_a = config.noLoadingState) !== null && _a !== void 0 ? _a : false;
-+ let noLoadingState =
-+ (_b = config.noLoadingState) !== null && _b !== void 0 ? _b : false;
- if (Platform.OS === 'android') {
-- NativeModules.PlaidAndroid.startLinkActivityForResult(config.token, noLoadingState, (_b = config.logLevel) !== null && _b !== void 0 ? _b : LinkLogLevel.ERROR, (result) => {
-- if (props.onSuccess != null) {
-- props.onSuccess(result);
-- }
-- }, (result) => {
-- if (props.onExit != null) {
-- if (result.error != null && result.error.displayMessage != null) {
-- //TODO(RNSDK-118): Remove errorDisplayMessage field in next major update.
-- result.error.errorDisplayMessage = result.error.displayMessage;
-- }
-- props.onExit(result);
-+ if (RNLinksdkAndroid === null) {
-+ throw new Error(
-+ '[react-native-plaid-link-sdk] RNLinksdkAndroid is not defined',
-+ );
-+ }
-+ RNLinksdkAndroid.startLinkActivityForResult(
-+ config.token,
-+ noLoadingState,
-+ (_c = config.logLevel) !== null && _c !== void 0
-+ ? _c
-+ : LinkLogLevel.ERROR,
-+ // @ts-ignore we use Object type in the spec file as it maps to NSDictionary and ReadableMap
-+ result => {
-+ if (props.onSuccess != null) {
-+ props.onSuccess(result);
-+ }
-+ },
-+ result => {
-+ if (props.onExit != null) {
-+ if (result.error != null && result.error.displayMessage != null) {
-+ //TODO(RNSDK-118): Remove errorDisplayMessage field in next major update.
-+ result.error.errorDisplayMessage = result.error.displayMessage;
- }
-- });
-- }
-- else {
-- NativeModules.RNLinksdk.create(config.token, noLoadingState);
-- let presentFullScreen = props.iOSPresentationStyle == LinkIOSPresentationStyle.FULL_SCREEN;
-- NativeModules.RNLinksdk.open(presentFullScreen, (result) => {
-- if (props.onSuccess != null) {
-- props.onSuccess(result);
-- }
-- }, (error, result) => {
-- if (props.onExit != null) {
-- if (error) {
-- var data = result || {};
-- data.error = error;
-- props.onExit(data);
-- }
-- else {
-- props.onExit(result);
-- }
-+ props.onExit(result);
-+ }
-+ },
-+ );
-+ } else {
-+ if (RNLinksdkiOS === null) {
-+ throw new Error(
-+ '[react-native-plaid-link-sdk] RNLinksdkiOS is not defined',
-+ );
-+ }
-+ RNLinksdkiOS.create(config.token, noLoadingState);
-+ let presentFullScreen =
-+ props.iOSPresentationStyle == LinkIOSPresentationStyle.FULL_SCREEN;
-+ RNLinksdkiOS.open(
-+ presentFullScreen,
-+ // @ts-ignore we use Object type in the spec file as it maps to NSDictionary and ReadableMap
-+ result => {
-+ if (props.onSuccess != null) {
-+ props.onSuccess(result);
-+ }
-+ },
-+ (error, result) => {
-+ if (props.onExit != null) {
-+ if (error) {
-+ var data = result || {};
-+ data.error = error;
-+ props.onExit(data);
-+ } else {
-+ props.onExit(result);
- }
-- });
-+ }
-+ },
-+ );
- }
--});
-+ });
- export const dismissLink = () => {
-- if (Platform.OS === 'ios') {
-- NativeModules.RNLinksdk.dismiss();
-+ if (Platform.OS === 'ios') {
-+ if (RNLinksdkiOS === null) {
-+ throw new Error(
-+ '[react-native-plaid-link-sdk] RNLinksdkiOS is not defined',
-+ );
- }
-+ RNLinksdkiOS.dismiss();
-+ }
- };
--export const PlaidLink = (props) => {
-- function onPress() {
-- var _a;
-- (_a = props.onPress) === null || _a === void 0 ? void 0 : _a.call(props);
-- openLink(props);
-- }
-- return {props.children} ;
-+export const PlaidLink = props => {
-+ function onPress() {
-+ var _a;
-+ (_a = props.onPress) === null || _a === void 0 ? void 0 : _a.call(props);
-+ openLink(props);
-+ }
-+ return (
-+ // @ts-ignore some types directories misconfiguration
-+ {props.children}
-+ );
- };
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/Types.js b/node_modules/react-native-plaid-link-sdk/dist/Types.js
-index 184adad..11b34e3 100644
---- a/node_modules/react-native-plaid-link-sdk/dist/Types.js
-+++ b/node_modules/react-native-plaid-link-sdk/dist/Types.js
-@@ -1,430 +1,681 @@
- export var LinkLogLevel;
--(function (LinkLogLevel) {
-- LinkLogLevel["DEBUG"] = "debug";
-- LinkLogLevel["INFO"] = "info";
-- LinkLogLevel["WARN"] = "warn";
-- LinkLogLevel["ERROR"] = "error";
-+(function(LinkLogLevel) {
-+ LinkLogLevel['DEBUG'] = 'debug';
-+ LinkLogLevel['INFO'] = 'info';
-+ LinkLogLevel['WARN'] = 'warn';
-+ LinkLogLevel['ERROR'] = 'error';
- })(LinkLogLevel || (LinkLogLevel = {}));
- export var PlaidEnvironment;
--(function (PlaidEnvironment) {
-- PlaidEnvironment["PRODUCTION"] = "production";
-- PlaidEnvironment["DEVELOPMENT"] = "development";
-- PlaidEnvironment["SANDBOX"] = "sandbox";
-+(function(PlaidEnvironment) {
-+ PlaidEnvironment['PRODUCTION'] = 'production';
-+ PlaidEnvironment['DEVELOPMENT'] = 'development';
-+ PlaidEnvironment['SANDBOX'] = 'sandbox';
- })(PlaidEnvironment || (PlaidEnvironment = {}));
- export var PlaidProduct;
--(function (PlaidProduct) {
-- PlaidProduct["ASSETS"] = "assets";
-- PlaidProduct["AUTH"] = "auth";
-- PlaidProduct["DEPOSIT_SWITCH"] = "deposit_switch";
-- PlaidProduct["IDENTITY"] = "identity";
-- PlaidProduct["INCOME"] = "income";
-- PlaidProduct["INVESTMENTS"] = "investments";
-- PlaidProduct["LIABILITIES"] = "liabilities";
-- PlaidProduct["LIABILITIES_REPORT"] = "liabilities_report";
-- PlaidProduct["PAYMENT_INITIATION"] = "payment_initiation";
-- PlaidProduct["TRANSACTIONS"] = "transactions";
-+(function(PlaidProduct) {
-+ PlaidProduct['ASSETS'] = 'assets';
-+ PlaidProduct['AUTH'] = 'auth';
-+ PlaidProduct['DEPOSIT_SWITCH'] = 'deposit_switch';
-+ PlaidProduct['IDENTITY'] = 'identity';
-+ PlaidProduct['INCOME'] = 'income';
-+ PlaidProduct['INVESTMENTS'] = 'investments';
-+ PlaidProduct['LIABILITIES'] = 'liabilities';
-+ PlaidProduct['LIABILITIES_REPORT'] = 'liabilities_report';
-+ PlaidProduct['PAYMENT_INITIATION'] = 'payment_initiation';
-+ PlaidProduct['TRANSACTIONS'] = 'transactions';
- })(PlaidProduct || (PlaidProduct = {}));
- export var LinkAccountType;
--(function (LinkAccountType) {
-- LinkAccountType["CREDIT"] = "credit";
-- LinkAccountType["DEPOSITORY"] = "depository";
-- LinkAccountType["INVESTMENT"] = "investment";
-- LinkAccountType["LOAN"] = "loan";
-- LinkAccountType["OTHER"] = "other";
-+(function(LinkAccountType) {
-+ LinkAccountType['CREDIT'] = 'credit';
-+ LinkAccountType['DEPOSITORY'] = 'depository';
-+ LinkAccountType['INVESTMENT'] = 'investment';
-+ LinkAccountType['LOAN'] = 'loan';
-+ LinkAccountType['OTHER'] = 'other';
- })(LinkAccountType || (LinkAccountType = {}));
- export var LinkAccountSubtypes;
--(function (LinkAccountSubtypes) {
-- LinkAccountSubtypes["ALL"] = "all";
-- LinkAccountSubtypes["CREDIT_CARD"] = "credit card";
-- LinkAccountSubtypes["PAYPAL"] = "paypal";
-- LinkAccountSubtypes["AUTO"] = "auto";
-- LinkAccountSubtypes["BUSINESS"] = "business";
-- LinkAccountSubtypes["COMMERCIAL"] = "commercial";
-- LinkAccountSubtypes["CONSTRUCTION"] = "construction";
-- LinkAccountSubtypes["CONSUMER"] = "consumer";
-- LinkAccountSubtypes["HOME_EQUITY"] = "home equity";
-- LinkAccountSubtypes["LINE_OF_CREDIT"] = "line of credit";
-- LinkAccountSubtypes["LOAN"] = "loan";
-- LinkAccountSubtypes["MORTGAGE"] = "mortgage";
-- LinkAccountSubtypes["OVERDRAFT"] = "overdraft";
-- LinkAccountSubtypes["STUDENT"] = "student";
-- LinkAccountSubtypes["CASH_MANAGEMENT"] = "cash management";
-- LinkAccountSubtypes["CD"] = "cd";
-- LinkAccountSubtypes["CHECKING"] = "checking";
-- LinkAccountSubtypes["EBT"] = "ebt";
-- LinkAccountSubtypes["HSA"] = "hsa";
-- LinkAccountSubtypes["MONEY_MARKET"] = "money market";
-- LinkAccountSubtypes["PREPAID"] = "prepaid";
-- LinkAccountSubtypes["SAVINGS"] = "savings";
-- LinkAccountSubtypes["FOUR_0_1_A"] = "401a";
-- LinkAccountSubtypes["FOUR_0_1_K"] = "401k";
-- LinkAccountSubtypes["FOUR_0_3_B"] = "403B";
-- LinkAccountSubtypes["FOUR_5_7_B"] = "457b";
-- LinkAccountSubtypes["FIVE_2_9"] = "529";
-- LinkAccountSubtypes["BROKERAGE"] = "brokerage";
-- LinkAccountSubtypes["CASH_ISA"] = "cash isa";
-- LinkAccountSubtypes["EDUCATION_SAVINGS_ACCOUNT"] = "education savings account";
-- LinkAccountSubtypes["FIXED_ANNUNITY"] = "fixed annuity";
-- LinkAccountSubtypes["GIC"] = "gic";
-- LinkAccountSubtypes["HEALTH_REIMBURSEMENT_ARRANGEMENT"] = "health reimbursement arrangement";
-- LinkAccountSubtypes["IRA"] = "ira";
-- LinkAccountSubtypes["ISA"] = "isa";
-- LinkAccountSubtypes["KEOGH"] = "keogh";
-- LinkAccountSubtypes["LIF"] = "lif";
-- LinkAccountSubtypes["LIRA"] = "lira";
-- LinkAccountSubtypes["LRIF"] = "lrif";
-- LinkAccountSubtypes["LRSP"] = "lrsp";
-- LinkAccountSubtypes["MUTUAL_FUND"] = "mutual fund";
-- LinkAccountSubtypes["NON_TAXABLE_BROKERAGE_ACCOUNT"] = "non-taxable brokerage account";
-- LinkAccountSubtypes["PENSION"] = "pension";
-- LinkAccountSubtypes["PLAN"] = "plan";
-- LinkAccountSubtypes["PRIF"] = "prif";
-- LinkAccountSubtypes["PROFIT_SHARING_PLAN"] = "profit sharing plan";
-- LinkAccountSubtypes["RDSP"] = "rdsp";
-- LinkAccountSubtypes["RESP"] = "resp";
-- LinkAccountSubtypes["RETIREMENT"] = "retirement";
-- LinkAccountSubtypes["RLIF"] = "rlif";
-- LinkAccountSubtypes["ROTH_401K"] = "roth 401k";
-- LinkAccountSubtypes["ROTH"] = "roth";
-- LinkAccountSubtypes["RRIF"] = "rrif";
-- LinkAccountSubtypes["RRSP"] = "rrsp";
-- LinkAccountSubtypes["SARSEP"] = "sarsep";
-- LinkAccountSubtypes["SEP_IRA"] = "sep ira";
-- LinkAccountSubtypes["SIMPLE_IRA"] = "simple ira";
-- LinkAccountSubtypes["SIPP"] = "sipp";
-- LinkAccountSubtypes["STOCK_PLAN"] = "stock plan";
-- LinkAccountSubtypes["TFSA"] = "tfsa";
-- LinkAccountSubtypes["THRIFT_SAVINGS_PLAN"] = "thrift savings plan";
-- LinkAccountSubtypes["TRUST"] = "trust";
-- LinkAccountSubtypes["UGMA"] = "ugma";
-- LinkAccountSubtypes["UTMA"] = "utma";
-- LinkAccountSubtypes["VARIABLE_ANNUITY"] = "variable annuity";
-+(function(LinkAccountSubtypes) {
-+ LinkAccountSubtypes['ALL'] = 'all';
-+ LinkAccountSubtypes['CREDIT_CARD'] = 'credit card';
-+ LinkAccountSubtypes['PAYPAL'] = 'paypal';
-+ LinkAccountSubtypes['AUTO'] = 'auto';
-+ LinkAccountSubtypes['BUSINESS'] = 'business';
-+ LinkAccountSubtypes['COMMERCIAL'] = 'commercial';
-+ LinkAccountSubtypes['CONSTRUCTION'] = 'construction';
-+ LinkAccountSubtypes['CONSUMER'] = 'consumer';
-+ LinkAccountSubtypes['HOME_EQUITY'] = 'home equity';
-+ LinkAccountSubtypes['LINE_OF_CREDIT'] = 'line of credit';
-+ LinkAccountSubtypes['LOAN'] = 'loan';
-+ LinkAccountSubtypes['MORTGAGE'] = 'mortgage';
-+ LinkAccountSubtypes['OVERDRAFT'] = 'overdraft';
-+ LinkAccountSubtypes['STUDENT'] = 'student';
-+ LinkAccountSubtypes['CASH_MANAGEMENT'] = 'cash management';
-+ LinkAccountSubtypes['CD'] = 'cd';
-+ LinkAccountSubtypes['CHECKING'] = 'checking';
-+ LinkAccountSubtypes['EBT'] = 'ebt';
-+ LinkAccountSubtypes['HSA'] = 'hsa';
-+ LinkAccountSubtypes['MONEY_MARKET'] = 'money market';
-+ LinkAccountSubtypes['PREPAID'] = 'prepaid';
-+ LinkAccountSubtypes['SAVINGS'] = 'savings';
-+ LinkAccountSubtypes['FOUR_0_1_A'] = '401a';
-+ LinkAccountSubtypes['FOUR_0_1_K'] = '401k';
-+ LinkAccountSubtypes['FOUR_0_3_B'] = '403B';
-+ LinkAccountSubtypes['FOUR_5_7_B'] = '457b';
-+ LinkAccountSubtypes['FIVE_2_9'] = '529';
-+ LinkAccountSubtypes['BROKERAGE'] = 'brokerage';
-+ LinkAccountSubtypes['CASH_ISA'] = 'cash isa';
-+ LinkAccountSubtypes['EDUCATION_SAVINGS_ACCOUNT'] =
-+ 'education savings account';
-+ LinkAccountSubtypes['FIXED_ANNUNITY'] = 'fixed annuity';
-+ LinkAccountSubtypes['GIC'] = 'gic';
-+ LinkAccountSubtypes['HEALTH_REIMBURSEMENT_ARRANGEMENT'] =
-+ 'health reimbursement arrangement';
-+ LinkAccountSubtypes['IRA'] = 'ira';
-+ LinkAccountSubtypes['ISA'] = 'isa';
-+ LinkAccountSubtypes['KEOGH'] = 'keogh';
-+ LinkAccountSubtypes['LIF'] = 'lif';
-+ LinkAccountSubtypes['LIRA'] = 'lira';
-+ LinkAccountSubtypes['LRIF'] = 'lrif';
-+ LinkAccountSubtypes['LRSP'] = 'lrsp';
-+ LinkAccountSubtypes['MUTUAL_FUND'] = 'mutual fund';
-+ LinkAccountSubtypes['NON_TAXABLE_BROKERAGE_ACCOUNT'] =
-+ 'non-taxable brokerage account';
-+ LinkAccountSubtypes['PENSION'] = 'pension';
-+ LinkAccountSubtypes['PLAN'] = 'plan';
-+ LinkAccountSubtypes['PRIF'] = 'prif';
-+ LinkAccountSubtypes['PROFIT_SHARING_PLAN'] = 'profit sharing plan';
-+ LinkAccountSubtypes['RDSP'] = 'rdsp';
-+ LinkAccountSubtypes['RESP'] = 'resp';
-+ LinkAccountSubtypes['RETIREMENT'] = 'retirement';
-+ LinkAccountSubtypes['RLIF'] = 'rlif';
-+ LinkAccountSubtypes['ROTH_401K'] = 'roth 401k';
-+ LinkAccountSubtypes['ROTH'] = 'roth';
-+ LinkAccountSubtypes['RRIF'] = 'rrif';
-+ LinkAccountSubtypes['RRSP'] = 'rrsp';
-+ LinkAccountSubtypes['SARSEP'] = 'sarsep';
-+ LinkAccountSubtypes['SEP_IRA'] = 'sep ira';
-+ LinkAccountSubtypes['SIMPLE_IRA'] = 'simple ira';
-+ LinkAccountSubtypes['SIPP'] = 'sipp';
-+ LinkAccountSubtypes['STOCK_PLAN'] = 'stock plan';
-+ LinkAccountSubtypes['TFSA'] = 'tfsa';
-+ LinkAccountSubtypes['THRIFT_SAVINGS_PLAN'] = 'thrift savings plan';
-+ LinkAccountSubtypes['TRUST'] = 'trust';
-+ LinkAccountSubtypes['UGMA'] = 'ugma';
-+ LinkAccountSubtypes['UTMA'] = 'utma';
-+ LinkAccountSubtypes['VARIABLE_ANNUITY'] = 'variable annuity';
- })(LinkAccountSubtypes || (LinkAccountSubtypes = {}));
- export class LinkAccountSubtypeCredit {
-- constructor(type, subtype) {
-- this.type = type;
-- this.subtype = subtype;
-- }
-+ constructor(type, subtype) {
-+ this.type = type;
-+ this.subtype = subtype;
-+ }
- }
--LinkAccountSubtypeCredit.ALL = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.ALL);
--LinkAccountSubtypeCredit.CREDIT_CARD = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.CREDIT_CARD);
--LinkAccountSubtypeCredit.PAYPAL = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.PAYPAL);
-+LinkAccountSubtypeCredit.ALL = new LinkAccountSubtypeCredit(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.ALL,
-+);
-+LinkAccountSubtypeCredit.CREDIT_CARD = new LinkAccountSubtypeCredit(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.CREDIT_CARD,
-+);
-+LinkAccountSubtypeCredit.PAYPAL = new LinkAccountSubtypeCredit(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.PAYPAL,
-+);
- export class LinkAccountSubtypeDepository {
-- constructor(type, subtype) {
-- this.type = type;
-- this.subtype = subtype;
-- }
-+ constructor(type, subtype) {
-+ this.type = type;
-+ this.subtype = subtype;
-+ }
- }
--LinkAccountSubtypeDepository.ALL = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.ALL);
--LinkAccountSubtypeDepository.CASH_MANAGEMENT = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CASH_MANAGEMENT);
--LinkAccountSubtypeDepository.CD = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CD);
--LinkAccountSubtypeDepository.CHECKING = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CHECKING);
--LinkAccountSubtypeDepository.EBT = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.EBT);
--LinkAccountSubtypeDepository.HSA = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.HSA);
--LinkAccountSubtypeDepository.MONEY_MARKET = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.MONEY_MARKET);
--LinkAccountSubtypeDepository.PAYPAL = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.PAYPAL);
--LinkAccountSubtypeDepository.PREPAID = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.PREPAID);
--LinkAccountSubtypeDepository.SAVINGS = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.SAVINGS);
-+LinkAccountSubtypeDepository.ALL = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.ALL,
-+);
-+LinkAccountSubtypeDepository.CASH_MANAGEMENT = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.CASH_MANAGEMENT,
-+);
-+LinkAccountSubtypeDepository.CD = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.CD,
-+);
-+LinkAccountSubtypeDepository.CHECKING = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.CHECKING,
-+);
-+LinkAccountSubtypeDepository.EBT = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.EBT,
-+);
-+LinkAccountSubtypeDepository.HSA = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.HSA,
-+);
-+LinkAccountSubtypeDepository.MONEY_MARKET = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.MONEY_MARKET,
-+);
-+LinkAccountSubtypeDepository.PAYPAL = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.PAYPAL,
-+);
-+LinkAccountSubtypeDepository.PREPAID = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.PREPAID,
-+);
-+LinkAccountSubtypeDepository.SAVINGS = new LinkAccountSubtypeDepository(
-+ LinkAccountType.DEPOSITORY,
-+ LinkAccountSubtypes.SAVINGS,
-+);
- export class LinkAccountSubtypeInvestment {
-- constructor(type, subtype) {
-- this.type = type;
-- this.subtype = subtype;
-- }
-+ constructor(type, subtype) {
-+ this.type = type;
-+ this.subtype = subtype;
-+ }
- }
--LinkAccountSubtypeInvestment.ALL = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ALL);
--LinkAccountSubtypeInvestment.BROKERAGE = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.BROKERAGE);
--LinkAccountSubtypeInvestment.CASH_ISA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.CASH_ISA);
--LinkAccountSubtypeInvestment.EDUCATION_SAVINGS_ACCOUNT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.EDUCATION_SAVINGS_ACCOUNT);
--LinkAccountSubtypeInvestment.FIXED_ANNUNITY = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FIXED_ANNUNITY);
--LinkAccountSubtypeInvestment.GIC = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.GIC);
--LinkAccountSubtypeInvestment.HEALTH_REIMBURSEMENT_ARRANGEMENT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.HEALTH_REIMBURSEMENT_ARRANGEMENT);
--LinkAccountSubtypeInvestment.HSA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.HSA);
--LinkAccountSubtypeInvestment.INVESTMENT_401A = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_1_A);
--LinkAccountSubtypeInvestment.INVESTMENT_401K = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_1_K);
--LinkAccountSubtypeInvestment.INVESTMENT_403B = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_3_B);
--LinkAccountSubtypeInvestment.INVESTMENT_457B = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_5_7_B);
--LinkAccountSubtypeInvestment.INVESTMENT_529 = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FIVE_2_9);
--LinkAccountSubtypeInvestment.IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.IRA);
--LinkAccountSubtypeInvestment.ISA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ISA);
--LinkAccountSubtypeInvestment.KEOGH = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.KEOGH);
--LinkAccountSubtypeInvestment.LIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LIF);
--LinkAccountSubtypeInvestment.LIRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LIRA);
--LinkAccountSubtypeInvestment.LRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LRIF);
--LinkAccountSubtypeInvestment.LRSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LRSP);
--LinkAccountSubtypeInvestment.MUTUAL_FUND = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.MUTUAL_FUND);
--LinkAccountSubtypeInvestment.NON_TAXABLE_BROKERAGE_ACCOUNT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.NON_TAXABLE_BROKERAGE_ACCOUNT);
--LinkAccountSubtypeInvestment.PENSION = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PENSION);
--LinkAccountSubtypeInvestment.PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PLAN);
--LinkAccountSubtypeInvestment.PRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PRIF);
--LinkAccountSubtypeInvestment.PROFIT_SHARING_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PROFIT_SHARING_PLAN);
--LinkAccountSubtypeInvestment.RDSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RDSP);
--LinkAccountSubtypeInvestment.RESP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RESP);
--LinkAccountSubtypeInvestment.RETIREMENT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RETIREMENT);
--LinkAccountSubtypeInvestment.RLIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RLIF);
--LinkAccountSubtypeInvestment.ROTH = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ROTH);
--LinkAccountSubtypeInvestment.ROTH_401K = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ROTH_401K);
--LinkAccountSubtypeInvestment.RRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RRIF);
--LinkAccountSubtypeInvestment.RRSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RRSP);
--LinkAccountSubtypeInvestment.SARSEP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SARSEP);
--LinkAccountSubtypeInvestment.SEP_IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SEP_IRA);
--LinkAccountSubtypeInvestment.SIMPLE_IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SIMPLE_IRA);
--LinkAccountSubtypeInvestment.SIIP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SIPP);
--LinkAccountSubtypeInvestment.STOCK_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.STOCK_PLAN);
--LinkAccountSubtypeInvestment.TFSA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.TFSA);
--LinkAccountSubtypeInvestment.THRIFT_SAVINGS_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.THRIFT_SAVINGS_PLAN);
--LinkAccountSubtypeInvestment.TRUST = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.TRUST);
--LinkAccountSubtypeInvestment.UGMA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.UGMA);
--LinkAccountSubtypeInvestment.UTMA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.UTMA);
--LinkAccountSubtypeInvestment.VARIABLE_ANNUITY = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.VARIABLE_ANNUITY);
-+LinkAccountSubtypeInvestment.ALL = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.ALL,
-+);
-+LinkAccountSubtypeInvestment.BROKERAGE = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.BROKERAGE,
-+);
-+LinkAccountSubtypeInvestment.CASH_ISA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.CASH_ISA,
-+);
-+LinkAccountSubtypeInvestment.EDUCATION_SAVINGS_ACCOUNT = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.EDUCATION_SAVINGS_ACCOUNT,
-+);
-+LinkAccountSubtypeInvestment.FIXED_ANNUNITY = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.FIXED_ANNUNITY,
-+);
-+LinkAccountSubtypeInvestment.GIC = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.GIC,
-+);
-+LinkAccountSubtypeInvestment.HEALTH_REIMBURSEMENT_ARRANGEMENT = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.HEALTH_REIMBURSEMENT_ARRANGEMENT,
-+);
-+LinkAccountSubtypeInvestment.HSA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.HSA,
-+);
-+LinkAccountSubtypeInvestment.INVESTMENT_401A = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.FOUR_0_1_A,
-+);
-+LinkAccountSubtypeInvestment.INVESTMENT_401K = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.FOUR_0_1_K,
-+);
-+LinkAccountSubtypeInvestment.INVESTMENT_403B = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.FOUR_0_3_B,
-+);
-+LinkAccountSubtypeInvestment.INVESTMENT_457B = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.FOUR_5_7_B,
-+);
-+LinkAccountSubtypeInvestment.INVESTMENT_529 = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.FIVE_2_9,
-+);
-+LinkAccountSubtypeInvestment.IRA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.IRA,
-+);
-+LinkAccountSubtypeInvestment.ISA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.ISA,
-+);
-+LinkAccountSubtypeInvestment.KEOGH = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.KEOGH,
-+);
-+LinkAccountSubtypeInvestment.LIF = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.LIF,
-+);
-+LinkAccountSubtypeInvestment.LIRA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.LIRA,
-+);
-+LinkAccountSubtypeInvestment.LRIF = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.LRIF,
-+);
-+LinkAccountSubtypeInvestment.LRSP = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.LRSP,
-+);
-+LinkAccountSubtypeInvestment.MUTUAL_FUND = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.MUTUAL_FUND,
-+);
-+LinkAccountSubtypeInvestment.NON_TAXABLE_BROKERAGE_ACCOUNT = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.NON_TAXABLE_BROKERAGE_ACCOUNT,
-+);
-+LinkAccountSubtypeInvestment.PENSION = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.PENSION,
-+);
-+LinkAccountSubtypeInvestment.PLAN = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.PLAN,
-+);
-+LinkAccountSubtypeInvestment.PRIF = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.PRIF,
-+);
-+LinkAccountSubtypeInvestment.PROFIT_SHARING_PLAN = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.PROFIT_SHARING_PLAN,
-+);
-+LinkAccountSubtypeInvestment.RDSP = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.RDSP,
-+);
-+LinkAccountSubtypeInvestment.RESP = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.RESP,
-+);
-+LinkAccountSubtypeInvestment.RETIREMENT = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.RETIREMENT,
-+);
-+LinkAccountSubtypeInvestment.RLIF = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.RLIF,
-+);
-+LinkAccountSubtypeInvestment.ROTH = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.ROTH,
-+);
-+LinkAccountSubtypeInvestment.ROTH_401K = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.ROTH_401K,
-+);
-+LinkAccountSubtypeInvestment.RRIF = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.RRIF,
-+);
-+LinkAccountSubtypeInvestment.RRSP = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.RRSP,
-+);
-+LinkAccountSubtypeInvestment.SARSEP = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.SARSEP,
-+);
-+LinkAccountSubtypeInvestment.SEP_IRA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.SEP_IRA,
-+);
-+LinkAccountSubtypeInvestment.SIMPLE_IRA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.SIMPLE_IRA,
-+);
-+LinkAccountSubtypeInvestment.SIIP = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.SIPP,
-+);
-+LinkAccountSubtypeInvestment.STOCK_PLAN = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.STOCK_PLAN,
-+);
-+LinkAccountSubtypeInvestment.TFSA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.TFSA,
-+);
-+LinkAccountSubtypeInvestment.THRIFT_SAVINGS_PLAN = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.THRIFT_SAVINGS_PLAN,
-+);
-+LinkAccountSubtypeInvestment.TRUST = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.TRUST,
-+);
-+LinkAccountSubtypeInvestment.UGMA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.UGMA,
-+);
-+LinkAccountSubtypeInvestment.UTMA = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.UTMA,
-+);
-+LinkAccountSubtypeInvestment.VARIABLE_ANNUITY = new LinkAccountSubtypeInvestment(
-+ LinkAccountType.INVESTMENT,
-+ LinkAccountSubtypes.VARIABLE_ANNUITY,
-+);
- export class LinkAccountSubtypeLoan {
-- constructor(type, subtype) {
-- this.type = type;
-- this.subtype = subtype;
-- }
-+ constructor(type, subtype) {
-+ this.type = type;
-+ this.subtype = subtype;
-+ }
- }
--LinkAccountSubtypeLoan.ALL = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.ALL);
--LinkAccountSubtypeLoan.AUTO = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.AUTO);
--LinkAccountSubtypeLoan.BUSINESS = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.BUSINESS);
--LinkAccountSubtypeLoan.COMMERCIAL = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.COMMERCIAL);
--LinkAccountSubtypeLoan.CONSTRUCTION = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.CONSTRUCTION);
--LinkAccountSubtypeLoan.CONSUMER = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.CONSUMER);
--LinkAccountSubtypeLoan.HOME_EQUITY = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.HOME_EQUITY);
--LinkAccountSubtypeLoan.LINE_OF_CREDIT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.LINE_OF_CREDIT);
--LinkAccountSubtypeLoan.LOAN = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.LOAN);
--LinkAccountSubtypeLoan.MORTGAGE = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.MORTGAGE);
--LinkAccountSubtypeLoan.OVERDRAFT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.OVERDRAFT);
--LinkAccountSubtypeLoan.STUDENT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.STUDENT);
-+LinkAccountSubtypeLoan.ALL = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.ALL,
-+);
-+LinkAccountSubtypeLoan.AUTO = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.AUTO,
-+);
-+LinkAccountSubtypeLoan.BUSINESS = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.BUSINESS,
-+);
-+LinkAccountSubtypeLoan.COMMERCIAL = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.COMMERCIAL,
-+);
-+LinkAccountSubtypeLoan.CONSTRUCTION = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.CONSTRUCTION,
-+);
-+LinkAccountSubtypeLoan.CONSUMER = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.CONSUMER,
-+);
-+LinkAccountSubtypeLoan.HOME_EQUITY = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.HOME_EQUITY,
-+);
-+LinkAccountSubtypeLoan.LINE_OF_CREDIT = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.LINE_OF_CREDIT,
-+);
-+LinkAccountSubtypeLoan.LOAN = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.LOAN,
-+);
-+LinkAccountSubtypeLoan.MORTGAGE = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.MORTGAGE,
-+);
-+LinkAccountSubtypeLoan.OVERDRAFT = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.OVERDRAFT,
-+);
-+LinkAccountSubtypeLoan.STUDENT = new LinkAccountSubtypeLoan(
-+ LinkAccountType.CREDIT,
-+ LinkAccountSubtypes.STUDENT,
-+);
- export class LinkAccountSubtypeUnknown {
-- constructor(type, subtype) {
-- this.type = type;
-- this.subtype = subtype;
-- }
-+ constructor(type, subtype) {
-+ this.type = type;
-+ this.subtype = subtype;
-+ }
- }
- export var LinkAccountVerificationStatus;
--(function (LinkAccountVerificationStatus) {
-- LinkAccountVerificationStatus["PENDING_AUTOMATIC_VERIFICATION"] = "pending_automatic_verification";
-- LinkAccountVerificationStatus["PENDING_MANUAL_VERIFICATION"] = "pending_manual_verification";
-- LinkAccountVerificationStatus["MANUALLY_VERIFIED"] = "manually_verified";
-+(function(LinkAccountVerificationStatus) {
-+ LinkAccountVerificationStatus['PENDING_AUTOMATIC_VERIFICATION'] =
-+ 'pending_automatic_verification';
-+ LinkAccountVerificationStatus['PENDING_MANUAL_VERIFICATION'] =
-+ 'pending_manual_verification';
-+ LinkAccountVerificationStatus['MANUALLY_VERIFIED'] = 'manually_verified';
- })(LinkAccountVerificationStatus || (LinkAccountVerificationStatus = {}));
- export var LinkExitMetadataStatus;
--(function (LinkExitMetadataStatus) {
-- LinkExitMetadataStatus["CONNECTED"] = "connected";
-- LinkExitMetadataStatus["CHOOSE_DEVICE"] = "choose_device";
-- LinkExitMetadataStatus["REQUIRES_ACCOUNT_SELECTION"] = "requires_account_selection";
-- LinkExitMetadataStatus["REQUIRES_CODE"] = "requires_code";
-- LinkExitMetadataStatus["REQUIRES_CREDENTIALS"] = "requires_credentials";
-- LinkExitMetadataStatus["REQUIRES_EXTERNAL_ACTION"] = "requires_external_action";
-- LinkExitMetadataStatus["REQUIRES_OAUTH"] = "requires_oauth";
-- LinkExitMetadataStatus["REQUIRES_QUESTIONS"] = "requires_questions";
-- LinkExitMetadataStatus["REQUIRES_RECAPTCHA"] = "requires_recaptcha";
-- LinkExitMetadataStatus["REQUIRES_SELECTIONS"] = "requires_selections";
-- LinkExitMetadataStatus["REQUIRES_DEPOSIT_SWITCH_ALLOCATION_CONFIGURATION"] = "requires_deposit_switch_allocation_configuration";
-- LinkExitMetadataStatus["REQUIRES_DEPOSIT_SWITCH_ALLOCATION_SELECTION"] = "requires_deposit_switch_allocation_selection";
-+(function(LinkExitMetadataStatus) {
-+ LinkExitMetadataStatus['CONNECTED'] = 'connected';
-+ LinkExitMetadataStatus['CHOOSE_DEVICE'] = 'choose_device';
-+ LinkExitMetadataStatus['REQUIRES_ACCOUNT_SELECTION'] =
-+ 'requires_account_selection';
-+ LinkExitMetadataStatus['REQUIRES_CODE'] = 'requires_code';
-+ LinkExitMetadataStatus['REQUIRES_CREDENTIALS'] = 'requires_credentials';
-+ LinkExitMetadataStatus['REQUIRES_EXTERNAL_ACTION'] =
-+ 'requires_external_action';
-+ LinkExitMetadataStatus['REQUIRES_OAUTH'] = 'requires_oauth';
-+ LinkExitMetadataStatus['REQUIRES_QUESTIONS'] = 'requires_questions';
-+ LinkExitMetadataStatus['REQUIRES_RECAPTCHA'] = 'requires_recaptcha';
-+ LinkExitMetadataStatus['REQUIRES_SELECTIONS'] = 'requires_selections';
-+ LinkExitMetadataStatus['REQUIRES_DEPOSIT_SWITCH_ALLOCATION_CONFIGURATION'] =
-+ 'requires_deposit_switch_allocation_configuration';
-+ LinkExitMetadataStatus['REQUIRES_DEPOSIT_SWITCH_ALLOCATION_SELECTION'] =
-+ 'requires_deposit_switch_allocation_selection';
- })(LinkExitMetadataStatus || (LinkExitMetadataStatus = {}));
- export var LinkErrorCode;
--(function (LinkErrorCode) {
-- // ITEM_ERROR
-- LinkErrorCode["INVALID_CREDENTIALS"] = "INVALID_CREDENTIALS";
-- LinkErrorCode["INVALID_MFA"] = "INVALID_MFA";
-- LinkErrorCode["ITEM_LOGIN_REQUIRED"] = "ITEM_LOGIN_REQUIRED";
-- LinkErrorCode["INSUFFICIENT_CREDENTIALS"] = "INSUFFICIENT_CREDENTIALS";
-- LinkErrorCode["ITEM_LOCKED"] = "ITEM_LOCKED";
-- LinkErrorCode["USER_SETUP_REQUIRED"] = "USER_SETUP_REQUIRED";
-- LinkErrorCode["MFA_NOT_SUPPORTED"] = "MFA_NOT_SUPPORTED";
-- LinkErrorCode["INVALID_SEND_METHOD"] = "INVALID_SEND_METHOD";
-- LinkErrorCode["NO_ACCOUNTS"] = "NO_ACCOUNTS";
-- LinkErrorCode["ITEM_NOT_SUPPORTED"] = "ITEM_NOT_SUPPORTED";
-- LinkErrorCode["TOO_MANY_VERIFICATION_ATTEMPTS"] = "TOO_MANY_VERIFICATION_ATTEMPTS";
-- LinkErrorCode["INVALD_UPDATED_USERNAME"] = "INVALD_UPDATED_USERNAME";
-- LinkErrorCode["INVALID_UPDATED_USERNAME"] = "INVALID_UPDATED_USERNAME";
-- LinkErrorCode["ITEM_NO_ERROR"] = "ITEM_NO_ERROR";
-- LinkErrorCode["item_no_error"] = "item-no-error";
-- LinkErrorCode["NO_AUTH_ACCOUNTS"] = "NO_AUTH_ACCOUNTS";
-- LinkErrorCode["NO_INVESTMENT_ACCOUNTS"] = "NO_INVESTMENT_ACCOUNTS";
-- LinkErrorCode["NO_INVESTMENT_AUTH_ACCOUNTS"] = "NO_INVESTMENT_AUTH_ACCOUNTS";
-- LinkErrorCode["NO_LIABILITY_ACCOUNTS"] = "NO_LIABILITY_ACCOUNTS";
-- LinkErrorCode["PRODUCTS_NOT_SUPPORTED"] = "PRODUCTS_NOT_SUPPORTED";
-- LinkErrorCode["ITEM_NOT_FOUND"] = "ITEM_NOT_FOUND";
-- LinkErrorCode["ITEM_PRODUCT_NOT_READY"] = "ITEM_PRODUCT_NOT_READY";
-- // INSTITUTION_ERROR
-- LinkErrorCode["INSTITUTION_DOWN"] = "INSTITUTION_DOWN";
-- LinkErrorCode["INSTITUTION_NOT_RESPONDING"] = "INSTITUTION_NOT_RESPONDING";
-- LinkErrorCode["INSTITUTION_NOT_AVAILABLE"] = "INSTITUTION_NOT_AVAILABLE";
-- LinkErrorCode["INSTITUTION_NO_LONGER_SUPPORTED"] = "INSTITUTION_NO_LONGER_SUPPORTED";
-- // API_ERROR
-- LinkErrorCode["INTERNAL_SERVER_ERROR"] = "INTERNAL_SERVER_ERROR";
-- LinkErrorCode["PLANNED_MAINTENANCE"] = "PLANNED_MAINTENANCE";
-- // ASSET_REPORT_ERROR
-- LinkErrorCode["PRODUCT_NOT_ENABLED"] = "PRODUCT_NOT_ENABLED";
-- LinkErrorCode["DATA_UNAVAILABLE"] = "DATA_UNAVAILABLE";
-- LinkErrorCode["ASSET_PRODUCT_NOT_READY"] = "ASSET_PRODUCT_NOT_READY";
-- LinkErrorCode["ASSET_REPORT_GENERATION_FAILED"] = "ASSET_REPORT_GENERATION_FAILED";
-- LinkErrorCode["INVALID_PARENT"] = "INVALID_PARENT";
-- LinkErrorCode["INSIGHTS_NOT_ENABLED"] = "INSIGHTS_NOT_ENABLED";
-- LinkErrorCode["INSIGHTS_PREVIOUSLY_NOT_ENABLED"] = "INSIGHTS_PREVIOUSLY_NOT_ENABLED";
-- // BANK_TRANSFER_ERROR
-- LinkErrorCode["BANK_TRANSFER_LIMIT_EXCEEDED"] = "BANK_TRANSFER_LIMIT_EXCEEDED";
-- LinkErrorCode["BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT"] = "BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT";
-- LinkErrorCode["BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT"] = "BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT";
-- LinkErrorCode["BANK_TRANSFER_ACCOUNT_BLOCKED"] = "BANK_TRANSFER_ACCOUNT_BLOCKED";
-- LinkErrorCode["BANK_TRANSFER_INSUFFICIENT_FUNDS"] = "BANK_TRANSFER_INSUFFICIENT_FUNDS";
-- LinkErrorCode["BANK_TRANSFER_NOT_CANCELLABLE"] = "BANK_TRANSFER_NOT_CANCELLABLE";
-- LinkErrorCode["BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE"] = "BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE";
-- LinkErrorCode["BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT"] = "BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT";
-- // SANDBOX_ERROR
-- LinkErrorCode["SANDBOX_PRODUCT_NOT_ENABLED"] = "SANDBOX_PRODUCT_NOT_ENABLED";
-- LinkErrorCode["SANDBOX_WEBHOOK_INVALID"] = "SANDBOX_WEBHOOK_INVALID";
-- LinkErrorCode["SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID"] = "SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID";
-- // INVALID_REQUEST
-- LinkErrorCode["MISSING_FIELDS"] = "MISSING_FIELDS";
-- LinkErrorCode["UNKNOWN_FIELDS"] = "UNKNOWN_FIELDS";
-- LinkErrorCode["INVALID_FIELD"] = "INVALID_FIELD";
-- LinkErrorCode["INCOMPATIBLE_API_VERSION"] = "INCOMPATIBLE_API_VERSION";
-- LinkErrorCode["INVALID_BODY"] = "INVALID_BODY";
-- LinkErrorCode["INVALID_HEADERS"] = "INVALID_HEADERS";
-- LinkErrorCode["NOT_FOUND"] = "NOT_FOUND";
-- LinkErrorCode["NO_LONGER_AVAILABLE"] = "NO_LONGER_AVAILABLE";
-- LinkErrorCode["SANDBOX_ONLY"] = "SANDBOX_ONLY";
-- LinkErrorCode["INVALID_ACCOUNT_NUMBER"] = "INVALID_ACCOUNT_NUMBER";
-- // INVALID_INPUT
-- // From above ITEM_LOGIN_REQUIRED = "INVALID_CREDENTIALS",
-- LinkErrorCode["INCORRECT_DEPOSIT_AMOUNTS"] = "INCORRECT_DEPOSIT_AMOUNTS";
-- LinkErrorCode["UNAUTHORIZED_ENVIRONMENT"] = "UNAUTHORIZED_ENVIRONMENT";
-- LinkErrorCode["INVALID_PRODUCT"] = "INVALID_PRODUCT";
-- LinkErrorCode["UNAUTHORIZED_ROUTE_ACCESS"] = "UNAUTHORIZED_ROUTE_ACCESS";
-- LinkErrorCode["DIRECT_INTEGRATION_NOT_ENABLED"] = "DIRECT_INTEGRATION_NOT_ENABLED";
-- LinkErrorCode["INVALID_API_KEYS"] = "INVALID_API_KEYS";
-- LinkErrorCode["INVALID_ACCESS_TOKEN"] = "INVALID_ACCESS_TOKEN";
-- LinkErrorCode["INVALID_PUBLIC_TOKEN"] = "INVALID_PUBLIC_TOKEN";
-- LinkErrorCode["INVALID_LINK_TOKEN"] = "INVALID_LINK_TOKEN";
-- LinkErrorCode["INVALID_PROCESSOR_TOKEN"] = "INVALID_PROCESSOR_TOKEN";
-- LinkErrorCode["INVALID_AUDIT_COPY_TOKEN"] = "INVALID_AUDIT_COPY_TOKEN";
-- LinkErrorCode["INVALID_ACCOUNT_ID"] = "INVALID_ACCOUNT_ID";
-- LinkErrorCode["MICRODEPOSITS_ALREADY_VERIFIED"] = "MICRODEPOSITS_ALREADY_VERIFIED";
-- // INVALID_RESULT
-- LinkErrorCode["PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA"] = "PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA";
-- // RATE_LIMIT_EXCEEDED
-- LinkErrorCode["ACCOUNTS_LIMIT"] = "ACCOUNTS_LIMIT";
-- LinkErrorCode["ADDITION_LIMIT"] = "ADDITION_LIMIT";
-- LinkErrorCode["AUTH_LIMIT"] = "AUTH_LIMIT";
-- LinkErrorCode["BALANCE_LIMIT"] = "BALANCE_LIMIT";
-- LinkErrorCode["IDENTITY_LIMIT"] = "IDENTITY_LIMIT";
-- LinkErrorCode["ITEM_GET_LIMIT"] = "ITEM_GET_LIMIT";
-- LinkErrorCode["RATE_LIMIT"] = "RATE_LIMIT";
-- LinkErrorCode["TRANSACTIONS_LIMIT"] = "TRANSACTIONS_LIMIT";
-- // RECAPTCHA_ERROR
-- LinkErrorCode["RECAPTCHA_REQUIRED"] = "RECAPTCHA_REQUIRED";
-- LinkErrorCode["RECAPTCHA_BAD"] = "RECAPTCHA_BAD";
-- // OAUTH_ERROR
-- LinkErrorCode["INCORRECT_OAUTH_NONCE"] = "INCORRECT_OAUTH_NONCE";
-- LinkErrorCode["OAUTH_STATE_ID_ALREADY_PROCESSED"] = "OAUTH_STATE_ID_ALREADY_PROCESSED";
-+(function(LinkErrorCode) {
-+ // ITEM_ERROR
-+ LinkErrorCode['INVALID_CREDENTIALS'] = 'INVALID_CREDENTIALS';
-+ LinkErrorCode['INVALID_MFA'] = 'INVALID_MFA';
-+ LinkErrorCode['ITEM_LOGIN_REQUIRED'] = 'ITEM_LOGIN_REQUIRED';
-+ LinkErrorCode['INSUFFICIENT_CREDENTIALS'] = 'INSUFFICIENT_CREDENTIALS';
-+ LinkErrorCode['ITEM_LOCKED'] = 'ITEM_LOCKED';
-+ LinkErrorCode['USER_SETUP_REQUIRED'] = 'USER_SETUP_REQUIRED';
-+ LinkErrorCode['MFA_NOT_SUPPORTED'] = 'MFA_NOT_SUPPORTED';
-+ LinkErrorCode['INVALID_SEND_METHOD'] = 'INVALID_SEND_METHOD';
-+ LinkErrorCode['NO_ACCOUNTS'] = 'NO_ACCOUNTS';
-+ LinkErrorCode['ITEM_NOT_SUPPORTED'] = 'ITEM_NOT_SUPPORTED';
-+ LinkErrorCode['TOO_MANY_VERIFICATION_ATTEMPTS'] =
-+ 'TOO_MANY_VERIFICATION_ATTEMPTS';
-+ LinkErrorCode['INVALD_UPDATED_USERNAME'] = 'INVALD_UPDATED_USERNAME';
-+ LinkErrorCode['INVALID_UPDATED_USERNAME'] = 'INVALID_UPDATED_USERNAME';
-+ LinkErrorCode['ITEM_NO_ERROR'] = 'ITEM_NO_ERROR';
-+ LinkErrorCode['item_no_error'] = 'item-no-error';
-+ LinkErrorCode['NO_AUTH_ACCOUNTS'] = 'NO_AUTH_ACCOUNTS';
-+ LinkErrorCode['NO_INVESTMENT_ACCOUNTS'] = 'NO_INVESTMENT_ACCOUNTS';
-+ LinkErrorCode['NO_INVESTMENT_AUTH_ACCOUNTS'] = 'NO_INVESTMENT_AUTH_ACCOUNTS';
-+ LinkErrorCode['NO_LIABILITY_ACCOUNTS'] = 'NO_LIABILITY_ACCOUNTS';
-+ LinkErrorCode['PRODUCTS_NOT_SUPPORTED'] = 'PRODUCTS_NOT_SUPPORTED';
-+ LinkErrorCode['ITEM_NOT_FOUND'] = 'ITEM_NOT_FOUND';
-+ LinkErrorCode['ITEM_PRODUCT_NOT_READY'] = 'ITEM_PRODUCT_NOT_READY';
-+ // INSTITUTION_ERROR
-+ LinkErrorCode['INSTITUTION_DOWN'] = 'INSTITUTION_DOWN';
-+ LinkErrorCode['INSTITUTION_NOT_RESPONDING'] = 'INSTITUTION_NOT_RESPONDING';
-+ LinkErrorCode['INSTITUTION_NOT_AVAILABLE'] = 'INSTITUTION_NOT_AVAILABLE';
-+ LinkErrorCode['INSTITUTION_NO_LONGER_SUPPORTED'] =
-+ 'INSTITUTION_NO_LONGER_SUPPORTED';
-+ // API_ERROR
-+ LinkErrorCode['INTERNAL_SERVER_ERROR'] = 'INTERNAL_SERVER_ERROR';
-+ LinkErrorCode['PLANNED_MAINTENANCE'] = 'PLANNED_MAINTENANCE';
-+ // ASSET_REPORT_ERROR
-+ LinkErrorCode['PRODUCT_NOT_ENABLED'] = 'PRODUCT_NOT_ENABLED';
-+ LinkErrorCode['DATA_UNAVAILABLE'] = 'DATA_UNAVAILABLE';
-+ LinkErrorCode['ASSET_PRODUCT_NOT_READY'] = 'ASSET_PRODUCT_NOT_READY';
-+ LinkErrorCode['ASSET_REPORT_GENERATION_FAILED'] =
-+ 'ASSET_REPORT_GENERATION_FAILED';
-+ LinkErrorCode['INVALID_PARENT'] = 'INVALID_PARENT';
-+ LinkErrorCode['INSIGHTS_NOT_ENABLED'] = 'INSIGHTS_NOT_ENABLED';
-+ LinkErrorCode['INSIGHTS_PREVIOUSLY_NOT_ENABLED'] =
-+ 'INSIGHTS_PREVIOUSLY_NOT_ENABLED';
-+ // BANK_TRANSFER_ERROR
-+ LinkErrorCode['BANK_TRANSFER_LIMIT_EXCEEDED'] =
-+ 'BANK_TRANSFER_LIMIT_EXCEEDED';
-+ LinkErrorCode['BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT'] =
-+ 'BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT';
-+ LinkErrorCode['BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT'] =
-+ 'BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT';
-+ LinkErrorCode['BANK_TRANSFER_ACCOUNT_BLOCKED'] =
-+ 'BANK_TRANSFER_ACCOUNT_BLOCKED';
-+ LinkErrorCode['BANK_TRANSFER_INSUFFICIENT_FUNDS'] =
-+ 'BANK_TRANSFER_INSUFFICIENT_FUNDS';
-+ LinkErrorCode['BANK_TRANSFER_NOT_CANCELLABLE'] =
-+ 'BANK_TRANSFER_NOT_CANCELLABLE';
-+ LinkErrorCode['BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE'] =
-+ 'BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE';
-+ LinkErrorCode['BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT'] =
-+ 'BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT';
-+ // SANDBOX_ERROR
-+ LinkErrorCode['SANDBOX_PRODUCT_NOT_ENABLED'] = 'SANDBOX_PRODUCT_NOT_ENABLED';
-+ LinkErrorCode['SANDBOX_WEBHOOK_INVALID'] = 'SANDBOX_WEBHOOK_INVALID';
-+ LinkErrorCode['SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID'] =
-+ 'SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID';
-+ // INVALID_REQUEST
-+ LinkErrorCode['MISSING_FIELDS'] = 'MISSING_FIELDS';
-+ LinkErrorCode['UNKNOWN_FIELDS'] = 'UNKNOWN_FIELDS';
-+ LinkErrorCode['INVALID_FIELD'] = 'INVALID_FIELD';
-+ LinkErrorCode['INCOMPATIBLE_API_VERSION'] = 'INCOMPATIBLE_API_VERSION';
-+ LinkErrorCode['INVALID_BODY'] = 'INVALID_BODY';
-+ LinkErrorCode['INVALID_HEADERS'] = 'INVALID_HEADERS';
-+ LinkErrorCode['NOT_FOUND'] = 'NOT_FOUND';
-+ LinkErrorCode['NO_LONGER_AVAILABLE'] = 'NO_LONGER_AVAILABLE';
-+ LinkErrorCode['SANDBOX_ONLY'] = 'SANDBOX_ONLY';
-+ LinkErrorCode['INVALID_ACCOUNT_NUMBER'] = 'INVALID_ACCOUNT_NUMBER';
-+ // INVALID_INPUT
-+ // From above ITEM_LOGIN_REQUIRED = "INVALID_CREDENTIALS",
-+ LinkErrorCode['INCORRECT_DEPOSIT_AMOUNTS'] = 'INCORRECT_DEPOSIT_AMOUNTS';
-+ LinkErrorCode['UNAUTHORIZED_ENVIRONMENT'] = 'UNAUTHORIZED_ENVIRONMENT';
-+ LinkErrorCode['INVALID_PRODUCT'] = 'INVALID_PRODUCT';
-+ LinkErrorCode['UNAUTHORIZED_ROUTE_ACCESS'] = 'UNAUTHORIZED_ROUTE_ACCESS';
-+ LinkErrorCode['DIRECT_INTEGRATION_NOT_ENABLED'] =
-+ 'DIRECT_INTEGRATION_NOT_ENABLED';
-+ LinkErrorCode['INVALID_API_KEYS'] = 'INVALID_API_KEYS';
-+ LinkErrorCode['INVALID_ACCESS_TOKEN'] = 'INVALID_ACCESS_TOKEN';
-+ LinkErrorCode['INVALID_PUBLIC_TOKEN'] = 'INVALID_PUBLIC_TOKEN';
-+ LinkErrorCode['INVALID_LINK_TOKEN'] = 'INVALID_LINK_TOKEN';
-+ LinkErrorCode['INVALID_PROCESSOR_TOKEN'] = 'INVALID_PROCESSOR_TOKEN';
-+ LinkErrorCode['INVALID_AUDIT_COPY_TOKEN'] = 'INVALID_AUDIT_COPY_TOKEN';
-+ LinkErrorCode['INVALID_ACCOUNT_ID'] = 'INVALID_ACCOUNT_ID';
-+ LinkErrorCode['MICRODEPOSITS_ALREADY_VERIFIED'] =
-+ 'MICRODEPOSITS_ALREADY_VERIFIED';
-+ // INVALID_RESULT
-+ LinkErrorCode['PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA'] =
-+ 'PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA';
-+ // RATE_LIMIT_EXCEEDED
-+ LinkErrorCode['ACCOUNTS_LIMIT'] = 'ACCOUNTS_LIMIT';
-+ LinkErrorCode['ADDITION_LIMIT'] = 'ADDITION_LIMIT';
-+ LinkErrorCode['AUTH_LIMIT'] = 'AUTH_LIMIT';
-+ LinkErrorCode['BALANCE_LIMIT'] = 'BALANCE_LIMIT';
-+ LinkErrorCode['IDENTITY_LIMIT'] = 'IDENTITY_LIMIT';
-+ LinkErrorCode['ITEM_GET_LIMIT'] = 'ITEM_GET_LIMIT';
-+ LinkErrorCode['RATE_LIMIT'] = 'RATE_LIMIT';
-+ LinkErrorCode['TRANSACTIONS_LIMIT'] = 'TRANSACTIONS_LIMIT';
-+ // RECAPTCHA_ERROR
-+ LinkErrorCode['RECAPTCHA_REQUIRED'] = 'RECAPTCHA_REQUIRED';
-+ LinkErrorCode['RECAPTCHA_BAD'] = 'RECAPTCHA_BAD';
-+ // OAUTH_ERROR
-+ LinkErrorCode['INCORRECT_OAUTH_NONCE'] = 'INCORRECT_OAUTH_NONCE';
-+ LinkErrorCode['OAUTH_STATE_ID_ALREADY_PROCESSED'] =
-+ 'OAUTH_STATE_ID_ALREADY_PROCESSED';
- })(LinkErrorCode || (LinkErrorCode = {}));
- export var LinkErrorType;
--(function (LinkErrorType) {
-- LinkErrorType["BANK_TRANSFER"] = "BANK_TRANSFER_ERROR";
-- LinkErrorType["INVALID_REQUEST"] = "INVALID_REQUEST";
-- LinkErrorType["INVALID_RESULT"] = "INVALID_RESULT";
-- LinkErrorType["INVALID_INPUT"] = "INVALID_INPUT";
-- LinkErrorType["INSTITUTION_ERROR"] = "INSTITUTION_ERROR";
-- LinkErrorType["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED";
-- LinkErrorType["API_ERROR"] = "API_ERROR";
-- LinkErrorType["ITEM_ERROR"] = "ITEM_ERROR";
-- LinkErrorType["AUTH_ERROR"] = "AUTH_ERROR";
-- LinkErrorType["ASSET_REPORT_ERROR"] = "ASSET_REPORT_ERROR";
-- LinkErrorType["SANDBOX_ERROR"] = "SANDBOX_ERROR";
-- LinkErrorType["RECAPTCHA_ERROR"] = "RECAPTCHA_ERROR";
-- LinkErrorType["OAUTH_ERROR"] = "OAUTH_ERROR";
-+(function(LinkErrorType) {
-+ LinkErrorType['BANK_TRANSFER'] = 'BANK_TRANSFER_ERROR';
-+ LinkErrorType['INVALID_REQUEST'] = 'INVALID_REQUEST';
-+ LinkErrorType['INVALID_RESULT'] = 'INVALID_RESULT';
-+ LinkErrorType['INVALID_INPUT'] = 'INVALID_INPUT';
-+ LinkErrorType['INSTITUTION_ERROR'] = 'INSTITUTION_ERROR';
-+ LinkErrorType['RATE_LIMIT_EXCEEDED'] = 'RATE_LIMIT_EXCEEDED';
-+ LinkErrorType['API_ERROR'] = 'API_ERROR';
-+ LinkErrorType['ITEM_ERROR'] = 'ITEM_ERROR';
-+ LinkErrorType['AUTH_ERROR'] = 'AUTH_ERROR';
-+ LinkErrorType['ASSET_REPORT_ERROR'] = 'ASSET_REPORT_ERROR';
-+ LinkErrorType['SANDBOX_ERROR'] = 'SANDBOX_ERROR';
-+ LinkErrorType['RECAPTCHA_ERROR'] = 'RECAPTCHA_ERROR';
-+ LinkErrorType['OAUTH_ERROR'] = 'OAUTH_ERROR';
- })(LinkErrorType || (LinkErrorType = {}));
- export var LinkEventName;
--(function (LinkEventName) {
-- LinkEventName["BANK_INCOME_INSIGHTS_COMPLETED"] = "BANK_INCOME_INSIGHTS_COMPLETED";
-- LinkEventName["CLOSE_OAUTH"] = "CLOSE_OAUTH";
-- LinkEventName["ERROR"] = "ERROR";
-- LinkEventName["EXIT"] = "EXIT";
-- LinkEventName["FAIL_OAUTH"] = "FAIL_OAUTH";
-- LinkEventName["HANDOFF"] = "HANDOFF";
-- LinkEventName["IDENTITY_VERIFICATION_START_STEP"] = "IDENTITY_VERIFICATION_START_STEP";
-- LinkEventName["IDENTITY_VERIFICATION_PASS_STEP"] = "IDENTITY_VERIFICATION_PASS_STEP";
-- LinkEventName["IDENTITY_VERIFICATION_FAIL_STEP"] = "IDENTITY_VERIFICATION_FAIL_STEP";
-- LinkEventName["IDENTITY_VERIFICATION_PENDING_REVIEW_STEP"] = "IDENTITY_VERIFICATION_PENDING_REVIEW_STEP";
-- LinkEventName["IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION"] = "IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION";
-- LinkEventName["IDENTITY_VERIFICATION_CREATE_SESSION"] = "IDENTITY_VERIFICATION_CREATE_SESSION";
-- LinkEventName["IDENTITY_VERIFICATION_RESUME_SESSION"] = "IDENTITY_VERIFICATION_RESUME_SESSION";
-- LinkEventName["IDENTITY_VERIFICATION_PASS_SESSION"] = "IDENTITY_VERIFICATION_PASS_SESSION";
-- LinkEventName["IDENTITY_VERIFICATION_FAIL_SESSION"] = "IDENTITY_VERIFICATION_FAIL_SESSION";
-- LinkEventName["IDENTITY_VERIFICATION_OPEN_UI"] = "IDENTITY_VERIFICATION_OPEN_UI";
-- LinkEventName["IDENTITY_VERIFICATION_RESUME_UI"] = "IDENTITY_VERIFICATION_RESUME_UI";
-- LinkEventName["IDENTITY_VERIFICATION_CLOSE_UI"] = "IDENTITY_VERIFICATION_CLOSE_UI";
-- LinkEventName["MATCHED_CONSENT"] = "MATCHED_CONSENT";
-- LinkEventName["MATCHED_SELECT_INSTITUTION"] = "MATCHED_SELECT_INSTITUTION";
-- LinkEventName["MATCHED_SELECT_VERIFY_METHOD"] = "MATCHED_SELECT_VERIFY_METHOD";
-- LinkEventName["OPEN"] = "OPEN";
-- LinkEventName["OPEN_MY_PLAID"] = "OPEN_MY_PLAID";
-- LinkEventName["OPEN_OAUTH"] = "OPEN_OAUTH";
-- LinkEventName["SEARCH_INSTITUTION"] = "SEARCH_INSTITUTION";
-- LinkEventName["SELECT_DEGRADED_INSTITUTION"] = "SELECT_DEGRADED_INSTITUTION";
-- LinkEventName["SELECT_DOWN_INSTITUTION"] = "SELECT_DOWN_INSTITUTION";
-- LinkEventName["SELECT_FILTERED_INSTITUTION"] = "SELECT_FILTERED_INSTITUTION";
-- LinkEventName["SELECT_INSTITUTION"] = "SELECT_INSTITUTION";
-- LinkEventName["SELECT_BRAND"] = "SELECT_BRAND";
-- LinkEventName["SELECT_AUTH_TYPE"] = "SELECT_AUTH_TYPE";
-- LinkEventName["SUBMIT_ACCOUNT_NUMBER"] = "SUBMIT_ACCOUNT_NUMBER";
-- LinkEventName["SUBMIT_DOCUMENTS"] = "SUBMIT_DOCUMENTS";
-- LinkEventName["SUBMIT_DOCUMENTS_SUCCESS"] = "SUBMIT_DOCUMENTS_SUCCESS";
-- LinkEventName["SUBMIT_DOCUMENTS_ERROR"] = "SUBMIT_DOCUMENTS_ERROR";
-- LinkEventName["SUBMIT_ROUTING_NUMBER"] = "SUBMIT_ROUTING_NUMBER";
-- LinkEventName["VIEW_DATA_TYPES"] = "VIEW_DATA_TYPES";
-- LinkEventName["SUBMIT_PHONE"] = "SUBMIT_PHONE";
-- LinkEventName["SKIP_SUBMIT_PHONE"] = "SKIP_SUBMIT_PHONE";
-- LinkEventName["VERIFY_PHONE"] = "VERIFY_PHONE";
-- LinkEventName["SUBMIT_CREDENTIALS"] = "SUBMIT_CREDENTIALS";
-- LinkEventName["SUBMIT_MFA"] = "SUBMIT_MFA";
-- LinkEventName["TRANSITION_VIEW"] = "TRANSITION_VIEW";
-- LinkEventName["CONNECT_NEW_INSTITUTION"] = "CONNECT_NEW_INSTITUTION";
-+(function(LinkEventName) {
-+ LinkEventName['BANK_INCOME_INSIGHTS_COMPLETED'] =
-+ 'BANK_INCOME_INSIGHTS_COMPLETED';
-+ LinkEventName['CLOSE_OAUTH'] = 'CLOSE_OAUTH';
-+ LinkEventName['ERROR'] = 'ERROR';
-+ LinkEventName['EXIT'] = 'EXIT';
-+ LinkEventName['FAIL_OAUTH'] = 'FAIL_OAUTH';
-+ LinkEventName['HANDOFF'] = 'HANDOFF';
-+ LinkEventName['IDENTITY_VERIFICATION_START_STEP'] =
-+ 'IDENTITY_VERIFICATION_START_STEP';
-+ LinkEventName['IDENTITY_VERIFICATION_PASS_STEP'] =
-+ 'IDENTITY_VERIFICATION_PASS_STEP';
-+ LinkEventName['IDENTITY_VERIFICATION_FAIL_STEP'] =
-+ 'IDENTITY_VERIFICATION_FAIL_STEP';
-+ LinkEventName['IDENTITY_VERIFICATION_PENDING_REVIEW_STEP'] =
-+ 'IDENTITY_VERIFICATION_PENDING_REVIEW_STEP';
-+ LinkEventName['IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION'] =
-+ 'IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION';
-+ LinkEventName['IDENTITY_VERIFICATION_CREATE_SESSION'] =
-+ 'IDENTITY_VERIFICATION_CREATE_SESSION';
-+ LinkEventName['IDENTITY_VERIFICATION_RESUME_SESSION'] =
-+ 'IDENTITY_VERIFICATION_RESUME_SESSION';
-+ LinkEventName['IDENTITY_VERIFICATION_PASS_SESSION'] =
-+ 'IDENTITY_VERIFICATION_PASS_SESSION';
-+ LinkEventName['IDENTITY_VERIFICATION_FAIL_SESSION'] =
-+ 'IDENTITY_VERIFICATION_FAIL_SESSION';
-+ LinkEventName['IDENTITY_VERIFICATION_OPEN_UI'] =
-+ 'IDENTITY_VERIFICATION_OPEN_UI';
-+ LinkEventName['IDENTITY_VERIFICATION_RESUME_UI'] =
-+ 'IDENTITY_VERIFICATION_RESUME_UI';
-+ LinkEventName['IDENTITY_VERIFICATION_CLOSE_UI'] =
-+ 'IDENTITY_VERIFICATION_CLOSE_UI';
-+ LinkEventName['MATCHED_CONSENT'] = 'MATCHED_CONSENT';
-+ LinkEventName['MATCHED_SELECT_INSTITUTION'] = 'MATCHED_SELECT_INSTITUTION';
-+ LinkEventName['MATCHED_SELECT_VERIFY_METHOD'] =
-+ 'MATCHED_SELECT_VERIFY_METHOD';
-+ LinkEventName['OPEN'] = 'OPEN';
-+ LinkEventName['OPEN_MY_PLAID'] = 'OPEN_MY_PLAID';
-+ LinkEventName['OPEN_OAUTH'] = 'OPEN_OAUTH';
-+ LinkEventName['SEARCH_INSTITUTION'] = 'SEARCH_INSTITUTION';
-+ LinkEventName['SELECT_DEGRADED_INSTITUTION'] = 'SELECT_DEGRADED_INSTITUTION';
-+ LinkEventName['SELECT_DOWN_INSTITUTION'] = 'SELECT_DOWN_INSTITUTION';
-+ LinkEventName['SELECT_FILTERED_INSTITUTION'] = 'SELECT_FILTERED_INSTITUTION';
-+ LinkEventName['SELECT_INSTITUTION'] = 'SELECT_INSTITUTION';
-+ LinkEventName['SELECT_BRAND'] = 'SELECT_BRAND';
-+ LinkEventName['SELECT_AUTH_TYPE'] = 'SELECT_AUTH_TYPE';
-+ LinkEventName['SUBMIT_ACCOUNT_NUMBER'] = 'SUBMIT_ACCOUNT_NUMBER';
-+ LinkEventName['SUBMIT_DOCUMENTS'] = 'SUBMIT_DOCUMENTS';
-+ LinkEventName['SUBMIT_DOCUMENTS_SUCCESS'] = 'SUBMIT_DOCUMENTS_SUCCESS';
-+ LinkEventName['SUBMIT_DOCUMENTS_ERROR'] = 'SUBMIT_DOCUMENTS_ERROR';
-+ LinkEventName['SUBMIT_ROUTING_NUMBER'] = 'SUBMIT_ROUTING_NUMBER';
-+ LinkEventName['VIEW_DATA_TYPES'] = 'VIEW_DATA_TYPES';
-+ LinkEventName['SUBMIT_PHONE'] = 'SUBMIT_PHONE';
-+ LinkEventName['SKIP_SUBMIT_PHONE'] = 'SKIP_SUBMIT_PHONE';
-+ LinkEventName['VERIFY_PHONE'] = 'VERIFY_PHONE';
-+ LinkEventName['SUBMIT_CREDENTIALS'] = 'SUBMIT_CREDENTIALS';
-+ LinkEventName['SUBMIT_MFA'] = 'SUBMIT_MFA';
-+ LinkEventName['TRANSITION_VIEW'] = 'TRANSITION_VIEW';
-+ LinkEventName['CONNECT_NEW_INSTITUTION'] = 'CONNECT_NEW_INSTITUTION';
- })(LinkEventName || (LinkEventName = {}));
- export var LinkEventViewName;
--(function (LinkEventViewName) {
-- LinkEventViewName["ACCEPT_TOS"] = "ACCEPT_TOS";
-- LinkEventViewName["CONNECTED"] = "CONNECTED";
-- LinkEventViewName["CONSENT"] = "CONSENT";
-- LinkEventViewName["CREDENTIAL"] = "CREDENTIAL";
-- LinkEventViewName["DATA_TRANSPARENCY"] = "DATA_TRANSPARENCY";
-- LinkEventViewName["DATA_TRANSPARENCY_CONSENT"] = "DATA_TRANSPARENCY_CONSENT";
-- LinkEventViewName["DOCUMENTARY_VERIFICATION"] = "DOCUMENTARY_VERIFICATION";
-- LinkEventViewName["ERROR"] = "ERROR";
-- LinkEventViewName["EXIT"] = "EXIT";
-- LinkEventViewName["KYC_CHECK"] = "KYC_CHECK";
-- LinkEventViewName["SELFIE_CHECK"] = "SELFIE_CHECK";
-- LinkEventViewName["LOADING"] = "LOADING";
-- LinkEventViewName["MATCHED_CONSENT"] = "MATCHED_CONSENT";
-- LinkEventViewName["MATCHED_CREDENTIAL"] = "MATCHED_CREDENTIAL";
-- LinkEventViewName["MATCHED_MFA"] = "MATCHED_MFA";
-- LinkEventViewName["MFA"] = "MFA";
-- LinkEventViewName["NUMBERS"] = "NUMBERS";
-- LinkEventViewName["NUMBERS_SELECT_INSTITUTION"] = "NUMBERS_SELECT_INSTITUTION";
-- LinkEventViewName["OAUTH"] = "OAUTH";
-- LinkEventViewName["RECAPTCHA"] = "RECAPTCHA";
-- LinkEventViewName["RISK_CHECK"] = "RISK_CHECK";
-- LinkEventViewName["SCREENING"] = "SCREENING";
-- LinkEventViewName["SELECT_ACCOUNT"] = "SELECT_ACCOUNT";
-- LinkEventViewName["SELECT_AUTH_TYPE"] = "SELECT_AUTH_TYPE";
-- LinkEventViewName["SUBMIT_PHONE"] = "SUBMIT_PHONE";
-- LinkEventViewName["VERIFY_PHONE"] = "VERIFY_PHONE";
-- LinkEventViewName["SELECT_SAVED_INSTITUTION"] = "SELECT_SAVED_INSTITUTION";
-- LinkEventViewName["SELECT_SAVED_ACCOUNT"] = "SELECT_SAVED_ACCOUNT";
-- LinkEventViewName["SELECT_BRAND"] = "SELECT_BRAND";
-- LinkEventViewName["SELECT_INSTITUTION"] = "SELECT_INSTITUTION";
-- LinkEventViewName["SUBMIT_DOCUMENTS"] = "SUBMIT_DOCUMENTS";
-- LinkEventViewName["SUBMIT_DOCUMENTS_SUCCESS"] = "SUBMIT_DOCUMENTS_SUCCESS";
-- LinkEventViewName["SUBMIT_DOCUMENTS_ERROR"] = "SUBMIT_DOCUMENTS_ERROR";
-- LinkEventViewName["UPLOAD_DOCUMENTS"] = "UPLOAD_DOCUMENTS";
-- LinkEventViewName["VERIFY_SMS"] = "VERIFY_SMS";
-+(function(LinkEventViewName) {
-+ LinkEventViewName['ACCEPT_TOS'] = 'ACCEPT_TOS';
-+ LinkEventViewName['CONNECTED'] = 'CONNECTED';
-+ LinkEventViewName['CONSENT'] = 'CONSENT';
-+ LinkEventViewName['CREDENTIAL'] = 'CREDENTIAL';
-+ LinkEventViewName['DATA_TRANSPARENCY'] = 'DATA_TRANSPARENCY';
-+ LinkEventViewName['DATA_TRANSPARENCY_CONSENT'] = 'DATA_TRANSPARENCY_CONSENT';
-+ LinkEventViewName['DOCUMENTARY_VERIFICATION'] = 'DOCUMENTARY_VERIFICATION';
-+ LinkEventViewName['ERROR'] = 'ERROR';
-+ LinkEventViewName['EXIT'] = 'EXIT';
-+ LinkEventViewName['KYC_CHECK'] = 'KYC_CHECK';
-+ LinkEventViewName['SELFIE_CHECK'] = 'SELFIE_CHECK';
-+ LinkEventViewName['LOADING'] = 'LOADING';
-+ LinkEventViewName['MATCHED_CONSENT'] = 'MATCHED_CONSENT';
-+ LinkEventViewName['MATCHED_CREDENTIAL'] = 'MATCHED_CREDENTIAL';
-+ LinkEventViewName['MATCHED_MFA'] = 'MATCHED_MFA';
-+ LinkEventViewName['MFA'] = 'MFA';
-+ LinkEventViewName['NUMBERS'] = 'NUMBERS';
-+ LinkEventViewName['NUMBERS_SELECT_INSTITUTION'] =
-+ 'NUMBERS_SELECT_INSTITUTION';
-+ LinkEventViewName['OAUTH'] = 'OAUTH';
-+ LinkEventViewName['RECAPTCHA'] = 'RECAPTCHA';
-+ LinkEventViewName['RISK_CHECK'] = 'RISK_CHECK';
-+ LinkEventViewName['SCREENING'] = 'SCREENING';
-+ LinkEventViewName['SELECT_ACCOUNT'] = 'SELECT_ACCOUNT';
-+ LinkEventViewName['SELECT_AUTH_TYPE'] = 'SELECT_AUTH_TYPE';
-+ LinkEventViewName['SUBMIT_PHONE'] = 'SUBMIT_PHONE';
-+ LinkEventViewName['VERIFY_PHONE'] = 'VERIFY_PHONE';
-+ LinkEventViewName['SELECT_SAVED_INSTITUTION'] = 'SELECT_SAVED_INSTITUTION';
-+ LinkEventViewName['SELECT_SAVED_ACCOUNT'] = 'SELECT_SAVED_ACCOUNT';
-+ LinkEventViewName['SELECT_BRAND'] = 'SELECT_BRAND';
-+ LinkEventViewName['SELECT_INSTITUTION'] = 'SELECT_INSTITUTION';
-+ LinkEventViewName['SUBMIT_DOCUMENTS'] = 'SUBMIT_DOCUMENTS';
-+ LinkEventViewName['SUBMIT_DOCUMENTS_SUCCESS'] = 'SUBMIT_DOCUMENTS_SUCCESS';
-+ LinkEventViewName['SUBMIT_DOCUMENTS_ERROR'] = 'SUBMIT_DOCUMENTS_ERROR';
-+ LinkEventViewName['UPLOAD_DOCUMENTS'] = 'UPLOAD_DOCUMENTS';
-+ LinkEventViewName['VERIFY_SMS'] = 'VERIFY_SMS';
- })(LinkEventViewName || (LinkEventViewName = {}));
- /// Methods to present Link on iOS.
- /// FULL_SCREEN is the converts to UIModalPresentationOverFullScreen on the native side.
- /// MODAL will use the default presentation style for iOS which is UIModalPresentationAutomatic.
- export var LinkIOSPresentationStyle;
--(function (LinkIOSPresentationStyle) {
-- LinkIOSPresentationStyle["FULL_SCREEN"] = "FULL_SCREEN";
-- LinkIOSPresentationStyle["MODAL"] = "MODAL";
-+(function(LinkIOSPresentationStyle) {
-+ LinkIOSPresentationStyle['FULL_SCREEN'] = 'FULL_SCREEN';
-+ LinkIOSPresentationStyle['MODAL'] = 'MODAL';
- })(LinkIOSPresentationStyle || (LinkIOSPresentationStyle = {}));
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.d.ts b/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.d.ts
-index cd4ccde..cb0ff5c 100644
---- a/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.d.ts
-+++ b/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.d.ts
-@@ -1 +1 @@
--declare const Types: any;
-+export {};
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.js b/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.js
-index 831866b..f38aff8 100644
---- a/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.js
-+++ b/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.js
-@@ -1,14 +1,14 @@
--"use strict";
- const Types = require('./../Types');
- test('test token configuration', () => {
-- const linkTokenConfiguration = {
-- token: "test-token",
-- noLoadingState: false,
-- logLevel: Types.LinkLogLevel.DEBUG,
-- extras: null,
-- };
-- expect(linkTokenConfiguration.noLoadingState).toBe(false);
-- expect(linkTokenConfiguration.token).toBe("test-token");
-- expect(linkTokenConfiguration.logLevel).toBe(Types.LinkLogLevel.DEBUG);
-- expect(linkTokenConfiguration.extras).toBe(null);
-+ const linkTokenConfiguration = {
-+ token: 'test-token',
-+ noLoadingState: false,
-+ logLevel: Types.LinkLogLevel.DEBUG,
-+ extras: null,
-+ };
-+ expect(linkTokenConfiguration.noLoadingState).toBe(false);
-+ expect(linkTokenConfiguration.token).toBe('test-token');
-+ expect(linkTokenConfiguration.logLevel).toBe(Types.LinkLogLevel.DEBUG);
-+ expect(linkTokenConfiguration.extras).toBe(null);
- });
-+export {};
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.d.ts b/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.d.ts
-new file mode 100644
-index 0000000..cb0ff5c
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.d.ts
-@@ -0,0 +1 @@
-+export {};
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.js b/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.js
-new file mode 100644
-index 0000000..34a47d4
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.js
-@@ -0,0 +1,15 @@
-+'use strict';
-+const fs = require('fs');
-+const rn_version = JSON.parse(fs.readFileSync('package.json', 'utf-8'))[
-+ 'version'
-+];
-+const android_version = fs
-+ .readFileSync('android/src/main/AndroidManifest.xml', 'utf-8')
-+ .match(/(?<=value=").*(?=")/)[0];
-+if (rn_version != android_version) {
-+ console.error('Commit failed SDK version mismatch');
-+ console.error(
-+ 'Please ensure package.json and android/src/main/AndroidManifest.xml have the same version',
-+ );
-+ process.exit(-1);
-+}
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.d.ts b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.d.ts
-new file mode 100644
-index 0000000..1de3019
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.d.ts
-@@ -0,0 +1,14 @@
-+import { TurboModule } from 'react-native';
-+import { Int32 } from 'react-native/Libraries/Types/CodegenTypes';
-+import { LinkSuccess as UnsafeObject, LinkExit as Double, LinkError as Float } from '../Types';
-+export interface Spec extends TurboModule {
-+ continueFromRedirectUriString(redirectUriString: string): void;
-+ create(configuration: Object): void;
-+ open(onSuccess: (success: UnsafeObject) => void, onExit: (error: Float, result: Double) => void): void;
-+ dismiss(): void;
-+ startLinkActivityForResult(data: string, onSuccessCallback: (result: UnsafeObject) => void, onExitCallback: (result: Double) => void): void;
-+ addListener(eventName: string): void;
-+ removeListeners(count: Int32): void;
-+}
-+declare const _default: Spec;
-+export default _default;
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.js b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.js
-new file mode 100644
-index 0000000..310a9c5
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.js
-@@ -0,0 +1,4 @@
-+// we use Object type because methods on the native side use NSDictionary and ReadableMap
-+// and we want to stay compatible with those
-+import { TurboModuleRegistry } from 'react-native';
-+export default TurboModuleRegistry.getEnforcing('RNLinksdk');
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.d.ts b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.d.ts
-new file mode 100644
-index 0000000..82f29a1
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.d.ts
-@@ -0,0 +1,9 @@
-+import { TurboModule } from 'react-native';
-+import { Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes';
-+export interface Spec extends TurboModule {
-+ startLinkActivityForResult(token: string, noLoadingState: boolean, logLevel: string, onSuccessCallback: (result: UnsafeObject) => void, onExitCallback: (result: UnsafeObject) => void): void;
-+ addListener(eventName: string): void;
-+ removeListeners(count: Int32): void;
-+}
-+declare const _default: Spec | null;
-+export default _default;
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.js b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.js
-new file mode 100644
-index 0000000..d0ea456
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.js
-@@ -0,0 +1,4 @@
-+// we use Object type because methods on the native side use NSDictionary and ReadableMap
-+// and we want to stay compatible with those
-+import { TurboModuleRegistry } from 'react-native';
-+export default TurboModuleRegistry.get('PlaidAndroid');
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.d.ts b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.d.ts
-new file mode 100644
-index 0000000..aefee8c
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.d.ts
-@@ -0,0 +1,11 @@
-+import { TurboModule } from 'react-native';
-+import { Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes';
-+export interface Spec extends TurboModule {
-+ create(token: string, noLoadingState: boolean): void;
-+ open(fullScreen: boolean, onSuccess: (success: UnsafeObject) => void, onExit: (error: UnsafeObject, result: UnsafeObject) => void): void;
-+ dismiss(): void;
-+ addListener(eventName: string): void;
-+ removeListeners(count: Int32): void;
-+}
-+declare const _default: Spec | null;
-+export default _default;
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.js b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.js
-new file mode 100644
-index 0000000..99845a1
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.js
-@@ -0,0 +1,4 @@
-+// we use Object type because methods on the native side use NSDictionary and ReadableMap
-+// and we want to stay compatible with those
-+import { TurboModuleRegistry } from 'react-native';
-+export default TurboModuleRegistry.get('RNLinksdk');
-diff --git a/node_modules/react-native-plaid-link-sdk/dist/index.js b/node_modules/react-native-plaid-link-sdk/dist/index.js
-index 68c2d4b..acad079 100644
---- a/node_modules/react-native-plaid-link-sdk/dist/index.js
-+++ b/node_modules/react-native-plaid-link-sdk/dist/index.js
-@@ -1,6 +1,6 @@
--import { openLink, dismissLink, usePlaidEmitter, PlaidLink, } from './PlaidLink';
-+import { openLink, dismissLink, usePlaidEmitter, PlaidLink } from './PlaidLink';
- export * from './Types';
- export default PlaidLink;
--export { PlaidLink, openLink, dismissLink, usePlaidEmitter, };
-+export { PlaidLink, openLink, dismissLink, usePlaidEmitter };
- // Components
- export { EmbeddedLinkView } from './EmbeddedLink/EmbeddedLinkView';
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.h b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.h
-index 8a1c350..035b91c 100644
---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.h
-+++ b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.h
-@@ -1,15 +1,17 @@
--
--#if __has_include()
--#import
--#import
--#else
--#import "RCTBridgeModule.h"
--#import "RCTEventEmitter.h"
-+#ifdef RCT_NEW_ARCH_ENABLED
-+#import
- #endif
-+#import
-+#import "RCTEventEmitter.h"
-
- #import
-
--@interface RNLinksdk : RCTEventEmitter
-+@interface RNLinksdk : RCTEventEmitter
-+#ifdef RCT_NEW_ARCH_ENABLED
-+
-+#else
-+
-+#endif
-
- + (NSDictionary *)dictionaryFromSuccess:(PLKLinkSuccess *)success;
- + (NSDictionary *)dictionaryFromEvent:(PLKLinkEvent *)event;
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm
-index ef3fe85..b3b92d6 100644
---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm
-+++ b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm
-@@ -1,4 +1,5 @@
- #import "RNLinksdk.h"
-+#import "RNPlaidHelper.h"
-
- #import
- #import
-@@ -66,11 +67,11 @@ - (void)stopObserving {
- self.hasObservers = NO;
- }
-
--RCT_EXPORT_METHOD(create:(NSString*)token :(BOOL)noLoadingState) {
-- __weak typeof(self) weakSelf = self;
-+RCT_EXPORT_METHOD(create:(NSString*)token noLoadingState:(BOOL)noLoadingState) {
-+ __weak RNLinksdk *weakSelf = self;
-
- void (^onSuccess)(PLKLinkSuccess *) = ^(PLKLinkSuccess *success) {
-- __typeof(weakSelf) strongSelf = weakSelf;
-+ RNLinksdk *strongSelf = weakSelf;
-
- if (strongSelf.successCallback) {
- NSDictionary *jsMetadata = [RNLinksdk dictionaryFromSuccess:success];
-@@ -80,7 +81,7 @@ - (void)stopObserving {
- };
-
- void (^onExit)(PLKLinkExit *) = ^(PLKLinkExit *exit) {
-- __typeof(weakSelf) strongSelf = weakSelf;
-+ RNLinksdk *strongSelf = weakSelf;
-
- if (strongSelf.exitCallback) {
- NSDictionary *exitMetadata = [RNLinksdk dictionaryFromExit:exit];
-@@ -94,7 +95,7 @@ - (void)stopObserving {
- };
-
- void (^onEvent)(PLKLinkEvent *) = ^(PLKLinkEvent *event) {
-- __typeof(weakSelf) strongSelf = weakSelf;
-+ RNLinksdk *strongSelf = weakSelf;
- if (strongSelf.hasObservers) {
- NSDictionary *eventDictionary = [RNLinksdk dictionaryFromEvent:event];
- [strongSelf sendEventWithName:kRNLinkKitOnEventEvent
-@@ -108,11 +109,11 @@ - (void)stopObserving {
- config.noLoadingState = noLoadingState;
-
- NSError *creationError = nil;
-- self.linkHandler = [PLKPlaid createWithLinkTokenConfiguration:config error:&creationError];
-+ self.linkHandler = [RNPlaidHelper createWithLinkTokenConfiguration:config error:&creationError];
- self.creationError = creationError;
- }
-
--RCT_EXPORT_METHOD(open: (BOOL)fullScreen :(RCTResponseSenderBlock)onSuccess :(RCTResponseSenderBlock)onExit) {
-+RCT_EXPORT_METHOD(open:(BOOL)fullScreen onSuccess:(RCTResponseSenderBlock)onSuccess onExit:(RCTResponseSenderBlock)onExit) {
- if (self.linkHandler) {
- self.successCallback = onSuccess;
- self.exitCallback = onExit;
-@@ -122,7 +123,7 @@ - (void)stopObserving {
- // unnecessarily invoked.
- __block bool didPresent = NO;
-
-- __weak typeof(self) weakSelf = self;
-+ __weak RNLinksdk *weakSelf = self;
- void(^presentationHandler)(UIViewController *) = ^(UIViewController *linkViewController) {
-
- if (fullScreen) {
-@@ -619,4 +620,12 @@ + (NSDictionary *)dictionaryFromExit:(PLKLinkExit *)exit {
- };
- }
-
-+#if RCT_NEW_ARCH_ENABLED
-+- (std::shared_ptr)getTurboModule:
-+ (const facebook::react::ObjCTurboModule::InitParams &)params
-+{
-+ return std::make_shared(params);
-+}
-+#endif
-+
- @end
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata
-deleted file mode 100644
-index 919434a..0000000
---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata
-+++ /dev/null
-@@ -1,7 +0,0 @@
--
--
--
--
--
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
-deleted file mode 100644
-index 18d9810..0000000
---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
-+++ /dev/null
-@@ -1,8 +0,0 @@
--
--
--
--
-- IDEDidComputeMac32BitWarning
--
--
--
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate
-deleted file mode 100644
-index 47e9cc2..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/xcuserdata/dtroupe.xcuserdatad/xcschemes/xcschememanagement.plist b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/xcuserdata/dtroupe.xcuserdatad/xcschemes/xcschememanagement.plist
-deleted file mode 100644
-index 5b4fa70..0000000
---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/xcuserdata/dtroupe.xcuserdatad/xcschemes/xcschememanagement.plist
-+++ /dev/null
-@@ -1,19 +0,0 @@
--
--
--
--
-- SchemeUserState
--
-- RNLinksdk.xcscheme_^#shared#^_
--
-- orderHint
-- 0
--
-- RNLinksdkUnitTests.xcscheme_^#shared#^_
--
-- orderHint
-- 1
--
--
--
--
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate
-deleted file mode 100644
-index 824773d..0000000
-Binary files a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.h b/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.h
-new file mode 100644
-index 0000000..535d333
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.h
-@@ -0,0 +1,7 @@
-+#import
-+
-+@interface RNPlaidHelper : NSObject
-+
-++ (id _Nullable)createWithLinkTokenConfiguration:(PLKLinkTokenConfiguration * _Nonnull)linkTokenConfiguration error:(NSError * _Nullable * _Nullable)error;
-+
-+@end
-diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.m b/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.m
-new file mode 100644
-index 0000000..2288299
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.m
-@@ -0,0 +1,10 @@
-+#import "RNPlaidHelper.h"
-+
-+@implementation RNPlaidHelper
-+
-++ (id _Nullable)createWithLinkTokenConfiguration:(PLKLinkTokenConfiguration * _Nonnull)linkTokenConfiguration error:(NSError * _Nullable * _Nullable)error
-+{
-+ return [PLKPlaid createWithLinkTokenConfiguration:linkTokenConfiguration error:error];
-+}
-+
-+@end
-diff --git a/node_modules/react-native-plaid-link-sdk/package.json b/node_modules/react-native-plaid-link-sdk/package.json
-index 22c7d2c..3d1b85c 100644
---- a/node_modules/react-native-plaid-link-sdk/package.json
-+++ b/node_modules/react-native-plaid-link-sdk/package.json
-@@ -4,11 +4,13 @@
- "description": "React Native Plaid Link SDK",
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
-+ "react-native": "src/index.ts",
- "files": [
- "dist/**/*",
- "android/**/*",
-- "ios",
-- "react-native-plaid-link-sdk.podspec"
-+ "ios/**/*",
-+ "react-native-plaid-link-sdk.podspec",
-+ "src/**/*"
- ],
- "scripts": {
- "lint": "eslint \"./**/*.{js,jsx}\" --fix",
-@@ -47,7 +49,7 @@
- "@react-native-community/eslint-plugin": "^1.1.0",
- "@types/jest": "^26.0.14",
- "@types/react": "^16.14.20",
-- "@types/react-native": "^0.66.0",
-+ "@types/react-native": "^0.71.3",
- "@types/react-test-renderer": "^16.9.3",
- "@typescript-eslint/eslint-plugin": "^4.33.0",
- "@typescript-eslint/parser": "^4.33.0",
-@@ -62,5 +64,16 @@
- "react": "18.0.0",
- "react-native": "0.69.9",
- "typescript": "^4.9.5"
-+ },
-+ "dependencies": {
-+ "react-native-plaid-link-sdk": "^10.4.0"
-+ },
-+ "codegenConfig": {
-+ "name": "rnplaidlink",
-+ "type": "modules",
-+ "jsSrcsDir": "./src/fabric",
-+ "android": {
-+ "javaPackageName": "com.plaid"
-+ }
- }
- }
-diff --git a/node_modules/react-native-plaid-link-sdk/react-native-plaid-link-sdk.podspec b/node_modules/react-native-plaid-link-sdk/react-native-plaid-link-sdk.podspec
-index ee59a19..40ac7df 100644
---- a/node_modules/react-native-plaid-link-sdk/react-native-plaid-link-sdk.podspec
-+++ b/node_modules/react-native-plaid-link-sdk/react-native-plaid-link-sdk.podspec
-@@ -2,6 +2,8 @@ require 'json'
-
- package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
-
-+fabric_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
-+
- Pod::Spec.new do |s|
- s.name = package['name']
- s.version = package['version']
-@@ -13,8 +15,13 @@ Pod::Spec.new do |s|
- s.platform = :ios, "14.0"
-
- s.source = { :git => "https://github.com/plaid/react-native-plaid-link-sdk.git", :tag => "v#{s.version}" }
-- s.source_files = "ios/*.{h,m,swift}"
-+ s.source_files = "ios/**/*.{h,m,mm,swift}"
-+
-+ if fabric_enabled
-+ install_modules_dependencies(s)
-+ else
-+ s.dependency "React-Core"
-+ end
-
-- s.dependency 'React-Core'
- s.dependency 'Plaid', '~> 5.2.0'
- end
-diff --git a/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.tsx b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.tsx
-new file mode 100644
-index 0000000..0243de2
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.tsx
-@@ -0,0 +1,95 @@
-+import React from 'react';
-+import { StyleProp, ViewStyle } from 'react-native';
-+import NativeEmbeddedLinkView from './NativeEmbeddedLinkView';
-+import {
-+ LinkSuccessListener,
-+ LinkSuccess,
-+ LinkExitListener,
-+ LinkExit,
-+ LinkIOSPresentationStyle,
-+ LinkOnEventListener,
-+ LinkEvent,
-+ LinkEventName,
-+ LinkEventMetadata,
-+ LinkError,
-+ LinkExitMetadata,
-+ LinkSuccessMetadata,
-+} from '../Types';
-+
-+type EmbeddedLinkProps = {
-+ token: String,
-+ iOSPresentationStyle: LinkIOSPresentationStyle,
-+ onEvent: LinkOnEventListener | undefined,
-+ onSuccess: LinkSuccessListener,
-+ onExit: LinkExitListener | undefined,
-+ style: StyleProp | undefined,
-+}
-+
-+class EmbeddedEvent implements LinkEvent {
-+ eventName: LinkEventName;
-+ metadata: LinkEventMetadata;
-+
-+ constructor(event: any) {
-+ this.eventName = event.eventName
-+ this.metadata = event.metadata
-+ }
-+}
-+
-+class EmbeddedExit implements LinkExit {
-+ error: LinkError | undefined;
-+ metadata: LinkExitMetadata;
-+
-+ constructor(event: any) {
-+ this.error = event.error;
-+ this.metadata = event.metadata;
-+ }
-+}
-+
-+class EmbeddedSuccess implements LinkSuccess {
-+ publicToken: string;
-+ metadata: LinkSuccessMetadata;
-+
-+ constructor(event: any) {
-+ this.publicToken = event.publicToken;
-+ this.metadata = event.metadata;
-+ }
-+}
-+
-+export const EmbeddedLinkView: React.FC = (props) => {
-+
-+ const {token, iOSPresentationStyle, onEvent, onSuccess, onExit, style} = props;
-+
-+ const onEmbeddedEvent = (event: any) => {
-+
-+ switch (event.nativeEvent.embeddedEventName) {
-+ case 'onSuccess': {
-+ if (!onSuccess) { return; }
-+ const embeddedSuccess = new EmbeddedSuccess(event.nativeEvent);
-+ onSuccess(embeddedSuccess);
-+ break;
-+ }
-+ case 'onExit': {
-+ if (!onExit) {return; }
-+ const embeddedExit = new EmbeddedExit(event.nativeEvent);
-+ onExit(embeddedExit);
-+ break;
-+ }
-+ case 'onEvent': {
-+ if (!onEvent) { return; }
-+ const embeddedEvent = new EmbeddedEvent(event.nativeEvent);
-+ onEvent(embeddedEvent);
-+ break;
-+ }
-+ default: {
-+ return;
-+ }
-+ }
-+ }
-+
-+ return
-+};
-\ No newline at end of file
-diff --git a/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.web.tsx b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.web.tsx
-new file mode 100644
-index 0000000..7a71609
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.web.tsx
-@@ -0,0 +1,5 @@
-+// EmbeddedLinkView.web.tsx is a shim file which causes web bundlers to ignore the EmbeddedLinkView.tsx file
-+// which imports requireNativeComponent (causing a runtime error with react-native-web).
-+// Ref - https://github.com/plaid/react-native-plaid-link-sdk/issues/564
-+import React from 'react';
-+export const EmbeddedLinkView = () => null;
-diff --git a/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/NativeEmbeddedLinkView.tsx b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/NativeEmbeddedLinkView.tsx
-new file mode 100644
-index 0000000..da05fb1
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/NativeEmbeddedLinkView.tsx
-@@ -0,0 +1,9 @@
-+import { requireNativeComponent } from 'react-native';
-+
-+// Error "Tried to register two views with the same name PLKEmbeddedView"
-+// will be thrown during hot reload when any change is made to the
-+// file that is calling this requireNativeComponent('PLKEmbeddedView') call.
-+// Leaving this in its own file resolves this issue.
-+const NativeEmbeddedLinkView = requireNativeComponent('PLKEmbeddedView');
-+
-+export default NativeEmbeddedLinkView;
-\ No newline at end of file
-diff --git a/node_modules/react-native-plaid-link-sdk/src/PlaidLink.tsx b/node_modules/react-native-plaid-link-sdk/src/PlaidLink.tsx
-new file mode 100644
-index 0000000..b35c06f
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/PlaidLink.tsx
-@@ -0,0 +1,117 @@
-+import React, { useEffect } from 'react';
-+import { NativeEventEmitter, Platform, TouchableOpacity } from 'react-native';
-+import {
-+ LinkError,
-+ LinkEventListener,
-+ LinkExit,
-+ LinkIOSPresentationStyle,
-+ LinkLogLevel,
-+ LinkSuccess,
-+ PlaidLinkComponentProps,
-+ PlaidLinkProps,
-+} from './Types';
-+import RNLinksdkAndroid from './fabric/NativePlaidLinkModuleAndroid';
-+import RNLinksdkiOS from './fabric/NativePlaidLinkModuleiOS';
-+
-+const RNLinksdk = (Platform.OS === 'android' ? RNLinksdkAndroid : RNLinksdkiOS) ?? undefined;
-+
-+/**
-+ * A hook that registers a listener on the Plaid emitter for the 'onEvent' type.
-+ * The listener is cleaned up when this view is unmounted
-+ *
-+ * @param linkEventListener the listener to call
-+ */
-+export const usePlaidEmitter = (linkEventListener: LinkEventListener) => {
-+ useEffect(() => {
-+ const emitter = new NativeEventEmitter(RNLinksdk);
-+ const listener = emitter.addListener('onEvent', linkEventListener);
-+ // Clean up after this effect:
-+ return function cleanup() {
-+ listener.remove();
-+ };
-+ }, []);
-+};
-+
-+export const openLink = async (props: PlaidLinkProps) => {
-+ let config = props.tokenConfig;
-+ let noLoadingState = config.noLoadingState ?? false;
-+
-+ if (Platform.OS === 'android') {
-+ if (RNLinksdkAndroid === null) {
-+ throw new Error('[react-native-plaid-link-sdk] RNLinksdkAndroid is not defined');
-+ }
-+
-+ RNLinksdkAndroid.startLinkActivityForResult(
-+ config.token,
-+ noLoadingState,
-+ config.logLevel ?? LinkLogLevel.ERROR,
-+ // @ts-ignore we use Object type in the spec file as it maps to NSDictionary and ReadableMap
-+ (result: LinkSuccess) => {
-+ if (props.onSuccess != null) {
-+ props.onSuccess(result);
-+ }
-+ },
-+ (result: LinkExit) => {
-+ if (props.onExit != null) {
-+ if (result.error != null && result.error.displayMessage != null) {
-+ //TODO(RNSDK-118): Remove errorDisplayMessage field in next major update.
-+ result.error.errorDisplayMessage = result.error.displayMessage;
-+ }
-+ props.onExit(result);
-+ }
-+ },
-+ );
-+ } else {
-+ if (RNLinksdkiOS === null) {
-+ throw new Error('[react-native-plaid-link-sdk] RNLinksdkiOS is not defined');
-+ }
-+
-+ RNLinksdkiOS.create(config.token, noLoadingState);
-+
-+ let presentFullScreen =
-+ props.iOSPresentationStyle == LinkIOSPresentationStyle.FULL_SCREEN;
-+
-+ RNLinksdkiOS.open(
-+ presentFullScreen,
-+ // @ts-ignore we use Object type in the spec file as it maps to NSDictionary and ReadableMap
-+ (result: LinkSuccess) => {
-+ if (props.onSuccess != null) {
-+ props.onSuccess(result);
-+ }
-+ },
-+ (error: LinkError, result: LinkExit) => {
-+ if (props.onExit != null) {
-+ if (error) {
-+ var data = result || {};
-+ data.error = error;
-+ props.onExit(data);
-+ } else {
-+ props.onExit(result);
-+ }
-+ }
-+ },
-+ );
-+ }
-+};
-+
-+export const dismissLink = () => {
-+ if (Platform.OS === 'ios') {
-+ if (RNLinksdkiOS === null) {
-+ throw new Error('[react-native-plaid-link-sdk] RNLinksdkiOS is not defined');
-+ }
-+
-+ RNLinksdkiOS.dismiss();
-+ }
-+};
-+
-+export const PlaidLink = (props: PlaidLinkComponentProps) => {
-+ function onPress() {
-+ props.onPress?.();
-+ openLink(props);
-+ }
-+
-+ return (
-+ // @ts-ignore some types directories misconfiguration
-+ {props.children}
-+ );
-+};
-diff --git a/node_modules/react-native-plaid-link-sdk/src/Types.ts b/node_modules/react-native-plaid-link-sdk/src/Types.ts
-new file mode 100644
-index 0000000..a7d30c6
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/Types.ts
-@@ -0,0 +1,550 @@
-+interface CommonPlaidLinkOptions {
-+ logLevel?: LinkLogLevel;
-+ extras?: Record;
-+}
-+
-+export type LinkTokenConfiguration = (CommonPlaidLinkOptions & {
-+ token: string;
-+ // A `Bool` indicating that Link should skip displaying a loading animation until the Link UI is fully loaded.
-+ // This can be used to display custom loading UI while Link content is loading (and will skip any initial loading UI in Link).
-+ // Note: Dismiss custom loading UI on the OPEN & EXIT events.
-+ //
-+ // Note: This should be set to `true` when setting the `eu_config.headless` field in /link/token/create requests to `true`.
-+ // For reference, see https://plaid.com/docs/api/tokens/#link-token-create-request-eu-config-headless
-+ noLoadingState?: boolean;
-+});
-+
-+export enum LinkLogLevel {
-+ DEBUG="debug",
-+ INFO="info",
-+ WARN="warn",
-+ ERROR="error",
-+}
-+
-+export enum PlaidEnvironment {
-+ PRODUCTION = 'production',
-+ DEVELOPMENT = 'development',
-+ SANDBOX = 'sandbox',
-+}
-+
-+export enum PlaidProduct {
-+ ASSETS="assets",
-+ AUTH="auth",
-+ DEPOSIT_SWITCH="deposit_switch",
-+ IDENTITY="identity",
-+ INCOME="income",
-+ INVESTMENTS="investments",
-+ LIABILITIES="liabilities",
-+ LIABILITIES_REPORT="liabilities_report",
-+ PAYMENT_INITIATION="payment_initiation",
-+ TRANSACTIONS="transactions",
-+}
-+
-+export enum LinkAccountType {
-+ CREDIT = 'credit',
-+ DEPOSITORY = 'depository',
-+ INVESTMENT = 'investment',
-+ LOAN = 'loan',
-+ OTHER = 'other',
-+}
-+
-+export enum LinkAccountSubtypes {
-+ ALL = 'all',
-+ CREDIT_CARD = 'credit card',
-+ PAYPAL = 'paypal',
-+ AUTO = 'auto',
-+ BUSINESS = 'business',
-+ COMMERCIAL = 'commercial',
-+ CONSTRUCTION = 'construction',
-+ CONSUMER = 'consumer',
-+ HOME_EQUITY = 'home equity',
-+ LINE_OF_CREDIT = 'line of credit',
-+ LOAN = 'loan',
-+ MORTGAGE = 'mortgage',
-+ OVERDRAFT = 'overdraft',
-+ STUDENT = 'student',
-+ CASH_MANAGEMENT = 'cash management',
-+ CD = 'cd',
-+ CHECKING = 'checking',
-+ EBT = 'ebt',
-+ HSA = 'hsa',
-+ MONEY_MARKET = 'money market',
-+ PREPAID = 'prepaid',
-+ SAVINGS = 'savings',
-+ FOUR_0_1_A = '401a',
-+ FOUR_0_1_K = '401k',
-+ FOUR_0_3_B = '403B',
-+ FOUR_5_7_B = '457b',
-+ FIVE_2_9 = '529',
-+ BROKERAGE = 'brokerage',
-+ CASH_ISA = 'cash isa',
-+ EDUCATION_SAVINGS_ACCOUNT = 'education savings account',
-+ FIXED_ANNUNITY = 'fixed annuity',
-+ GIC = 'gic',
-+ HEALTH_REIMBURSEMENT_ARRANGEMENT = 'health reimbursement arrangement',
-+ IRA = 'ira',
-+ ISA = 'isa',
-+ KEOGH = 'keogh',
-+ LIF = 'lif',
-+ LIRA = 'lira',
-+ LRIF = 'lrif',
-+ LRSP = 'lrsp',
-+ MUTUAL_FUND = 'mutual fund',
-+ NON_TAXABLE_BROKERAGE_ACCOUNT = 'non-taxable brokerage account',
-+ PENSION = 'pension',
-+ PLAN = 'plan',
-+ PRIF = 'prif',
-+ PROFIT_SHARING_PLAN = 'profit sharing plan',
-+ RDSP = 'rdsp',
-+ RESP = 'resp',
-+ RETIREMENT = 'retirement',
-+ RLIF = 'rlif',
-+ ROTH_401K = 'roth 401k',
-+ ROTH = 'roth',
-+ RRIF = 'rrif',
-+ RRSP = 'rrsp',
-+ SARSEP = 'sarsep',
-+ SEP_IRA = 'sep ira',
-+ SIMPLE_IRA = 'simple ira',
-+ SIPP = 'sipp',
-+ STOCK_PLAN = 'stock plan',
-+ TFSA = 'tfsa',
-+ THRIFT_SAVINGS_PLAN = 'thrift savings plan',
-+ TRUST = 'trust',
-+ UGMA = 'ugma',
-+ UTMA = 'utma',
-+ VARIABLE_ANNUITY = 'variable annuity'
-+}
-+
-+export interface LinkAccountSubtype {
-+}
-+
-+export class LinkAccountSubtypeCredit implements LinkAccountSubtype {
-+ public static readonly ALL = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.ALL);
-+ public static readonly CREDIT_CARD = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.CREDIT_CARD);
-+ public static readonly PAYPAL = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.PAYPAL);
-+
-+ private constructor(public readonly type: LinkAccountType, public readonly subtype: LinkAccountSubtype) { }
-+}
-+
-+export class LinkAccountSubtypeDepository implements LinkAccountSubtype {
-+ public static readonly ALL = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.ALL);
-+ public static readonly CASH_MANAGEMENT = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CASH_MANAGEMENT);
-+ public static readonly CD = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CD);
-+ public static readonly CHECKING = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CHECKING);
-+ public static readonly EBT = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.EBT);
-+ public static readonly HSA = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.HSA);
-+ public static readonly MONEY_MARKET = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.MONEY_MARKET);
-+ public static readonly PAYPAL = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.PAYPAL);
-+ public static readonly PREPAID = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.PREPAID);
-+ public static readonly SAVINGS = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.SAVINGS);
-+
-+ private constructor(public readonly type: LinkAccountType, public readonly subtype: LinkAccountSubtype) { }
-+}
-+
-+export class LinkAccountSubtypeInvestment implements LinkAccountSubtype {
-+ public static readonly ALL = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ALL);
-+ public static readonly BROKERAGE = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.BROKERAGE);
-+ public static readonly CASH_ISA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.CASH_ISA);
-+ public static readonly EDUCATION_SAVINGS_ACCOUNT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.EDUCATION_SAVINGS_ACCOUNT);
-+ public static readonly FIXED_ANNUNITY = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FIXED_ANNUNITY);
-+ public static readonly GIC = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.GIC);
-+ public static readonly HEALTH_REIMBURSEMENT_ARRANGEMENT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.HEALTH_REIMBURSEMENT_ARRANGEMENT);
-+ public static readonly HSA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.HSA);
-+ public static readonly INVESTMENT_401A = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_1_A);
-+ public static readonly INVESTMENT_401K = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_1_K);
-+ public static readonly INVESTMENT_403B = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_3_B);
-+ public static readonly INVESTMENT_457B = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_5_7_B);
-+ public static readonly INVESTMENT_529 = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FIVE_2_9);
-+ public static readonly IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.IRA);
-+ public static readonly ISA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ISA);
-+ public static readonly KEOGH = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.KEOGH);
-+ public static readonly LIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LIF);
-+ public static readonly LIRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LIRA);
-+ public static readonly LRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LRIF);
-+ public static readonly LRSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LRSP);
-+ public static readonly MUTUAL_FUND = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.MUTUAL_FUND);
-+ public static readonly NON_TAXABLE_BROKERAGE_ACCOUNT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.NON_TAXABLE_BROKERAGE_ACCOUNT);
-+ public static readonly PENSION = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PENSION);
-+ public static readonly PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PLAN);
-+ public static readonly PRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PRIF);
-+ public static readonly PROFIT_SHARING_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PROFIT_SHARING_PLAN);
-+ public static readonly RDSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RDSP);
-+ public static readonly RESP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RESP);
-+ public static readonly RETIREMENT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RETIREMENT);
-+ public static readonly RLIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RLIF);
-+ public static readonly ROTH = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ROTH);
-+ public static readonly ROTH_401K = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ROTH_401K);
-+ public static readonly RRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RRIF);
-+ public static readonly RRSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RRSP);
-+ public static readonly SARSEP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SARSEP);
-+ public static readonly SEP_IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SEP_IRA);
-+ public static readonly SIMPLE_IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SIMPLE_IRA);
-+ public static readonly SIIP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SIPP);
-+ public static readonly STOCK_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.STOCK_PLAN);
-+ public static readonly TFSA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.TFSA);
-+ public static readonly THRIFT_SAVINGS_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.THRIFT_SAVINGS_PLAN);
-+ public static readonly TRUST = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.TRUST);
-+ public static readonly UGMA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.UGMA);
-+ public static readonly UTMA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.UTMA);
-+ public static readonly VARIABLE_ANNUITY = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.VARIABLE_ANNUITY);
-+
-+ private constructor(public readonly type: LinkAccountType, public readonly subtype: LinkAccountSubtype) { }
-+}
-+
-+export class LinkAccountSubtypeLoan implements LinkAccountSubtype {
-+ public static readonly ALL = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.ALL);
-+ public static readonly AUTO = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.AUTO);
-+ public static readonly BUSINESS = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.BUSINESS);
-+ public static readonly COMMERCIAL = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.COMMERCIAL);
-+ public static readonly CONSTRUCTION = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.CONSTRUCTION);
-+ public static readonly CONSUMER = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.CONSUMER);
-+ public static readonly HOME_EQUITY = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.HOME_EQUITY);
-+ public static readonly LINE_OF_CREDIT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.LINE_OF_CREDIT);
-+ public static readonly LOAN = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.LOAN);
-+ public static readonly MORTGAGE = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.MORTGAGE);
-+ public static readonly OVERDRAFT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.OVERDRAFT);
-+ public static readonly STUDENT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.STUDENT);
-+
-+ private constructor(public readonly type: LinkAccountType, public readonly subtype: LinkAccountSubtype) { }
-+}
-+
-+export class LinkAccountSubtypeUnknown implements LinkAccountSubtype {
-+ constructor(public readonly type: string, public readonly subtype: string) { }
-+}
-+
-+export interface LinkSuccess {
-+ publicToken: string;
-+ metadata: LinkSuccessMetadata;
-+}
-+
-+export interface LinkSuccessMetadata {
-+ institution?: LinkInstitution;
-+ accounts: LinkAccount[];
-+ linkSessionId: string;
-+ metadataJson?: string;
-+}
-+
-+export interface LinkAccount {
-+ id: string;
-+ name?: string;
-+ mask?: string;
-+ type: LinkAccountType;
-+ subtype: LinkAccountSubtype;
-+ verificationStatus?: LinkAccountVerificationStatus;
-+}
-+
-+export enum LinkAccountVerificationStatus {
-+ PENDING_AUTOMATIC_VERIFICATION = 'pending_automatic_verification',
-+ PENDING_MANUAL_VERIFICATION = 'pending_manual_verification',
-+ MANUALLY_VERIFIED = 'manually_verified',
-+}
-+
-+export interface LinkInstitution {
-+ id: string;
-+ name: string;
-+}
-+
-+export interface LinkExit {
-+ error?: LinkError;
-+ metadata: LinkExitMetadata;
-+}
-+
-+export interface LinkExitMetadata {
-+ status?: LinkExitMetadataStatus;
-+ institution?: LinkInstitution;
-+ linkSessionId: string;
-+ requestId: string;
-+ metadataJson?: string;
-+}
-+
-+export enum LinkExitMetadataStatus {
-+ CONNECTED = 'connected',
-+ CHOOSE_DEVICE = 'choose_device',
-+ REQUIRES_ACCOUNT_SELECTION = 'requires_account_selection',
-+ REQUIRES_CODE = 'requires_code',
-+ REQUIRES_CREDENTIALS = 'requires_credentials',
-+ REQUIRES_EXTERNAL_ACTION = 'requires_external_action',
-+ REQUIRES_OAUTH = 'requires_oauth',
-+ REQUIRES_QUESTIONS = 'requires_questions',
-+ REQUIRES_RECAPTCHA = 'requires_recaptcha',
-+ REQUIRES_SELECTIONS = 'requires_selections',
-+ REQUIRES_DEPOSIT_SWITCH_ALLOCATION_CONFIGURATION = 'requires_deposit_switch_allocation_configuration',
-+ REQUIRES_DEPOSIT_SWITCH_ALLOCATION_SELECTION = 'requires_deposit_switch_allocation_selection',
-+}
-+
-+export interface LinkError {
-+ errorCode: LinkErrorCode;
-+ errorType: LinkErrorType;
-+ errorMessage: string;
-+ /** @deprecated DO NOT USE, data not guaranteed. Use `displayMessage` instead */
-+ errorDisplayMessage?: string;
-+ displayMessage?: string;
-+ errorJson?: string;
-+}
-+
-+export enum LinkErrorCode {
-+ // ITEM_ERROR
-+ INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
-+ INVALID_MFA = "INVALID_MFA",
-+ ITEM_LOGIN_REQUIRED = "ITEM_LOGIN_REQUIRED",
-+ INSUFFICIENT_CREDENTIALS = "INSUFFICIENT_CREDENTIALS",
-+ ITEM_LOCKED = "ITEM_LOCKED",
-+ USER_SETUP_REQUIRED = "USER_SETUP_REQUIRED",
-+ MFA_NOT_SUPPORTED = "MFA_NOT_SUPPORTED",
-+ INVALID_SEND_METHOD = "INVALID_SEND_METHOD",
-+ NO_ACCOUNTS = "NO_ACCOUNTS",
-+ ITEM_NOT_SUPPORTED = "ITEM_NOT_SUPPORTED",
-+ TOO_MANY_VERIFICATION_ATTEMPTS = "TOO_MANY_VERIFICATION_ATTEMPTS",
-+
-+ INVALD_UPDATED_USERNAME = "INVALD_UPDATED_USERNAME",
-+ INVALID_UPDATED_USERNAME = "INVALID_UPDATED_USERNAME",
-+
-+ ITEM_NO_ERROR = "ITEM_NO_ERROR",
-+ item_no_error = "item-no-error",
-+ NO_AUTH_ACCOUNTS = "NO_AUTH_ACCOUNTS",
-+ NO_INVESTMENT_ACCOUNTS = "NO_INVESTMENT_ACCOUNTS",
-+ NO_INVESTMENT_AUTH_ACCOUNTS = "NO_INVESTMENT_AUTH_ACCOUNTS",
-+ NO_LIABILITY_ACCOUNTS = "NO_LIABILITY_ACCOUNTS",
-+ PRODUCTS_NOT_SUPPORTED = "PRODUCTS_NOT_SUPPORTED",
-+ ITEM_NOT_FOUND = "ITEM_NOT_FOUND",
-+ ITEM_PRODUCT_NOT_READY = "ITEM_PRODUCT_NOT_READY",
-+
-+ // INSTITUTION_ERROR
-+ INSTITUTION_DOWN = "INSTITUTION_DOWN",
-+ INSTITUTION_NOT_RESPONDING = "INSTITUTION_NOT_RESPONDING",
-+ INSTITUTION_NOT_AVAILABLE = "INSTITUTION_NOT_AVAILABLE",
-+ INSTITUTION_NO_LONGER_SUPPORTED = "INSTITUTION_NO_LONGER_SUPPORTED",
-+
-+ // API_ERROR
-+ INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
-+ PLANNED_MAINTENANCE = "PLANNED_MAINTENANCE",
-+
-+ // ASSET_REPORT_ERROR
-+ PRODUCT_NOT_ENABLED = "PRODUCT_NOT_ENABLED",
-+ DATA_UNAVAILABLE = "DATA_UNAVAILABLE",
-+ ASSET_PRODUCT_NOT_READY = "ASSET_PRODUCT_NOT_READY",
-+ ASSET_REPORT_GENERATION_FAILED = "ASSET_REPORT_GENERATION_FAILED",
-+ INVALID_PARENT = "INVALID_PARENT",
-+ INSIGHTS_NOT_ENABLED = "INSIGHTS_NOT_ENABLED",
-+ INSIGHTS_PREVIOUSLY_NOT_ENABLED = "INSIGHTS_PREVIOUSLY_NOT_ENABLED",
-+
-+ // BANK_TRANSFER_ERROR
-+ BANK_TRANSFER_LIMIT_EXCEEDED = "BANK_TRANSFER_LIMIT_EXCEEDED",
-+ BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT = "BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT",
-+ BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT = "BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT",
-+ BANK_TRANSFER_ACCOUNT_BLOCKED = "BANK_TRANSFER_ACCOUNT_BLOCKED",
-+ BANK_TRANSFER_INSUFFICIENT_FUNDS = "BANK_TRANSFER_INSUFFICIENT_FUNDS",
-+ BANK_TRANSFER_NOT_CANCELLABLE = "BANK_TRANSFER_NOT_CANCELLABLE",
-+ BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE = "BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE",
-+ BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT = "BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT",
-+
-+ // SANDBOX_ERROR
-+ SANDBOX_PRODUCT_NOT_ENABLED = "SANDBOX_PRODUCT_NOT_ENABLED",
-+ SANDBOX_WEBHOOK_INVALID = "SANDBOX_WEBHOOK_INVALID",
-+ SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID = "SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID",
-+
-+ // INVALID_REQUEST
-+ MISSING_FIELDS = "MISSING_FIELDS",
-+ UNKNOWN_FIELDS = "UNKNOWN_FIELDS",
-+ INVALID_FIELD = "INVALID_FIELD",
-+ INCOMPATIBLE_API_VERSION = "INCOMPATIBLE_API_VERSION",
-+ INVALID_BODY = "INVALID_BODY",
-+ INVALID_HEADERS = "INVALID_HEADERS",
-+ NOT_FOUND = "NOT_FOUND",
-+ NO_LONGER_AVAILABLE = "NO_LONGER_AVAILABLE",
-+ SANDBOX_ONLY = "SANDBOX_ONLY",
-+ INVALID_ACCOUNT_NUMBER = "INVALID_ACCOUNT_NUMBER",
-+
-+ // INVALID_INPUT
-+ // From above ITEM_LOGIN_REQUIRED = "INVALID_CREDENTIALS",
-+ INCORRECT_DEPOSIT_AMOUNTS = "INCORRECT_DEPOSIT_AMOUNTS",
-+ UNAUTHORIZED_ENVIRONMENT = "UNAUTHORIZED_ENVIRONMENT",
-+ INVALID_PRODUCT = "INVALID_PRODUCT",
-+ UNAUTHORIZED_ROUTE_ACCESS = "UNAUTHORIZED_ROUTE_ACCESS",
-+ DIRECT_INTEGRATION_NOT_ENABLED = "DIRECT_INTEGRATION_NOT_ENABLED",
-+ INVALID_API_KEYS = "INVALID_API_KEYS",
-+ INVALID_ACCESS_TOKEN = "INVALID_ACCESS_TOKEN",
-+ INVALID_PUBLIC_TOKEN = "INVALID_PUBLIC_TOKEN",
-+ INVALID_LINK_TOKEN = "INVALID_LINK_TOKEN",
-+ INVALID_PROCESSOR_TOKEN = "INVALID_PROCESSOR_TOKEN",
-+ INVALID_AUDIT_COPY_TOKEN = "INVALID_AUDIT_COPY_TOKEN",
-+ INVALID_ACCOUNT_ID = "INVALID_ACCOUNT_ID",
-+ MICRODEPOSITS_ALREADY_VERIFIED = "MICRODEPOSITS_ALREADY_VERIFIED",
-+
-+ // INVALID_RESULT
-+ PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA = "PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA",
-+
-+ // RATE_LIMIT_EXCEEDED
-+ ACCOUNTS_LIMIT = "ACCOUNTS_LIMIT",
-+ ADDITION_LIMIT = "ADDITION_LIMIT",
-+ AUTH_LIMIT = "AUTH_LIMIT",
-+ BALANCE_LIMIT = "BALANCE_LIMIT",
-+ IDENTITY_LIMIT = "IDENTITY_LIMIT",
-+ ITEM_GET_LIMIT = "ITEM_GET_LIMIT",
-+ RATE_LIMIT = "RATE_LIMIT",
-+ TRANSACTIONS_LIMIT = "TRANSACTIONS_LIMIT",
-+
-+ // RECAPTCHA_ERROR
-+ RECAPTCHA_REQUIRED = "RECAPTCHA_REQUIRED",
-+ RECAPTCHA_BAD = "RECAPTCHA_BAD",
-+
-+ // OAUTH_ERROR
-+ INCORRECT_OAUTH_NONCE = "INCORRECT_OAUTH_NONCE",
-+ OAUTH_STATE_ID_ALREADY_PROCESSED = "OAUTH_STATE_ID_ALREADY_PROCESSED",
-+}
-+
-+export enum LinkErrorType {
-+ BANK_TRANSFER = 'BANK_TRANSFER_ERROR',
-+ INVALID_REQUEST = 'INVALID_REQUEST',
-+ INVALID_RESULT = 'INVALID_RESULT',
-+ INVALID_INPUT = 'INVALID_INPUT',
-+ INSTITUTION_ERROR = 'INSTITUTION_ERROR',
-+ RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
-+ API_ERROR = 'API_ERROR',
-+ ITEM_ERROR = 'ITEM_ERROR',
-+ AUTH_ERROR = 'AUTH_ERROR',
-+ ASSET_REPORT_ERROR = 'ASSET_REPORT_ERROR',
-+ SANDBOX_ERROR = 'SANDBOX_ERROR',
-+ RECAPTCHA_ERROR = 'RECAPTCHA_ERROR',
-+ OAUTH_ERROR = 'OAUTH_ERROR',
-+}
-+
-+export type LinkEventListener = (linkEvent: LinkEvent) => void
-+
-+export interface LinkEvent {
-+ eventName: LinkEventName;
-+ metadata: LinkEventMetadata;
-+}
-+
-+export interface LinkEventMetadata {
-+ accountNumberMask?: string;
-+ linkSessionId: string;
-+ mfaType?: string;
-+ requestId?: string;
-+ viewName: LinkEventViewName;
-+ errorCode?: string;
-+ errorMessage?: string;
-+ errorType?: string;
-+ exitStatus?: string;
-+ institutionId?: string;
-+ institutionName?: string;
-+ institutionSearchQuery?: string;
-+ isUpdateMode?: string;
-+ matchReason?: string;
-+ // see possible values for selection at https://plaid.com/docs/link/web/#link-web-onevent-selection
-+ selection?: null | string;
-+ timestamp: string;
-+}
-+
-+export enum LinkEventName {
-+ BANK_INCOME_INSIGHTS_COMPLETED = "BANK_INCOME_INSIGHTS_COMPLETED",
-+ CLOSE_OAUTH = 'CLOSE_OAUTH',
-+ ERROR = 'ERROR',
-+ EXIT = 'EXIT',
-+ FAIL_OAUTH = 'FAIL_OAUTH',
-+ HANDOFF = 'HANDOFF',
-+ IDENTITY_VERIFICATION_START_STEP = 'IDENTITY_VERIFICATION_START_STEP',
-+ IDENTITY_VERIFICATION_PASS_STEP = 'IDENTITY_VERIFICATION_PASS_STEP',
-+ IDENTITY_VERIFICATION_FAIL_STEP = 'IDENTITY_VERIFICATION_FAIL_STEP',
-+ IDENTITY_VERIFICATION_PENDING_REVIEW_STEP = 'IDENTITY_VERIFICATION_PENDING_REVIEW_STEP',
-+ IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION = 'IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION',
-+ IDENTITY_VERIFICATION_CREATE_SESSION = 'IDENTITY_VERIFICATION_CREATE_SESSION',
-+ IDENTITY_VERIFICATION_RESUME_SESSION = 'IDENTITY_VERIFICATION_RESUME_SESSION',
-+ IDENTITY_VERIFICATION_PASS_SESSION = 'IDENTITY_VERIFICATION_PASS_SESSION',
-+ IDENTITY_VERIFICATION_FAIL_SESSION = 'IDENTITY_VERIFICATION_FAIL_SESSION',
-+ IDENTITY_VERIFICATION_OPEN_UI = 'IDENTITY_VERIFICATION_OPEN_UI',
-+ IDENTITY_VERIFICATION_RESUME_UI = 'IDENTITY_VERIFICATION_RESUME_UI',
-+ IDENTITY_VERIFICATION_CLOSE_UI = 'IDENTITY_VERIFICATION_CLOSE_UI',
-+ MATCHED_CONSENT = 'MATCHED_CONSENT',
-+ MATCHED_SELECT_INSTITUTION = 'MATCHED_SELECT_INSTITUTION',
-+ MATCHED_SELECT_VERIFY_METHOD = 'MATCHED_SELECT_VERIFY_METHOD',
-+ OPEN = 'OPEN',
-+ OPEN_MY_PLAID = 'OPEN_MY_PLAID',
-+ OPEN_OAUTH = 'OPEN_OAUTH',
-+ SEARCH_INSTITUTION = 'SEARCH_INSTITUTION',
-+ SELECT_DEGRADED_INSTITUTION = 'SELECT_DEGRADED_INSTITUTION',
-+ SELECT_DOWN_INSTITUTION = 'SELECT_DOWN_INSTITUTION',
-+ SELECT_FILTERED_INSTITUTION = 'SELECT_FILTERED_INSTITUTION',
-+ SELECT_INSTITUTION = 'SELECT_INSTITUTION',
-+ SELECT_BRAND = 'SELECT_BRAND',
-+ SELECT_AUTH_TYPE = 'SELECT_AUTH_TYPE',
-+ SUBMIT_ACCOUNT_NUMBER = 'SUBMIT_ACCOUNT_NUMBER',
-+ SUBMIT_DOCUMENTS = 'SUBMIT_DOCUMENTS',
-+ SUBMIT_DOCUMENTS_SUCCESS = 'SUBMIT_DOCUMENTS_SUCCESS',
-+ SUBMIT_DOCUMENTS_ERROR = 'SUBMIT_DOCUMENTS_ERROR',
-+ SUBMIT_ROUTING_NUMBER = 'SUBMIT_ROUTING_NUMBER',
-+ VIEW_DATA_TYPES = 'VIEW_DATA_TYPES',
-+ SUBMIT_PHONE = 'SUBMIT_PHONE',
-+ SKIP_SUBMIT_PHONE = 'SKIP_SUBMIT_PHONE',
-+ VERIFY_PHONE = 'VERIFY_PHONE',
-+ SUBMIT_CREDENTIALS = 'SUBMIT_CREDENTIALS',
-+ SUBMIT_MFA = 'SUBMIT_MFA',
-+ TRANSITION_VIEW = 'TRANSITION_VIEW',
-+ CONNECT_NEW_INSTITUTION = 'CONNECT_NEW_INSTITUTION',
-+}
-+
-+export enum LinkEventViewName {
-+ ACCEPT_TOS = 'ACCEPT_TOS',
-+ CONNECTED = 'CONNECTED',
-+ CONSENT = 'CONSENT',
-+ CREDENTIAL = 'CREDENTIAL',
-+ DATA_TRANSPARENCY = 'DATA_TRANSPARENCY',
-+ DATA_TRANSPARENCY_CONSENT = 'DATA_TRANSPARENCY_CONSENT',
-+ DOCUMENTARY_VERIFICATION = 'DOCUMENTARY_VERIFICATION',
-+ ERROR = 'ERROR',
-+ EXIT = 'EXIT',
-+ KYC_CHECK = 'KYC_CHECK',
-+ SELFIE_CHECK = 'SELFIE_CHECK',
-+ LOADING = 'LOADING',
-+ MATCHED_CONSENT = 'MATCHED_CONSENT',
-+ MATCHED_CREDENTIAL = 'MATCHED_CREDENTIAL',
-+ MATCHED_MFA = 'MATCHED_MFA',
-+ MFA = 'MFA',
-+ NUMBERS = 'NUMBERS',
-+ NUMBERS_SELECT_INSTITUTION = 'NUMBERS_SELECT_INSTITUTION',
-+ OAUTH = 'OAUTH',
-+ RECAPTCHA = 'RECAPTCHA',
-+ RISK_CHECK = 'RISK_CHECK',
-+ SCREENING = 'SCREENING',
-+ SELECT_ACCOUNT = 'SELECT_ACCOUNT',
-+ SELECT_AUTH_TYPE = 'SELECT_AUTH_TYPE',
-+ SUBMIT_PHONE = 'SUBMIT_PHONE',
-+ VERIFY_PHONE = 'VERIFY_PHONE',
-+ SELECT_SAVED_INSTITUTION = 'SELECT_SAVED_INSTITUTION',
-+ SELECT_SAVED_ACCOUNT = 'SELECT_SAVED_ACCOUNT',
-+ SELECT_BRAND = 'SELECT_BRAND',
-+ SELECT_INSTITUTION = 'SELECT_INSTITUTION',
-+ SUBMIT_DOCUMENTS = 'SUBMIT_DOCUMENTS',
-+ SUBMIT_DOCUMENTS_SUCCESS = 'SUBMIT_DOCUMENTS_SUCCESS',
-+ SUBMIT_DOCUMENTS_ERROR = 'SUBMIT_DOCUMENTS_ERROR',
-+ UPLOAD_DOCUMENTS = 'UPLOAD_DOCUMENTS',
-+ VERIFY_SMS = 'VERIFY_SMS',
-+}
-+
-+/// Methods to present Link on iOS.
-+/// FULL_SCREEN is the converts to UIModalPresentationOverFullScreen on the native side.
-+/// MODAL will use the default presentation style for iOS which is UIModalPresentationAutomatic.
-+export enum LinkIOSPresentationStyle {
-+ FULL_SCREEN = 'FULL_SCREEN',
-+ MODAL = 'MODAL'
-+}
-+
-+export type LinkSuccessListener = (LinkSuccess: LinkSuccess) => void
-+
-+export type LinkExitListener = (LinkExit: LinkExit) => void
-+
-+export type LinkOnEventListener = (LinkEvent: LinkEvent) => void
-+
-+export interface PlaidLinkProps {
-+ tokenConfig: LinkTokenConfiguration
-+ onSuccess: LinkSuccessListener
-+ onExit?: LinkExitListener
-+ iOSPresentationStyle?: LinkIOSPresentationStyle
-+ logLevel?: LinkLogLevel
-+ onPress?(): any
-+}
-+
-+export type PlaidLinkComponentProps = (PlaidLinkProps & {
-+ children: React.ReactNode
-+});
-diff --git a/node_modules/react-native-plaid-link-sdk/src/__tests__/Types.tests.ts b/node_modules/react-native-plaid-link-sdk/src/__tests__/Types.tests.ts
-new file mode 100644
-index 0000000..2dd7a54
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/__tests__/Types.tests.ts
-@@ -0,0 +1,15 @@
-+const Types = require('./../Types');
-+
-+test('test token configuration', () => {
-+ const linkTokenConfiguration = {
-+ token: "test-token",
-+ noLoadingState: false,
-+ logLevel: Types.LinkLogLevel.DEBUG,
-+ extras: null,
-+ };
-+
-+ expect(linkTokenConfiguration.noLoadingState).toBe(false);
-+ expect(linkTokenConfiguration.token).toBe("test-token");
-+ expect(linkTokenConfiguration.logLevel).toBe(Types.LinkLogLevel.DEBUG);
-+ expect(linkTokenConfiguration.extras).toBe(null);
-+});
-\ No newline at end of file
-diff --git a/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleAndroid.ts b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleAndroid.ts
-new file mode 100644
-index 0000000..d1e4062
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleAndroid.ts
-@@ -0,0 +1,20 @@
-+// we use Object type because methods on the native side use NSDictionary and ReadableMap
-+// and we want to stay compatible with those
-+import {TurboModuleRegistry, TurboModule} from 'react-native';
-+import { Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes';
-+
-+export interface Spec extends TurboModule {
-+ startLinkActivityForResult(
-+ token: string,
-+ noLoadingState: boolean,
-+ logLevel: string,
-+ onSuccessCallback: (result: UnsafeObject) => void,
-+ onExitCallback: (result: UnsafeObject) => void
-+ ): void;
-+
-+ // those two are here for event emitter methods
-+ addListener(eventName: string): void;
-+ removeListeners(count: Int32): void;
-+}
-+
-+export default TurboModuleRegistry.get('PlaidAndroid');
-diff --git a/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts
-new file mode 100644
-index 0000000..d1b3565
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts
-@@ -0,0 +1,19 @@
-+// we use Object type because methods on the native side use NSDictionary and ReadableMap
-+// and we want to stay compatible with those
-+import {TurboModuleRegistry, TurboModule} from 'react-native';
-+import { Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes';
-+
-+export interface Spec extends TurboModule {
-+ create(token: string, noLoadingState: boolean): void;
-+ open(
-+ fullScreen: boolean,
-+ onSuccess: (success: UnsafeObject) => void,
-+ onExit: (error: UnsafeObject, result: UnsafeObject) => void,
-+ ): void;
-+ dismiss(): void;
-+ // those two are here for event emitter methods
-+ addListener(eventName: string): void;
-+ removeListeners(count: Int32): void;
-+}
-+
-+export default TurboModuleRegistry.get('RNLinksdk');
-diff --git a/node_modules/react-native-plaid-link-sdk/src/index.ts b/node_modules/react-native-plaid-link-sdk/src/index.ts
-new file mode 100644
-index 0000000..23ef946
---- /dev/null
-+++ b/node_modules/react-native-plaid-link-sdk/src/index.ts
-@@ -0,0 +1,21 @@
-+import {
-+ openLink,
-+ dismissLink,
-+ usePlaidEmitter,
-+ PlaidLink,
-+} from './PlaidLink';
-+
-+export * from './Types';
-+
-+export default PlaidLink;
-+
-+export {
-+ PlaidLink,
-+ openLink,
-+ dismissLink,
-+ usePlaidEmitter,
-+};
-+
-+// Components
-+
-+export { EmbeddedLinkView } from './EmbeddedLink/EmbeddedLinkView';
-\ No newline at end of file
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/patches/react-native-screens+3.30.1+001+fix-screen-type.patch b/patches/react-native-screens+3.30.1+001+fix-screen-type.patch
deleted file mode 100644
index f282ec58b07b..000000000000
--- a/patches/react-native-screens+3.30.1+001+fix-screen-type.patch
+++ /dev/null
@@ -1,12 +0,0 @@
-diff --git a/node_modules/react-native-screens/src/components/Screen.tsx b/node_modules/react-native-screens/src/components/Screen.tsx
-index 3f9a1cb..45767f7 100644
---- a/node_modules/react-native-screens/src/components/Screen.tsx
-+++ b/node_modules/react-native-screens/src/components/Screen.tsx
-@@ -79,6 +79,7 @@ export class InnerScreen extends React.Component {
- // Due to how Yoga resolves layout, we need to have different components for modal nad non-modal screens
- const AnimatedScreen =
- Platform.OS === 'android' ||
-+ stackPresentation === undefined ||
- stackPresentation === 'push' ||
- stackPresentation === 'containedModal' ||
- stackPresentation === 'containedTransparentModal'
diff --git a/patches/react-native-web+0.19.12+003+image-header-support.patch b/patches/react-native-web+0.19.12+003+image-header-support.patch
index 6652f0345cc4..d0a490a4ed70 100644
--- a/patches/react-native-web+0.19.12+003+image-header-support.patch
+++ b/patches/react-native-web+0.19.12+003+image-header-support.patch
@@ -1,5 +1,5 @@
diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js
-index 9649d27..3281cc8 100644
+index 9649d27..66ef95c 100644
--- a/node_modules/react-native-web/dist/exports/Image/index.js
+++ b/node_modules/react-native-web/dist/exports/Image/index.js
@@ -135,7 +135,22 @@ function resolveAssetUri(source) {
@@ -47,7 +47,7 @@ index 9649d27..3281cc8 100644
});
}
function abortPendingRequest() {
-@@ -279,10 +288,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => {
+@@ -279,10 +288,79 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => {
suppressHydrationWarning: true
}), hiddenImage, createTintColorSVG(tintColor, filterRef.current));
});
@@ -64,24 +64,20 @@ index 9649d27..3281cc8 100644
+ var _React$useState3 = React.useState(''),
+ blobUri = _React$useState3[0],
+ setBlobUri = _React$useState3[1];
-+ var request = React.useRef({
-+ cancel: () => {},
-+ source: {
-+ uri: '',
-+ headers: {}
-+ },
-+ promise: Promise.resolve('')
-+ });
++ var request = React.useRef(null);
+ var onError = props.onError,
+ onLoadStart = props.onLoadStart,
+ onLoadEnd = props.onLoadEnd;
+ React.useEffect(() => {
-+ if (!hasSourceDiff(nextSource, request.current.source)) {
++ if (request.current !== null && !hasSourceDiff(nextSource, request.current.source)) {
+ return;
+ }
+
+ // When source changes we want to clean up any old/running requests
-+ request.current.cancel();
++ if (request.current !== null) {
++ request.current.cancel();
++ }
++
+ if (onLoadStart) {
+ onLoadStart();
+ }
@@ -96,7 +92,12 @@ index 9649d27..3281cc8 100644
+ }, [nextSource, onLoadStart, onError, onLoadEnd]);
+
+ // Cancel any request on unmount
-+ React.useEffect(() => request.current.cancel, []);
++ React.useEffect(() => () => {
++ if (request.current !== null) {
++ request.current.cancel();
++ request.current = null;
++ }
++ }, []);
+ var propsToPass = _objectSpread(_objectSpread({}, props), {}, {
+ // `onLoadStart` is called from the current component
+ // We skip passing it down to prevent BaseImage raising it a 2nd time
diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh
new file mode 100755
index 000000000000..4ce023755258
--- /dev/null
+++ b/scripts/applyPatches.sh
@@ -0,0 +1,49 @@
+#!/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.
+function patchPackage {
+ OS="$(uname)"
+ if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then
+ npx patch-package --error-on-fail --color=always
+ else
+ error "Unsupported OS: $OS"
+ exit 1
+ fi
+}
+
+# Run patch-package and capture its output and exit code, while still displaying the original output to the terminal
+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/scripts/symbolicate-profile.ts b/scripts/symbolicate-profile.ts
index a100c05029dd..a58c2894edb6 100755
--- a/scripts/symbolicate-profile.ts
+++ b/scripts/symbolicate-profile.ts
@@ -32,7 +32,7 @@ if (Object.keys(argsMap).length === 0 || argsMap.help !== undefined) {
Logger.log('Options:');
Logger.log(' --profile= The .cpuprofile file to symbolicate');
Logger.log(' --platform= The platform for which the source map was uploaded');
- Logger.log(' --gh-token Token to use for requests send to the GitHub API. By default tries to pick up from the environment variable GITHUB_TOKEN');
+ Logger.log(' --ghToken Token to use for requests send to the GitHub API. By default tries to pick up from the environment variable GITHUB_TOKEN');
Logger.log(' --help Display this help message');
process.exit(0);
}
@@ -53,7 +53,7 @@ if (argsMap.platform === undefined) {
const githubToken = argsMap.ghToken ?? process.env.GITHUB_TOKEN;
if (githubToken === undefined) {
- Logger.error('No GitHub token provided. Either set a GITHUB_TOKEN environment variable or pass it using --gh-token');
+ Logger.error('No GitHub token provided. Either set a GITHUB_TOKEN environment variable or pass it using --ghToken');
process.exit(1);
}
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 233b35e6ac4b..e2ba7ef0aa1d 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -74,6 +74,10 @@ const onboardingChoices = {
type OnboardingPurposeType = ValueOf;
const CONST = {
+ RECENT_WAYPOINTS_NUMBER: 20,
+ DEFAULT_DB_NAME: 'OnyxDB',
+ DEFAULT_TABLE_NAME: 'keyvaluepairs',
+ DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt',
DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL],
// Note: Group and Self-DM excluded as these are not tied to a Workspace
@@ -331,6 +335,8 @@ const CONST = {
VERIFICATION_MAX_ATTEMPTS: 7,
STATE: {
VERIFYING: 'VERIFYING',
+ VALIDATING: 'VALIDATING',
+ SETUP: 'SETUP',
PENDING: 'PENDING',
OPEN: 'OPEN',
},
@@ -357,13 +363,14 @@ const CONST = {
DEFAULT_ROOMS: 'defaultRooms',
VIOLATIONS: 'violations',
DUPE_DETECTION: 'dupeDetection',
- REPORT_FIELDS: 'reportFields',
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission',
SPOTNANA_TRAVEL: 'spotnanaTravel',
NETSUITE_ON_NEW_EXPENSIFY: 'netsuiteOnNewExpensify',
REPORT_FIELDS_FEATURE: 'reportFieldsFeature',
WORKSPACE_FEEDS: 'workspaceFeeds',
+ NETSUITE_USA_TAX: 'netsuiteUsaTax',
+ INTACCT_ON_NEW_EXPENSIFY: 'intacctOnNewExpensify',
},
BUTTON_STATES: {
DEFAULT: 'default',
@@ -603,6 +610,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`,
@@ -656,11 +664,13 @@ const CONST = {
MEMBER: 'member',
},
MAX_COUNT_BEFORE_FOCUS_UPDATE: 30,
+ MIN_INITIAL_REPORT_ACTION_COUNT: 15,
SPLIT_REPORTID: '-2',
ACTIONS: {
LIMIT: 50,
// OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts
TYPE: {
+ ACTIONABLE_ADD_PAYMENT_CARD: 'ACTIONABLEADDPAYMENTCARD',
ACTIONABLE_JOIN_REQUEST: 'ACTIONABLEJOINREQUEST',
ACTIONABLE_MENTION_WHISPER: 'ACTIONABLEMENTIONWHISPER',
ACTIONABLE_REPORT_MENTION_WHISPER: 'ACTIONABLEREPORTMENTIONWHISPER',
@@ -674,12 +684,12 @@ const CONST = {
CLOSED: 'CLOSED',
CREATED: 'CREATED',
DELEGATE_SUBMIT: 'DELEGATESUBMIT', // OldDot Action
- DELETED_ACCOUNT: 'DELETEDACCOUNT', // OldDot Action
+ DELETED_ACCOUNT: 'DELETEDACCOUNT', // Deprecated OldDot Action
DISMISSED_VIOLATION: 'DISMISSEDVIOLATION',
- DONATION: 'DONATION', // OldDot Action
+ DONATION: 'DONATION', // Deprecated OldDot Action
EXPORTED_TO_CSV: 'EXPORTCSV', // OldDot Action
EXPORTED_TO_INTEGRATION: 'EXPORTINTEGRATION', // OldDot Action
- EXPORTED_TO_QUICK_BOOKS: 'EXPORTED', // OldDot Action
+ EXPORTED_TO_QUICK_BOOKS: 'EXPORTED', // Deprecated OldDot Action
FORWARDED: 'FORWARDED', // OldDot Action
HOLD: 'HOLD',
HOLD_COMMENT: 'HOLDCOMMENT',
@@ -699,9 +709,9 @@ const CONST = {
REIMBURSEMENT_DELAYED: 'REIMBURSEMENTDELAYED', // OldDot Action
REIMBURSEMENT_QUEUED: 'REIMBURSEMENTQUEUED',
REIMBURSEMENT_DEQUEUED: 'REIMBURSEMENTDEQUEUED',
- REIMBURSEMENT_REQUESTED: 'REIMBURSEMENTREQUESTED', // OldDot Action
- REIMBURSEMENT_SETUP: 'REIMBURSEMENTSETUP', // OldDot Action
- REIMBURSEMENT_SETUP_REQUESTED: 'REIMBURSEMENTSETUPREQUESTED', // OldDot Action
+ REIMBURSEMENT_REQUESTED: 'REIMBURSEMENTREQUESTED', // Deprecated OldDot Action
+ REIMBURSEMENT_SETUP: 'REIMBURSEMENTSETUP', // Deprecated OldDot Action
+ REIMBURSEMENT_SETUP_REQUESTED: 'REIMBURSEMENTSETUPREQUESTED', // Deprecated OldDot Action
RENAMED: 'RENAMED',
REPORT_PREVIEW: 'REPORTPREVIEW',
SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action
@@ -714,7 +724,7 @@ const CONST = {
TASK_EDITED: 'TASKEDITED',
TASK_REOPENED: 'TASKREOPENED',
TRIPPREVIEW: 'TRIPPREVIEW',
- UNAPPROVED: 'UNAPPROVED', // OldDot Action
+ UNAPPROVED: 'UNAPPROVED',
UNHOLD: 'UNHOLD',
UNSHARE: 'UNSHARE', // OldDot Action
UPDATE_GROUP_CHAT_MEMBER_ROLE: 'UPDATEGROUPCHATMEMBERROLE',
@@ -785,6 +795,7 @@ const CONST = {
UPDATE_TIME_ENABLED: 'POLICYCHANGELOG_UPDATE_TIME_ENABLED',
UPDATE_TIME_RATE: 'POLICYCHANGELOG_UPDATE_TIME_RATE',
LEAVE_POLICY: 'POLICYCHANGELOG_LEAVE_POLICY',
+ CORPORATE_UPGRADE: 'POLICYCHANGELOG_CORPORATE_UPGRADE',
},
ROOM_CHANGE_LOG: {
INVITE_TO_ROOM: 'INVITETOROOM',
@@ -833,6 +844,10 @@ const CONST = {
TASK: 'task',
INVOICE: 'invoice',
},
+ UNSUPPORTED_TYPE: {
+ PAYCHECK: 'paycheck',
+ BILL: 'bill',
+ },
CHAT_TYPE: chatTypes,
WORKSPACE_CHAT_ROOMS: {
ANNOUNCE: '#announce',
@@ -885,6 +900,10 @@ const CONST = {
INDIVIDUAL: 'individual',
BUSINESS: 'policy',
},
+ EXPORT_OPTIONS: {
+ EXPORT_TO_INTEGRATION: 'exportToIntegration',
+ MARK_AS_EXPORTED: 'markAsExported',
+ },
},
NEXT_STEP: {
FINISHED: 'Finished!',
@@ -1115,8 +1134,6 @@ const CONST = {
// around each header.
EMOJI_NUM_PER_ROW: 8,
- EMOJI_FREQUENT_ROW_COUNT: 3,
-
EMOJI_DEFAULT_SKIN_TONE: -1,
// Amount of emojis to render ahead at the end of the update cycle
@@ -1195,6 +1212,8 @@ const CONST = {
NOTE: 'n',
},
+ IMAGE_HIGH_RESOLUTION_THRESHOLD: 7000,
+
IMAGE_OBJECT_POSITION: {
TOP: 'top',
INITIAL: 'initial',
@@ -1237,7 +1256,7 @@ const CONST = {
MAX_AMOUNT_OF_SUGGESTIONS: 20,
MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER: 5,
HERE_TEXT: '@here',
- SUGGESTION_BOX_MAX_SAFE_DISTANCE: 38,
+ SUGGESTION_BOX_MAX_SAFE_DISTANCE: 10,
BIG_SCREEN_SUGGESTION_WIDTH: 300,
},
COMPOSER_MAX_HEIGHT: 125,
@@ -1287,6 +1306,7 @@ const CONST = {
REPORT_FIELD: 'REPORT_FIELD',
NOT_IMPORTED: 'NOT_IMPORTED',
IMPORTED: 'IMPORTED',
+ NETSUITE_DEFAULT: 'NETSUITE_DEFAULT',
},
QUICKBOOKS_ONLINE: 'quickbooksOnline',
@@ -1337,8 +1357,46 @@ const CONST = {
},
},
- NETSUITE_CONFIG: {
- SUBSIDIARY: 'subsidiary',
+ 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',
+ ENTITY: 'entity',
+ },
+
+ SAGE_INTACCT: {
+ APPROVAL_MODE: {
+ APPROVAL_MANUAL: 'APPROVAL_MANUAL',
+ },
},
QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE: {
@@ -1347,12 +1405,263 @@ const CONST = {
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',
+ EXPORT_DATE: 'exportDate',
+ REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'reimbursableExpensesExportDestination',
+ NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'nonreimbursableExpensesExportDestination',
+ DEFAULT_VENDOR: 'defaultVendor',
+ REIMBURSABLE_PAYABLE_ACCOUNT: 'reimbursablePayableAccount',
+ PAYABLE_ACCT: 'payableAcct',
+ JOURNAL_POSTING_PREFERENCE: 'journalPostingPreference',
+ RECEIVABLE_ACCOUNT: 'receivableAccount',
+ INVOICE_ITEM_PREFERENCE: 'invoiceItemPreference',
+ INVOICE_ITEM: 'invoiceItem',
+ TAX_POSTING_ACCOUNT: 'taxPostingAccount',
+ PROVINCIAL_TAX_POSTING_ACCOUNT: 'provincialTaxPostingAccount',
+ ALLOW_FOREIGN_CURRENCY: 'allowForeignCurrency',
+ EXPORT_TO_NEXT_OPEN_PERIOD: 'exportToNextOpenPeriod',
+ IMPORT_FIELDS: ['departments', 'classes', 'locations'],
+ AUTO_SYNC: 'autoSync',
+ REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID',
+ COLLECTION_ACCOUNT: 'collectionAccount',
+ AUTO_CREATE_ENTITIES: 'autoCreateEntities',
+ APPROVAL_ACCOUNT: 'approvalAccount',
+ CUSTOM_FORM_ID_OPTIONS: 'customFormIDOptions',
+ TOKEN_INPUT_STEP_NAMES: ['1', '2,', '3', '4', '5'],
+ TOKEN_INPUT_STEP_KEYS: {
+ 0: 'installBundle',
+ 1: 'enableTokenAuthentication',
+ 2: 'enableSoapServices',
+ 3: 'createAccessToken',
+ 4: 'enterCredentials',
+ },
+ 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',
+ },
+ SYNC_OPTIONS: {
+ SYNC_REIMBURSED_REPORTS: 'syncReimbursedReports',
+ SYNC_PEOPLE: 'syncPeople',
+ ENABLE_NEW_CATEGORIES: 'enableNewCategories',
+ EXPORT_REPORTS_TO: 'exportReportsTo',
+ EXPORT_VENDOR_BILLS_TO: 'exportVendorBillsTo',
+ EXPORT_JOURNALS_TO: 'exportJournalsTo',
+ SYNC_TAX: 'syncTax',
+ CROSS_SUBSIDIARY_CUSTOMERS: 'crossSubsidiaryCustomers',
+ CUSTOMER_MAPPINGS: {
+ CUSTOMERS: 'customers',
+ 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: {
+ LAST_EXPENSE: 'LAST_EXPENSE',
+ EXPORTED: 'EXPORTED',
+ SUBMITTED: 'SUBMITTED',
+ },
+
+ NETSUITE_EXPORT_DESTINATION: {
+ EXPENSE_REPORT: 'EXPENSE_REPORT',
+ VENDOR_BILL: 'VENDOR_BILL',
+ JOURNAL_ENTRY: 'JOURNAL_ENTRY',
+ },
+
+ NETSUITE_MAP_EXPORT_DESTINATION: {
+ EXPENSE_REPORT: 'expenseReport',
+ VENDOR_BILL: 'vendorBill',
+ JOURNAL_ENTRY: 'journalEntry',
+ },
+
+ NETSUITE_INVOICE_ITEM_PREFERENCE: {
+ CREATE: 'create',
+ SELECT: 'select',
+ },
+
+ NETSUITE_JOURNAL_POSTING_PREFERENCE: {
+ JOURNALS_POSTING_INDIVIDUAL_LINE: 'JOURNALS_POSTING_INDIVIDUAL_LINE',
+ JOURNALS_POSTING_TOTAL_LINE: 'JOURNALS_POSTING_TOTAL_LINE',
+ },
+
+ NETSUITE_EXPENSE_TYPE: {
+ REIMBURSABLE: 'reimbursable',
+ NON_REIMBURSABLE: 'nonreimbursable',
+ },
+
+ NETSUITE_REPORTS_APPROVAL_LEVEL: {
+ REPORTS_APPROVED_NONE: 'REPORTS_APPROVED_NONE',
+ REPORTS_SUPERVISOR_APPROVED: 'REPORTS_SUPERVISOR_APPROVED',
+ REPORTS_ACCOUNTING_APPROVED: 'REPORTS_ACCOUNTING_APPROVED',
+ REPORTS_APPROVED_BOTH: 'REPORTS_APPROVED_BOTH',
+ },
+
+ NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL: {
+ VENDOR_BILLS_APPROVED_NONE: 'VENDOR_BILLS_APPROVED_NONE',
+ VENDOR_BILLS_APPROVAL_PENDING: 'VENDOR_BILLS_APPROVAL_PENDING',
+ VENDOR_BILLS_APPROVED: 'VENDOR_BILLS_APPROVED',
+ },
+
+ NETSUITE_JOURNALS_APPROVAL_LEVEL: {
+ JOURNALS_APPROVED_NONE: 'JOURNALS_APPROVED_NONE',
+ JOURNALS_APPROVAL_PENDING: 'JOURNALS_APPROVAL_PENDING',
+ JOURNALS_APPROVED: 'JOURNALS_APPROVED',
+ },
+
+ NETSUITE_ACCOUNT_TYPE: {
+ ACCOUNTS_PAYABLE: '_accountsPayable',
+ ACCOUNTS_RECEIVABLE: '_accountsReceivable',
+ OTHER_CURRENT_LIABILITY: '_otherCurrentLiability',
+ CREDIT_CARD: '_creditCard',
+ BANK: '_bank',
+ OTHER_CURRENT_ASSET: '_otherCurrentAsset',
+ LONG_TERM_LIABILITY: '_longTermLiability',
+ EXPENSE: '_expense',
+ },
+
+ NETSUITE_APPROVAL_ACCOUNT_DEFAULT: 'APPROVAL_ACCOUNT_DEFAULT',
+
+ /**
+ * Countries where tax setting is permitted (Strings are in the format of Netsuite's Country type/enum)
+ *
+ * Should mirror the list on the OldDot.
+ */
+ NETSUITE_TAX_COUNTRIES: [
+ '_canada',
+ '_unitedKingdomGB',
+ '_unitedKingdom',
+ '_australia',
+ '_southAfrica',
+ '_india',
+ '_france',
+ '_netherlands',
+ '_germany',
+ '_singapore',
+ '_spain',
+ '_ireland',
+ '_denmark',
+ '_brazil',
+ '_japan',
+ '_philippines',
+ '_china',
+ '_argentina',
+ '_newZealand',
+ '_switzerland',
+ '_sweden',
+ '_portugal',
+ '_mexico',
+ '_israel',
+ '_thailand',
+ '_czechRepublic',
+ '_egypt',
+ '_ghana',
+ '_indonesia',
+ '_iranIslamicRepublicOf',
+ '_jordan',
+ '_kenya',
+ '_kuwait',
+ '_lebanon',
+ '_malaysia',
+ '_morocco',
+ '_myanmar',
+ '_nigeria',
+ '_pakistan',
+ '_saudiArabia',
+ '_sriLanka',
+ '_unitedArabEmirates',
+ '_vietnam',
+ '_austria',
+ '_bulgaria',
+ '_greece',
+ '_cyprus',
+ '_norway',
+ '_romania',
+ '_poland',
+ '_hongKong',
+ '_luxembourg',
+ '_lithuania',
+ '_malta',
+ '_finland',
+ '_koreaRepublicOf',
+ '_italy',
+ '_georgia',
+ '_hungary',
+ '_latvia',
+ '_estonia',
+ '_slovenia',
+ '_serbia',
+ '_croatiaHrvatska',
+ '_belgium',
+ '_turkey',
+ '_taiwan',
+ '_azerbaijan',
+ '_slovakRepublic',
+ '_costaRica',
+ ] as string[],
+
QUICKBOOKS_EXPORT_DATE: {
LAST_EXPENSE: 'LAST_EXPENSE',
REPORT_EXPORTED: 'REPORT_EXPORTED',
@@ -1721,6 +2030,11 @@ const CONST = {
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
},
+ BULK_ACTION_TYPES: {
+ DELETE: 'delete',
+ DISABLE: 'disable',
+ ENABLE: 'enable',
+ },
MORE_FEATURES: {
ARE_CATEGORIES_ENABLED: 'areCategoriesEnabled',
ARE_TAGS_ENABLED: 'areTagsEnabled',
@@ -1728,23 +2042,9 @@ const CONST = {
ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled',
ARE_REPORT_FIELDS_ENABLED: 'areReportFieldsEnabled',
ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled',
+ ARE_EXPENSIFY_CARDS_ENABLED: 'areExpensifyCardsEnabled',
ARE_TAXES_ENABLED: 'tax',
},
- CATEGORIES_BULK_ACTION_TYPES: {
- DELETE: 'delete',
- DISABLE: 'disable',
- ENABLE: 'enable',
- },
- TAGS_BULK_ACTION_TYPES: {
- DELETE: 'delete',
- DISABLE: 'disable',
- ENABLE: 'enable',
- },
- DISTANCE_RATES_BULK_ACTION_TYPES: {
- DELETE: 'delete',
- DISABLE: 'disable',
- ENABLE: 'enable',
- },
DEFAULT_CATEGORIES: [
'Advertising',
'Benefits',
@@ -1775,11 +2075,6 @@ const CONST = {
DUPLICATE_SUBSCRIPTION: 'duplicateSubscription',
FAILED_TO_CLEAR_BALANCE: 'failedToClearBalance',
},
- TAX_RATES_BULK_ACTION_TYPES: {
- DELETE: 'delete',
- DISABLE: 'disable',
- ENABLE: 'enable',
- },
COLLECTION_KEYS: {
DESCRIPTION: 'description',
REIMBURSER: 'reimburser',
@@ -1801,8 +2096,12 @@ const CONST = {
NAME_USER_FRIENDLY: {
netsuite: 'NetSuite',
quickbooksOnline: 'Quickbooks Online',
+ quickbooksDesktop: 'Quickbooks Desktop',
xero: 'Xero',
intacct: 'Sage Intacct',
+ financialForce: 'FinancialForce',
+ billCom: 'Bill.com',
+ zenefits: 'Zenefits',
},
SYNC_STAGE_NAME: {
STARTING_IMPORT_QBO: 'startingImportQBO',
@@ -1862,7 +2161,11 @@ const CONST = {
ACCESS_VARIANTS: {
PAID: 'paid',
ADMIN: 'admin',
+ CONTROL: 'control',
},
+ DEFAULT_MAX_EXPENSE_AGE: 90,
+ DEFAULT_MAX_EXPENSE_AMOUNT: 200000,
+ DEFAULT_MAX_AMOUNT_NO_RECEIPT: 2500,
},
CUSTOM_UNITS: {
@@ -1941,6 +2244,10 @@ const CONST = {
CARD_NAME: 'CardName',
CONFIRMATION: 'Confirmation',
},
+ CARD_TYPE: {
+ PHYSICAL: 'physical',
+ VIRTUAL: 'virtual',
+ },
},
AVATAR_ROW_SIZE: {
DEFAULT: 4,
@@ -2042,6 +2349,8 @@ const CONST = {
POLICY_ID_FROM_PATH: /\/w\/([a-zA-Z0-9]+)(\/|$)/,
SHORT_MENTION: new RegExp(`@[\\w\\-\\+\\'#@]+(?:\\.[\\w\\-\\'\\+]+)*(?![^\`]*\`)`, 'gim'),
+
+ REPORT_ID_FROM_PATH: /\/r\/(\d+)/,
},
PRONOUNS: {
@@ -2058,6 +2367,7 @@ const CONST = {
WORKSPACE_INVOICES: 'WorkspaceSendInvoices',
WORKSPACE_TRAVEL: 'WorkspaceBookTravel',
WORKSPACE_MEMBERS: 'WorkspaceManageMembers',
+ WORKSPACE_EXPENSIFY_CARD: 'WorkspaceExpensifyCard',
WORKSPACE_WORKFLOWS: 'WorkspaceWorkflows',
WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount',
WORKSPACE_SETTINGS: 'WorkspaceSettings',
@@ -2100,7 +2410,7 @@ const CONST = {
this.ACCOUNT_ID.REWARDS,
this.ACCOUNT_ID.STUDENT_AMBASSADOR,
this.ACCOUNT_ID.SVFG,
- ];
+ ].filter((id) => id !== -1);
},
// Emails that profile view is prohibited
@@ -2126,6 +2436,7 @@ const CONST = {
LOGIN_CHARACTER_LIMIT: 254,
CATEGORY_NAME_LIMIT: 256,
TAG_NAME_LIMIT: 256,
+ WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH: 256,
REPORT_NAME_LIMIT: 100,
TITLE_CHARACTER_LIMIT: 100,
DESCRIPTION_LIMIT: 500,
@@ -2158,8 +2469,11 @@ const CONST = {
SETTINGS: 'settings',
LEAVE_ROOM: 'leaveRoom',
PRIVATE_NOTES: 'privateNotes',
+ EXPORT: 'export',
DELETE: 'delete',
MARK_AS_INCOMPLETE: 'markAsIncomplete',
+ CANCEL_PAYMENT: 'cancelPayment',
+ UNAPPROVE: 'unapprove',
},
EDIT_REQUEST_FIELD: {
AMOUNT: 'amount',
@@ -3604,6 +3918,7 @@ const CONST = {
},
EVENTS: {
SCROLLING: 'scrolling',
+ ON_RETURN_TO_OLD_DOT: 'onReturnToOldDot',
},
CHAT_HEADER_LOADER_HEIGHT: 36,
@@ -3714,6 +4029,15 @@ const CONST = {
WARNING: 'warning',
},
+ /**
+ * Constants with different types for the modifiedAmount violation
+ */
+ MODIFIED_AMOUNT_VIOLATION_DATA: {
+ DISTANCE: 'distance',
+ CARD: 'card',
+ SMARTSCAN: 'smartscan',
+ },
+
/**
* Constants for types of violation names.
* Defined here because they need to be referenced by the type system to generate the
@@ -3755,6 +4079,7 @@ const CONST = {
TAX_REQUIRED: 'taxRequired',
HOLD: 'hold',
},
+ REVIEW_DUPLICATES_ORDER: ['merchant', 'category', 'tag', 'description', 'taxCode', 'billable', 'reimbursable'],
/** Context menu types */
CONTEXT_MENU_TYPES: {
@@ -3948,13 +4273,13 @@ const CONST = {
type: 'setupCategories',
autoCompleted: false,
title: 'Set up categories',
- description:
+ description: ({workspaceLink}: {workspaceLink: string}) =>
'*Set up categories* so your team can code expenses for easy reporting.\n' +
'\n' +
'Here’s how to set up categories:\n' +
'\n' +
'1. Click your profile picture.\n' +
- '2. Go to *Workspaces* > [your workspace].\n' +
+ `2. Go to [*Workspaces* > [your workspace]](${workspaceLink}).\n` +
'3. Click *Categories*.\n' +
'4. Enable and disable default categories.\n' +
'5. Click *Add categories* to make your own.\n' +
@@ -3965,13 +4290,13 @@ const CONST = {
type: 'addExpenseApprovals',
autoCompleted: false,
title: 'Add expense approvals',
- description:
+ description: ({workspaceLink}: {workspaceLink: string}) =>
'*Add expense approvals* to review your team’s spend and keep it under control.\n' +
'\n' +
'Here’s how to add expense approvals:\n' +
'\n' +
'1. Click your profile picture.\n' +
- '2. Go to *Workspaces* > [your workspace].\n' +
+ `2. Go to [*Workspaces* > [your workspace]](${workspaceLink}).\n` +
'3. Click *More features*.\n' +
'4. Enable *Workflows*.\n' +
'5. In *Workflows*, enable *Add approvals*.\n' +
@@ -3982,13 +4307,13 @@ const CONST = {
type: 'inviteTeam',
autoCompleted: false,
title: 'Invite your team',
- description:
+ description: ({workspaceLink}: {workspaceLink: string}) =>
'*Invite your team* to Expensify so they can start tracking expenses today.\n' +
'\n' +
'Here’s how to invite your team:\n' +
'\n' +
'1. Click your profile picture.\n' +
- '2. Go to *Workspaces* > [your workspace].\n' +
+ `2. Go to [*Workspaces* > [your workspace]](${workspaceLink}).\n` +
'3. Click *Members* > *Invite member*.\n' +
'4. Enter emails or phone numbers. \n' +
'5. Add an invite message if you want.\n' +
@@ -4866,9 +5191,16 @@ const CONST = {
REPORT: 'report',
},
ACTION_TYPES: {
+ VIEW: 'view',
+ REVIEW: 'review',
DONE: 'done',
PAID: 'paid',
- VIEW: 'view',
+ },
+ BULK_ACTION_TYPES: {
+ EXPORT: 'export',
+ HOLD: 'hold',
+ UNHOLD: 'unhold',
+ DELETE: 'delete',
},
TRANSACTION_TYPE: {
CASH: 'cash',
@@ -4899,6 +5231,39 @@ const CONST = {
ACTION: 'action',
TAX_AMOUNT: 'taxAmount',
},
+ SYNTAX_OPERATORS: {
+ AND: 'and',
+ OR: 'or',
+ EQUAL_TO: 'eq',
+ NOT_EQUAL_TO: 'neq',
+ GREATER_THAN: 'gt',
+ GREATER_THAN_OR_EQUAL_TO: 'gte',
+ LOWER_THAN: 'lt',
+ LOWER_THAN_OR_EQUAL_TO: 'lte',
+ },
+ SYNTAX_ROOT_KEYS: {
+ TYPE: 'type',
+ STATUS: 'status',
+ SORT_BY: 'sortBy',
+ SORT_ORDER: 'sortOrder',
+ OFFSET: 'offset',
+ },
+ SYNTAX_FILTER_KEYS: {
+ DATE: 'date',
+ AMOUNT: 'amount',
+ EXPENSE_TYPE: 'expenseType',
+ CURRENCY: 'currency',
+ MERCHANT: 'merchant',
+ DESCRIPTION: 'description',
+ FROM: 'from',
+ TO: 'to',
+ CATEGORY: 'category',
+ TAG: 'tag',
+ TAX_RATE: 'taxRate',
+ CARD_ID: 'cardID',
+ REPORT_ID: 'reportID',
+ KEYWORD: 'keyword',
+ },
},
REFERRER: {
@@ -4907,18 +5272,16 @@ const CONST = {
SUBSCRIPTION_SIZE_LIMIT: 20000,
+ PAGINATION_START_ID: '-1',
+ PAGINATION_END_ID: '-2',
+
PAYMENT_CARD_CURRENCY: {
USD: 'USD',
AUD: 'AUD',
- GBP: 'GBP',
NZD: 'NZD',
},
SUBSCRIPTION_PRICE_FACTOR: 2,
- SUBSCRIPTION_POSSIBLE_COST_SAVINGS: {
- COLLECT_PLAN: 10,
- CONTROL_PLAN: 18,
- },
FEEDBACK_SURVEY_OPTIONS: {
TOO_LIMITED: {
ID: 'tooLimited',
@@ -4938,7 +5301,72 @@ const CONST = {
},
},
+ MAX_LENGTH_256: 256,
+ WORKSPACE_CARDS_LIST_LABEL_TYPE: {
+ CURRENT_BALANCE: 'currentBalance',
+ REMAINING_LIMIT: 'remainingLimit',
+ CASH_BACK: 'cashBack',
+ },
+
EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[],
+ EMPTY_STATE_MEDIA: {
+ ANIMATION: 'animation',
+ ILLUSTRATION: 'illustration',
+ VIDEO: 'video',
+ },
+ get UPGRADE_FEATURE_INTRO_MAPPING() {
+ return {
+ reportFields: {
+ id: 'reportFields' as const,
+ alias: 'report-fields',
+ name: 'Report Fields',
+ title: 'workspace.upgrade.reportFields.title' as const,
+ description: 'workspace.upgrade.reportFields.description' as const,
+ icon: 'Pencil',
+ },
+ [this.POLICY.CONNECTIONS.NAME.NETSUITE]: {
+ id: this.POLICY.CONNECTIONS.NAME.NETSUITE,
+ alias: 'netsuite',
+ name: this.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.netsuite,
+ title: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.NETSUITE}.title` as const,
+ description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.NETSUITE}.description` as const,
+ icon: 'NetSuiteSquare',
+ },
+ [this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]: {
+ id: this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT,
+ alias: 'sage-intacct',
+ name: this.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.intacct,
+ title: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}.title` as const,
+ description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}.description` as const,
+ icon: 'IntacctSquare',
+ },
+ glCodes: {
+ id: 'glCodes' as const,
+ alias: 'gl-codes',
+ name: 'GL codes',
+ title: 'workspace.upgrade.glCodes.title' as const,
+ description: 'workspace.upgrade.glCodes.description' as const,
+ icon: 'Tag',
+ },
+ glAndPayrollCodes: {
+ id: 'glAndPayrollCodes' as const,
+ alias: 'gl-and-payroll-codes',
+ name: 'GL & Payroll codes',
+ title: 'workspace.upgrade.glAndPayrollCodes.title' as const,
+ description: 'workspace.upgrade.glAndPayrollCodes.description' as const,
+ icon: 'FolderOpen',
+ },
+ };
+ },
+ REPORT_FIELD_TYPES: {
+ TEXT: 'text',
+ DATE: 'date',
+ LIST: 'dropdown',
+ },
+
+ NAVIGATION_ACTIONS: {
+ RESET: 'RESET',
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index bfe4db13d9c4..f9fd379d94ce 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -3,12 +3,13 @@ import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat
import type {NativeEventSubscription} from 'react-native';
import {AppState, Linking, NativeModules} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import Onyx, {withOnyx} from 'react-native-onyx';
+import Onyx, {useOnyx, withOnyx} from 'react-native-onyx';
import ConfirmModal from './components/ConfirmModal';
import DeeplinkWrapper from './components/DeeplinkWrapper';
import EmojiPicker from './components/EmojiPicker/EmojiPicker';
import FocusModeNotification from './components/FocusModeNotification';
import GrowlNotification from './components/GrowlNotification';
+import RequireTwoFactorAuthenticationModal from './components/RequireTwoFactorAuthenticationModal';
import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper';
import SplashScreenHider from './components/SplashScreenHider';
import UpdateAppModal from './components/UpdateAppModal';
@@ -19,6 +20,7 @@ import * as Report from './libs/actions/Report';
import * as User from './libs/actions/User';
import * as ActiveClientManager from './libs/ActiveClientManager';
import BootSplash from './libs/BootSplash';
+import FS from './libs/Fullstory';
import * as Growl from './libs/Growl';
import Log from './libs/Log';
import migrateOnyx from './libs/migrateOnyx';
@@ -36,6 +38,7 @@ import ONYXKEYS from './ONYXKEYS';
import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu';
import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu';
import type {Route} from './ROUTES';
+import ROUTES from './ROUTES';
import type {ScreenShareRequest, Session} from './types/onyx';
Onyx.registerLogger(({level, message}) => {
@@ -100,6 +103,16 @@ function Expensify({
const [isSplashHidden, setIsSplashHidden] = useState(false);
const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false);
const {translate} = useLocalize();
+ const [account] = useOnyx(ONYXKEYS.ACCOUNT);
+ const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false);
+
+ useEffect(() => {
+ if (!account?.needsTwoFactorAuthSetup || account.requiresTwoFactorAuth) {
+ return;
+ }
+ setShouldShowRequire2FAModal(true);
+ }, [account?.needsTwoFactorAuthSetup, account?.requiresTwoFactorAuth]);
+
const [initialUrl, setInitialUrl] = useState(null);
useEffect(() => {
@@ -147,6 +160,9 @@ function Expensify({
// Initialize this client as being an active client
ActiveClientManager.init();
+ // Initialize Fullstory lib
+ FS.init();
+
// Used for the offline indicator appearing when someone is offline
const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo();
@@ -208,7 +224,7 @@ function Expensify({
}
appStateChangeListener.current.remove();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run again
}, []);
// This is being done since we want to play sound even when iOS device is on silent mode, to align with other platforms.
@@ -249,6 +265,16 @@ function Expensify({
/>
) : null}
{focusModeNotification ? : null}
+ {shouldShowRequire2FAModal ? (
+ {
+ setShouldShowRequire2FAModal(false);
+ Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.HOME));
+ }}
+ isVisible
+ description={translate('twoFactorAuth.twoFactorAuthIsRequiredForAdminsDescription')}
+ />
+ ) : null}
>
)}
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 6f94a23acad8..091a6eceae9b 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -134,7 +134,7 @@ const ONYXKEYS = {
NVP_LAST_PAYMENT_METHOD: 'nvp_private_lastPaymentMethod',
/** This NVP holds to most recent waypoints that a person has used when creating a distance expense */
- NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints',
+ NVP_RECENT_WAYPOINTS: 'nvp_expensify_recentWaypoints',
/** This NVP contains the choice that the user made on the engagement modal */
NVP_INTRO_SELECTED: 'nvp_introSelected',
@@ -162,6 +162,15 @@ const ONYXKEYS = {
/** Store the state of the subscription */
NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription',
+ /** Store the stripe id status */
+ NVP_PRIVATE_STRIPE_CUSTOMER_ID: 'nvp_private_stripeCustomerID',
+
+ /** Store the billing dispute status */
+ NVP_PRIVATE_BILLING_DISPUTE_PENDING: 'nvp_private_billingDisputePending',
+
+ /** Store the billing status */
+ NVP_PRIVATE_BILLING_STATUS: 'nvp_private_billingStatus',
+
/** Store preferred skintone for emoji */
PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone',
@@ -187,7 +196,7 @@ const ONYXKEYS = {
NVP_BILLING_FUND_ID: 'nvp_expensify_billingFundID',
/** The amount owed by the workspace’s owner. */
- NVP_PRIVATE_AMOUNT_OWNED: 'nvp_private_amountOwed',
+ NVP_PRIVATE_AMOUNT_OWED: 'nvp_private_amountOwed',
/** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */
NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd',
@@ -311,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',
@@ -338,6 +350,9 @@ const ONYXKEYS = {
/** Indicates whether we should store logs or not */
SHOULD_STORE_LOGS: 'shouldStoreLogs',
+ /** Indicates whether we should mask fragile user data while exporting onyx state or not */
+ SHOULD_MASK_ONYX_STATE: 'shouldMaskOnyxState',
+
/** Stores new group chat draft */
NEW_GROUP_CHAT_DRAFT: 'newGroupChatDraft',
@@ -350,9 +365,23 @@ const ONYXKEYS = {
/** Holds the checks used while transferring the ownership of the workspace */
POLICY_OWNERSHIP_CHANGE_CHECKS: 'policyOwnershipChangeChecks',
+ // These statuses below are in separate keys on purpose - it allows us to have different behaviours of the banner based on the status
+
+ /** Indicates whether ClearOutstandingBalance failed */
+ SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed',
+
+ /** Indicates whether ClearOutstandingBalance was successful */
+ SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful',
+
+ /** Indicates whether ClearOutstandingBalance is pending */
+ SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING: 'subscriptionRetryBillingStatusPending',
+
/** Stores info during review duplicates flow */
REVIEW_DUPLICATES: 'reviewDuplicates',
+ /** Stores the last export method for policy */
+ LAST_EXPORT_METHOD: 'lastExportMethod',
+
/** Stores the information about the state of issuing a new card */
ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard',
@@ -385,6 +414,7 @@ const ONYXKEYS = {
REPORT_METADATA: 'reportMetadata_',
REPORT_ACTIONS: 'reportActions_',
REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_',
+ REPORT_ACTIONS_PAGES: 'reportActionsPages_',
REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_',
REPORT_DRAFT_COMMENT: 'reportDraftComment_',
REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_',
@@ -412,6 +442,21 @@ const ONYXKEYS = {
// Shared NVPs
/** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */
SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_',
+
+ /** Expensify cards settings */
+ SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS: 'sharedNVP_private_expensifyCardSettings_',
+
+ /**
+ * Stores the card list for a given fundID and feed in the format: card__
+ * So for example: card_12345_Expensify Card
+ */
+ WORKSPACE_CARDS_LIST: 'card_',
+
+ /** The bank account that Expensify Card payments will be reconciled against */
+ SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION: 'sharedNVP_expensifyCard_continuousReconciliationConnection_',
+
+ /** If continuous reconciliation is enabled */
+ SHARED_NVP_EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION: 'sharedNVP_expensifyCard_useContinuousReconciliation_',
},
/** List of Form ids */
@@ -430,6 +475,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: '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',
@@ -500,8 +547,8 @@ const ONYXKEYS = {
REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft',
GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm',
GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft',
- REPORT_FIELD_EDIT_FORM: 'reportFieldEditForm',
- REPORT_FIELD_EDIT_FORM_DRAFT: 'reportFieldEditFormDraft',
+ REPORT_FIELDS_EDIT_FORM: 'reportFieldsEditForm',
+ REPORT_FIELDS_EDIT_FORM_DRAFT: 'reportFieldsEditFormDraft',
REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount',
REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft',
PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccount',
@@ -517,6 +564,8 @@ const ONYXKEYS = {
WORKSPACE_NEW_TAX_FORM: 'workspaceNewTaxForm',
WORKSPACE_NEW_TAX_FORM_DRAFT: 'workspaceNewTaxFormDraft',
WORKSPACE_TAX_NAME_FORM: 'workspaceTaxNameForm',
+ WORKSPACE_TAX_CODE_FORM: 'workspaceTaxCodeForm',
+ WORKSPACE_TAX_CODE_FORM_DRAFT: 'workspaceTaxCodeFormDraft',
WORKSPACE_TAX_NAME_FORM_DRAFT: 'workspaceTaxNameFormDraft',
WORKSPACE_TAX_VALUE_FORM: 'workspaceTaxValueForm',
WORKSPACE_TAX_VALUE_FORM_DRAFT: 'workspaceTaxValueFormDraft',
@@ -524,10 +573,26 @@ 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',
+ EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName',
+ EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft',
+ EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit',
+ EDIT_EXPENSIFY_CARD_LIMIT_DRAFT_FORM: 'editExpensifyCardLimitDraft',
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;
@@ -540,6 +605,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.WorkspaceReportFieldForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
[ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
[ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm;
@@ -574,7 +640,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.ReportVirtualCardFraudForm;
[ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.ReportPhysicalCardForm;
[ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: FormTypes.GetPhysicalCardForm;
- [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: FormTypes.ReportFieldEditForm;
+ [ONYXKEYS.FORMS.REPORT_FIELDS_EDIT_FORM]: FormTypes.ReportFieldsEditForm;
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm;
[ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM]: FormTypes.PersonalBankAccountForm;
[ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm;
@@ -585,11 +651,20 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM]: FormTypes.PolicyDistanceRateEditForm;
[ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM]: FormTypes.PolicyDistanceRateTaxReclaimableOnEditForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm;
+ [ONYXKEYS.FORMS.WORKSPACE_TAX_CODE_FORM]: FormTypes.WorkspaceTaxCodeForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm;
[ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm;
[ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm;
[ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm;
+ [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm;
+ [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm;
[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 = {
@@ -614,6 +689,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts;
+ [ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.Pages;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions;
[ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string;
[ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean;
@@ -635,6 +711,10 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress;
[ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod;
+ [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings;
+ [ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList;
+ [ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.BankAccount;
+ [ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean;
};
type OnyxValuesMapping = {
@@ -687,6 +767,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION]: boolean;
[ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean;
[ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod;
+ [ONYXKEYS.LAST_EXPORT_METHOD]: OnyxTypes.LastExportMethod;
[ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[];
[ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected;
[ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates;
@@ -702,6 +783,9 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners;
[ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean;
[ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription;
+ [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: OnyxTypes.StripeCustomerID;
+ [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: number;
+ [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: OnyxTypes.BillingStatus;
[ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet;
[ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido;
[ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails;
@@ -737,6 +821,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;
@@ -747,16 +832,20 @@ type OnyxValuesMapping = {
[ONYXKEYS.PLAID_CURRENT_EVENT]: string;
[ONYXKEYS.LOGS]: OnyxTypes.CapturedLogs;
[ONYXKEYS.SHOULD_STORE_LOGS]: boolean;
+ [ONYXKEYS.SHOULD_MASK_ONYX_STATE]: boolean;
[ONYXKEYS.CACHED_PDF_PATHS]: Record;
[ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record;
[ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction;
+ [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean;
+ [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean;
+ [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING]: boolean;
[ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings;
[ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates;
[ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
[ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string;
[ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string;
[ONYXKEYS.NVP_BILLING_FUND_ID]: number;
- [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: number;
+ [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number;
[ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number;
};
@@ -768,6 +857,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping;
type OnyxValueKey = keyof OnyxValuesMapping;
type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey;
+type OnyxPagesKey = typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES;
type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`;
/** If this type errors, it means that the `OnyxKey` type is missing some keys. */
@@ -775,4 +865,4 @@ type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing:
type AssertOnyxKeys = AssertTypesEqual;
export default ONYXKEYS;
-export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxValueKey, OnyxValues};
+export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxPagesKey, OnyxValueKey, OnyxValues};
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 33eb78dc300d..4f9f2b7b8df6 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1,8 +1,9 @@
-import type {ValueOf} from 'type-fest';
+import type {TupleToUnion, ValueOf} from 'type-fest';
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 {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy';
import type {SearchQuery} from './types/onyx/SearchResults';
import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
@@ -35,7 +36,7 @@ const ROUTES = {
ALL_SETTINGS: 'all-settings',
- SEARCH: {
+ SEARCH_CENTRAL_PANE: {
route: '/search/:query',
getRoute: (searchQuery: SearchQuery, queryParams?: AuthScreensParamList['Search_Central_Pane']) => {
const {sortBy, sortOrder} = queryParams ?? {};
@@ -48,11 +49,22 @@ const ROUTES = {
},
},
+ SEARCH_ADVANCED_FILTERS: 'search/filters',
+
+ SEARCH_ADVANCED_FILTERS_DATE: 'search/filters/date',
+
+ SEARCH_ADVANCED_FILTERS_TYPE: 'search/filters/type',
+
SEARCH_REPORT: {
- route: '/search/:query/view/:reportID',
+ route: 'search/:query/view/:reportID',
getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const,
},
+ TRANSACTION_HOLD_REASON_RHP: {
+ route: 'search/:query/hold',
+ getRoute: (query: string) => `search/${query}/hold` 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: {
@@ -192,7 +204,8 @@ const ROUTES = {
},
SETTINGS_2FA: {
route: 'settings/security/two-factor-auth',
- getRoute: (backTo?: string) => getUrlWithBackToParam('settings/security/two-factor-auth', backTo),
+ getRoute: (backTo?: string, forwardTo?: string) =>
+ getUrlWithBackToParam(forwardTo ? `settings/security/two-factor-auth?forwardTo=${encodeURIComponent(forwardTo)}` : 'settings/security/two-factor-auth', backTo),
},
SETTINGS_STATUS: 'settings/profile/status',
@@ -241,7 +254,12 @@ const ROUTES = {
},
REPORT_AVATAR: {
route: 'r/:reportID/avatar',
- getRoute: (reportID: string) => `r/${reportID}/avatar` as const,
+ getRoute: (reportID: string, isNewGroupChat?: boolean) => {
+ if (isNewGroupChat) {
+ return `r/${reportID}/avatar?isNewGroupChat=${isNewGroupChat}` as const;
+ }
+ return `r/${reportID}/avatar` as const;
+ },
},
EDIT_CURRENCY_REQUEST: {
route: 'r/:threadReportID/edit/currency',
@@ -280,6 +298,10 @@ const ROUTES = {
route: 'r/:reportID/details',
getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details`, backTo),
},
+ REPORT_WITH_ID_DETAILS_EXPORT: {
+ route: 'r/:reportID/details/export/:connectionName',
+ getRoute: (reportID: string, connectionName: ConnectionName) => `r/${reportID}/details/export/${connectionName}` as const,
+ },
REPORT_SETTINGS: {
route: 'r/:reportID/settings',
getRoute: (reportID: string) => `r/${reportID}/settings` as const,
@@ -670,6 +692,15 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/invoice-account-selector',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/invoice-account-selector` as const,
},
+ WORKSPACE_ACCOUNTING_CARD_RECONCILIATION: {
+ route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation',
+ getRoute: (policyID: string, connection: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation` as const,
+ },
+ WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: {
+ route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation/account',
+ getRoute: (policyID: string, connection: ValueOf) =>
+ `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation/account` as const,
+ },
WORKSPACE_CATEGORIES: {
route: 'settings/workspaces/:policyID/categories',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const,
@@ -678,6 +709,11 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/categories/:categoryName',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}` as const,
},
+ WORKSPACE_UPGRADE: {
+ route: 'settings/workspaces/:policyID/upgrade/:featureName',
+ getRoute: (policyID: string, featureName: string, backTo?: string) =>
+ getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo),
+ },
WORKSPACE_CATEGORIES_SETTINGS: {
route: 'settings/workspaces/:policyID/categories/settings',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const,
@@ -690,6 +726,14 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/categories/:categoryName/edit',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit` as const,
},
+ WORKSPACE_CATEGORY_PAYROLL_CODE: {
+ route: 'settings/workspaces/:policyID/categories/:categoryName/payroll-code',
+ getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/payroll-code` as const,
+ },
+ WORKSPACE_CATEGORY_GL_CODE: {
+ route: 'settings/workspaces/:policyID/categories/:categoryName/gl-code',
+ getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/gl-code` as const,
+ },
WORKSPACE_MORE_FEATURES: {
route: 'settings/workspaces/:policyID/more-features',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const,
@@ -722,6 +766,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/tag-list/:orderWeight',
getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tag-list/${orderWeight}` as const,
},
+ WORKSPACE_TAG_GL_CODE: {
+ route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/gl-code',
+ getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/gl-code` as const,
+ },
WORKSPACE_TAXES: {
route: 'settings/workspaces/:policyID/taxes',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` as const,
@@ -779,21 +827,67 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/tax/:taxID/value',
getRoute: (policyID: string, taxID: string) => `settings/workspaces/${policyID}/tax/${encodeURIComponent(taxID)}/value` as const,
},
+ WORKSPACE_TAX_CODE: {
+ route: 'settings/workspaces/:policyID/tax/:taxID/tax-code',
+ getRoute: (policyID: string, taxID: string) => `settings/workspaces/${policyID}/tax/${encodeURIComponent(taxID)}/tax-code` as const,
+ },
WORKSPACE_REPORT_FIELDS: {
route: 'settings/workspaces/:policyID/reportFields',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields` as const,
},
- // TODO: uncomment after development is done
- // WORKSPACE_EXPENSIFY_CARD: {
- // route: 'settings/workspaces/:policyID/expensify-card',
- // getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const,
- // },
- // 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_CREATE_REPORT_FIELD: {
+ route: 'settings/workspaces/:policyID/reportFields/new',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new` as const,
+ },
+ WORKSPACE_REPORT_FIELDS_SETTINGS: {
+ route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit',
+ getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${encodeURIComponent(reportFieldID)}/edit` as const,
+ },
+ WORKSPACE_REPORT_FIELDS_LIST_VALUES: {
+ route: 'settings/workspaces/:policyID/reportFields/listValues/:reportFieldID?',
+ getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const,
+ },
+ WORKSPACE_REPORT_FIELDS_ADD_VALUE: {
+ route: 'settings/workspaces/:policyID/reportFields/addValue/:reportFieldID?',
+ getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const,
+ },
+ WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: {
+ route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?',
+ getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) =>
+ `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const,
+ },
+ WORKSPACE_REPORT_FIELDS_EDIT_VALUE: {
+ route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit',
+ getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}/edit` as const,
+ },
+ WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE: {
+ route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit/initialValue',
+ getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${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,
+ },
+ WORKSPACE_EXPENSIFY_CARD_DETAILS: {
+ route: 'settings/workspaces/:policyID/expensify-card/:cardID',
+ getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo),
+ },
+ WORKSPACE_EXPENSIFY_CARD_NAME: {
+ route: 'settings/workspaces/:policyID/expensify-card/:cardID/edit/name',
+ getRoute: (policyID: string, cardID: string) => `settings/workspaces/${policyID}/expensify-card/${cardID}/edit/name` as const,
+ },
+ WORKSPACE_EXPENSIFY_CARD_LIMIT: {
+ route: 'settings/workspaces/:policyID/expensify-card/:cardID/edit/limit',
+ getRoute: (policyID: string, cardID: string) => `settings/workspaces/${policyID}/expensify-card/${cardID}/edit/limit` as const,
+ },
+ 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_EXPENSIFY_CARD_BANK_ACCOUNT: {
+ route: 'settings/workspaces/:policyID/expensify-card/choose-bank-account',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/choose-bank-account` as const,
+ },
WORKSPACE_DISTANCE_RATES: {
route: 'settings/workspaces/:policyID/distance-rates',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const,
@@ -846,7 +940,38 @@ const ROUTES = {
route: 'r/:threadReportID/duplicates/review',
getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review` as const,
},
-
+ TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: {
+ route: 'r/:threadReportID/duplicates/review/merchant',
+ getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/merchant` as const,
+ },
+ TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: {
+ route: 'r/:threadReportID/duplicates/review/category',
+ getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/category` as const,
+ },
+ TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: {
+ route: 'r/:threadReportID/duplicates/review/tag',
+ getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tag` as const,
+ },
+ TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE: {
+ route: 'r/:threadReportID/duplicates/review/tax-code',
+ getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tax-code` as const,
+ },
+ TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: {
+ route: 'r/:threadReportID/duplicates/review/description',
+ getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/description` as const,
+ },
+ TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE: {
+ route: 'r/:threadReportID/duplicates/review/reimbursable',
+ getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/reimbursable` as const,
+ },
+ TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE: {
+ route: 'r/:threadReportID/duplicates/review/billable',
+ getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/billable` as const,
+ },
+ TRANSACTION_DUPLICATE_CONFIRMATION_PAGE: {
+ route: 'r/:threadReportID/duplicates/confirm',
+ getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const,
+ },
POLICY_ACCOUNTING_XERO_IMPORT: {
route: 'settings/workspaces/:policyID/accounting/xero/import',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import` as const,
@@ -932,14 +1057,152 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/taxes',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/taxes` as const,
},
- POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: {
- route: 'settings/workspaces/:policyID/accounting/net-suite/subsidiary-selector',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/net-suite/subsidiary-selector` as const,
- },
RESTRICTED_ACTION: {
route: 'restricted-action/workspace/:policyID',
getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const,
},
+ POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/subsidiary-selector',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/subsidiary-selector` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_EXISTING_CONNECTIONS: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/existing-connections',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/existing-connections` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/token-input',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/token-input` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_IMPORT: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/import',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_IMPORT_MAPPING: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/import/mapping/:importField',
+ 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,
+ },
+ POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT: {
+ route: 'settings/workspaces/:policyID/accounting/netsuite/import/customer-projects/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/customer-projects/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_EXPORT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_PREFERRED_EXPORTER_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/preferred-exporter/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/preferred-exporter/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_DATE_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/date/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/date/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType',
+ getRoute: (policyID: string, expenseType: ValueOf) =>
+ `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/destination/select',
+ getRoute: (policyID: string, expenseType: ValueOf) =>
+ `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/destination/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/vendor/select',
+ getRoute: (policyID: string, expenseType: ValueOf) =>
+ `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/vendor/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/payable-account/select',
+ getRoute: (policyID: string, expenseType: ValueOf) =>
+ `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/payable-account/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/journal-posting-preference/select',
+ getRoute: (policyID: string, expenseType: ValueOf) =>
+ `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/journal-posting-preference/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_RECEIVABLE_ACCOUNT_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/receivable-account/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/receivable-account/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/invoice-item-preference/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/invoice-item-preference/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/invoice-item-preference/invoice-item/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/invoice-item-preference/invoice-item/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_TAX_POSTING_ACCOUNT_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/tax-posting-account/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/tax-posting-account/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/export/provincial-tax-posting-account/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/provincial-tax-posting-account/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_ADVANCED: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/advanced/',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/advanced/reimbursement-account/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/reimbursement-account/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_COLLECTION_ACCOUNT_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/advanced/collection-account/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/collection-account/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/advanced/expense-report-approval-level/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/expense-report-approval-level/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/advanced/vendor-bill-approval-level/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/vendor-bill-approval-level/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/advanced/journal-entry-approval-level/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/journal-entry-approval-level/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_APPROVAL_ACCOUNT_SELECT: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/advanced/approval-account/select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/approval-account/select` as const,
+ },
+ POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID: {
+ route: 'settings/workspaces/:policyID/connections/netsuite/advanced/custom-form-id/:expenseType',
+ getRoute: (policyID: string, expenseType: ValueOf) =>
+ `settings/workspaces/${policyID}/connections/netsuite/advanced/custom-form-id/${expenseType}` as const,
+ },
POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES: {
route: 'settings/workspaces/:policyID/accounting/sage-intacct/prerequisites',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/prerequisites` as const,
@@ -952,6 +1215,70 @@ 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_ENTITY: {
+ route: 'settings/workspaces/:policyID/accounting/sage-intacct/entity',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/entity` 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 1807c9bb0bab..eddf11d95e4f 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -30,6 +30,10 @@ const SCREENS = {
SEARCH: {
CENTRAL_PANE: 'Search_Central_Pane',
REPORT_RHP: 'Search_Report_RHP',
+ ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP',
+ ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP',
+ ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP',
+ TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
BOTTOM_TAB: 'Search_Bottom_Tab',
},
SETTINGS: {
@@ -144,8 +148,10 @@ const SCREENS = {
TRANSACTION_DUPLICATE: 'TransactionDuplicate',
TRAVEL: 'Travel',
SEARCH_REPORT: 'SearchReport',
+ SEARCH_ADVANCED_FILTERS: 'SearchAdvancedFilters',
SETTINGS_CATEGORIES: 'SettingsCategories',
RESTRICTED_ACTION: 'RestrictedAction',
+ REPORT_EXPORT: 'Report_Export',
},
ONBOARDING_MODAL: {
ONBOARDING: 'Onboarding',
@@ -186,6 +192,14 @@ const SCREENS = {
TRANSACTION_DUPLICATE: {
REVIEW: 'Transaction_Duplicate_Review',
+ MERCHANT: 'Transaction_Duplicate_Merchant',
+ CATEGORY: 'Transaction_Duplicate_Category',
+ TAG: 'Transaction_Duplicate_Tag',
+ DESCRIPTION: 'Transaction_Duplicate_Description',
+ TAX_CODE: 'Transaction_Duplicate_Tax_Code',
+ REIMBURSABLE: 'Transaction_Duplicate_Reimburable',
+ BILLABLE: 'Transaction_Duplicate_Billable',
+ CONFIRMATION: 'Transaction_Duplicate_Confirmation',
},
IOU_SEND: {
@@ -232,6 +246,7 @@ const SCREENS = {
REPORT_DETAILS: {
ROOT: 'Report_Details_Root',
SHARE_CODE: 'Report_Details_Share_Code',
+ EXPORT: 'Report_Details_Export',
},
WORKSPACE: {
@@ -272,10 +287,60 @@ const SCREENS = {
XERO_EXPORT_PREFERRED_EXPORTER_SELECT: 'Workspace_Accounting_Xero_Export_Preferred_Exporter_Select',
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_SUBSIDIARY_SELECTOR: 'Policy_Accounting_Net_Suite_Subsidiary_Selector',
+ 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_REUSE_EXISTING_CONNECTIONS: 'Policy_Accounting_NetSuite_Reuse_Existing_Connections',
+ NETSUITE_TOKEN_INPUT: 'Policy_Accounting_NetSuite_Token_Input',
+ NETSUITE_SUBSIDIARY_SELECTOR: 'Policy_Accounting_NetSuite_Subsidiary_Selector',
+ NETSUITE_IMPORT: 'Policy_Accounting_NetSuite_Import',
+ NETSUITE_EXPORT: 'Policy_Accounting_NetSuite_Export',
+ NETSUITE_PREFERRED_EXPORTER_SELECT: 'Policy_Accounting_NetSuite_Preferred_Exporter_Select',
+ NETSUITE_DATE_SELECT: 'Policy_Accounting_NetSuite_Date_Select',
+ NETSUITE_EXPORT_EXPENSES: 'Policy_Accounting_NetSuite_Export_Expenses',
+ NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Destination_Select',
+ NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Vendor_Select',
+ NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Payable_Account_Select',
+ NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Journal_Posting_Preference_Select',
+ NETSUITE_RECEIVABLE_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Receivable_Account_Select',
+ NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT: 'Policy_Accounting_NetSuite_Invoice_Item_Preference_Select',
+ NETSUITE_INVOICE_ITEM_SELECT: 'Policy_Accounting_NetSuite_Invoice_Item_Select',
+ NETSUITE_TAX_POSTING_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Tax_Posting_Account_Select',
+ NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Provincial_Tax_Posting_Account_Select',
+ NETSUITE_ADVANCED: 'Policy_Accounting_NetSuite_Advanced',
+ NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Reimbursement_Account_Select',
+ NETSUITE_COLLECTION_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Collection_Account_Select',
+ NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Expense_Report_Approval_Level_Select',
+ NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Vendor_Bill_Approval_Level_Select',
+ NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Journal_Entry_Approval_Level_Select',
+ NETSUITE_APPROVAL_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Approval_Account_Select',
+ NETSUITE_CUSTOM_FORM_ID: 'Policy_Accounting_NetSuite_Custom_Form_ID',
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_ENTITY: 'Policy_Sage_Intacct_Entity',
+ 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',
+ CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation',
+ RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings',
},
INITIAL: 'Workspace_Initial',
PROFILE: 'Workspace_Profile',
@@ -285,7 +350,11 @@ const SCREENS = {
RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate',
RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit',
EXPENSIFY_CARD: 'Workspace_ExpensifyCard',
+ EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details',
+ EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit',
EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New',
+ EXPENSIFY_CARD_NAME: 'Workspace_ExpensifyCard_Name',
+ EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount',
BILLS: 'Workspace_Bills',
INVOICES: 'Workspace_Invoices',
TRAVEL: 'Workspace_Travel',
@@ -299,9 +368,17 @@ const SCREENS = {
TAG_EDIT: 'Tag_Edit',
TAXES: 'Workspace_Taxes',
REPORT_FIELDS: 'Workspace_ReportFields',
+ REPORT_FIELDS_SETTINGS: 'Workspace_ReportFields_Settings',
+ REPORT_FIELDS_CREATE: 'Workspace_ReportFields_Create',
+ REPORT_FIELDS_LIST_VALUES: 'Workspace_ReportFields_ListValues',
+ 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',
+ TAX_CODE: 'Workspace_Tax_Code',
TAXES_SETTINGS: 'Workspace_Taxes_Settings',
TAXES_SETTINGS_CUSTOM_TAX_NAME: 'Workspace_Taxes_Settings_CustomTaxName',
TAXES_SETTINGS_WORKSPACE_CURRENCY_DEFAULT: 'Workspace_Taxes_Settings_WorkspaceCurrency',
@@ -310,6 +387,7 @@ const SCREENS = {
TAG_CREATE: 'Tag_Create',
TAG_SETTINGS: 'Tag_Settings',
TAG_LIST_VIEW: 'Tag_List_View',
+ TAG_GL_CODE: 'Tag_GL_Code',
CURRENCY: 'Workspace_Profile_Currency',
ADDRESS: 'Workspace_Profile_Address',
WORKFLOWS: 'Workspace_Workflows',
@@ -322,6 +400,8 @@ const SCREENS = {
NAME: 'Workspace_Profile_Name',
CATEGORY_CREATE: 'Category_Create',
CATEGORY_EDIT: 'Category_Edit',
+ CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code',
+ CATEGORY_GL_CODE: 'Category_GL_Code',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
MORE_FEATURES: 'Workspace_More_Features',
@@ -336,6 +416,7 @@ const SCREENS = {
DISTANCE_RATE_EDIT: 'Distance_Rate_Edit',
DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit',
DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit',
+ UPGRADE: 'Workspace_Upgrade',
},
EDIT_REQUEST: {
diff --git a/src/components/AccountingListSkeletonView.tsx b/src/components/AccountingListSkeletonView.tsx
index b977903d3adc..dbe8ada6c4b7 100644
--- a/src/components/AccountingListSkeletonView.tsx
+++ b/src/components/AccountingListSkeletonView.tsx
@@ -4,12 +4,14 @@ import ItemListSkeletonView from './Skeletons/ItemListSkeletonView';
type AccountingListSkeletonViewProps = {
shouldAnimate?: boolean;
+ gradientOpacityEnabled?: boolean;
};
-function AccountingListSkeletonView({shouldAnimate = true}: AccountingListSkeletonViewProps) {
+function AccountingListSkeletonView({shouldAnimate = true, gradientOpacityEnabled = false}: AccountingListSkeletonViewProps) {
return (
(
<>
;
+ initialCurrency?: ValueOf;
isSecurityCodeRequired?: boolean;
- changeBillingCurrency: (currency?: ValueOf, values?: FormOnyxValues) => void;
+ changeBillingCurrency: (currency?: ValueOf, values?: FormOnyxValues) => void;
};
const REQUIRED_FIELDS = [INPUT_IDS.SECURITY_CODE];
@@ -30,7 +30,7 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq
const {translate} = useLocalize();
const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false);
- const [currency, setCurrency] = useState>(initialCurrency ?? CONST.CURRENCY.USD);
+ const [currency, setCurrency] = useState>(initialCurrency ?? CONST.PAYMENT_CARD_CURRENCY.USD);
const validate = (values: FormOnyxValues): FormInputErrors => {
const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS);
@@ -46,7 +46,7 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq
() => ({
sections: [
{
- data: (Object.keys(CONST.CURRENCY) as Array>).map((currencyItem) => ({
+ data: (Object.keys(CONST.PAYMENT_CARD_CURRENCY) as Array>).map((currencyItem) => ({
text: currencyItem,
value: currencyItem,
keyForList: currencyItem,
@@ -62,13 +62,13 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq
setIsCurrencyModalVisible(true);
}, []);
- const changeCurrency = useCallback((selectedCurrency: ValueOf) => {
+ const changeCurrency = useCallback((selectedCurrency: ValueOf) => {
setCurrency(selectedCurrency);
setIsCurrencyModalVisible(false);
}, []);
const selectCurrency = useCallback(
- (selectedCurrency: ValueOf) => {
+ (selectedCurrency: ValueOf) => {
setCurrency(selectedCurrency);
changeBillingCurrency(selectedCurrency);
},
@@ -109,7 +109,7 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq
>
>}
+ currencies={Object.keys(CONST.PAYMENT_CARD_CURRENCY) as Array>}
currentCurrency={currency}
onCurrencyChange={changeCurrency}
onClose={() => setIsCurrencyModalVisible(false)}
diff --git a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx
index 19b3399caea8..c3c38c4aec72 100644
--- a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx
+++ b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx
@@ -15,19 +15,19 @@ type PaymentCardCurrencyModalProps = {
isVisible: boolean;
/** The list of currencies to render */
- currencies: Array>;
+ currencies: Array>;
/** Currently selected currency */
- currentCurrency: ValueOf;
+ currentCurrency: ValueOf;
/** Function to call when the user selects a currency */
- onCurrencyChange?: (currency: ValueOf) => void;
+ onCurrencyChange?: (currency: ValueOf) => void;
/** Function to call when the user closes the currency picker */
onClose?: () => void;
};
-function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.CURRENCY.USD, onCurrencyChange, onClose}: PaymentCardCurrencyModalProps) {
+function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.PAYMENT_CARD_CURRENCY.USD, onCurrencyChange, onClose}: PaymentCardCurrencyModalProps) {
const {isSmallScreenWidth} = useWindowDimensions();
const styles = useThemeStyles();
const {translate} = useLocalize();
diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx
index 6cd967e55bbf..f38ea60f1aad 100644
--- a/src/components/AddPaymentCard/PaymentCardForm.tsx
+++ b/src/components/AddPaymentCard/PaymentCardForm.tsx
@@ -32,7 +32,7 @@ type PaymentCardFormProps = {
showCurrencyField?: boolean;
showStateSelector?: boolean;
isDebitCard?: boolean;
- addPaymentCard: (values: FormOnyxValues, currency?: ValueOf) => void;
+ addPaymentCard: (values: FormOnyxValues, currency?: ValueOf) => void;
submitButtonText: string;
/** Custom content to display in the footer after card form */
footerContent?: ReactNode;
@@ -295,7 +295,7 @@ function PaymentCardForm({
diff --git a/src/components/AddPlaidBankAccount.tsx b/src/components/AddPlaidBankAccount.tsx
index a112b36705c3..4de286183ea8 100644
--- a/src/components/AddPlaidBankAccount.tsx
+++ b/src/components/AddPlaidBankAccount.tsx
@@ -153,7 +153,7 @@ function AddPlaidBankAccount({
return unsubscribeToNavigationShortcuts;
// disabling this rule, as we want this to run only on the first render
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
useEffect(() => {
diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx
index 27822fb390a6..7ca4cc3273ca 100644
--- a/src/components/AddressForm.tsx
+++ b/src/components/AddressForm.tsx
@@ -182,6 +182,7 @@ function AddressForm({
InputComponent={CountrySelector}
inputID={INPUT_IDS.COUNTRY}
value={country}
+ onValueChange={onAddressChanged}
shouldSaveDraft={shouldSaveDraft}
/>
diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx
index 9bd6142b5604..2679a550f72f 100644
--- a/src/components/AddressSearch/index.tsx
+++ b/src/components/AddressSearch/index.tsx
@@ -24,7 +24,24 @@ import CONST from '@src/CONST';
import type {Address} from '@src/types/onyx/PrivatePersonalDetails';
import CurrentLocationButton from './CurrentLocationButton';
import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer';
-import type {AddressSearchProps} from './types';
+import type {AddressSearchProps, PredefinedPlace} from './types';
+
+/**
+ * Check if the place matches the search by the place name or description.
+ * @param search The search string for a place
+ * @param place The place to check for a match on the search
+ * @returns true if search is related to place, otherwise it returns false.
+ */
+function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean {
+ if (!search) {
+ return true;
+ }
+ if (!place) {
+ return false;
+ }
+ const fullSearchSentence = `${place.name ?? ''} ${place.description}`;
+ return search.split(' ').every((searchTerm) => !searchTerm || fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()));
+}
// The error that's being thrown below will be ignored until we fork the
// react-native-google-places-autocomplete repo and replace the
@@ -42,6 +59,7 @@ function AddressSearch(
isLimitedToUSA = false,
label,
maxInputLength,
+ onFocus,
onBlur,
onInputChange,
onPress,
@@ -72,7 +90,7 @@ function AddressSearch(
const [isTyping, setIsTyping] = useState(false);
const [isFocused, setIsFocused] = useState(false);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const [searchValue, setSearchValue] = useState(value || defaultValue || '');
+ const [searchValue, setSearchValue] = useState('');
const [locationErrorCode, setLocationErrorCode] = useState(null);
const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false);
const shouldTriggerGeolocationCallbacks = useRef(true);
@@ -282,7 +300,7 @@ function AddressSearch(
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{(predefinedPlaces?.length ?? 0) > 0 && (
- <>
+
{/* This will show current location button in list if there are some recent destinations */}
{shouldShowCurrentLocationButton && (
)}
{!value && {translate('common.recentDestinations')} }
- >
+
)}
>
);
@@ -304,10 +322,16 @@ function AddressSearch(
};
}, []);
+ const filteredPredefinedPlaces = useMemo(() => {
+ if (!searchValue) {
+ return predefinedPlaces ?? [];
+ }
+ return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? [];
+ }, [predefinedPlaces, searchValue]);
+
const listEmptyComponent = useCallback(
- () =>
- !!isOffline || !isTyping ? null : {translate('common.noResultsFound')} ,
- [isOffline, isTyping, styles, translate],
+ () => (!isTyping ? null : {translate('common.noResultsFound')} ),
+ [isTyping, styles, translate],
);
const listLoader = useCallback(
@@ -348,7 +372,7 @@ function AddressSearch(
fetchDetails
suppressDefaultStyles
enablePoweredByContainer={false}
- predefinedPlaces={predefinedPlaces ?? undefined}
+ predefinedPlaces={filteredPredefinedPlaces}
listEmptyComponent={listEmptyComponent}
listLoaderComponent={listLoader}
renderHeaderComponent={renderHeaderComponent}
@@ -357,7 +381,7 @@ function AddressSearch(
const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text;
return (
- {!!title && {title} }
+ {!!title && {title} }
{subtitle}
);
@@ -391,6 +415,7 @@ function AddressSearch(
shouldSaveDraft,
onFocus: () => {
setIsFocused(true);
+ onFocus?.();
},
onBlur: (event) => {
if (!isCurrentTargetInsideContainer(event, containerRef)) {
@@ -420,10 +445,11 @@ function AddressSearch(
}}
styles={{
textInputContainer: [styles.flexColumn],
- listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.overflowAuto, styles.borderLeft, styles.borderRight, !isFocused && {height: 0}],
+ listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.borderLeft, styles.borderRight, !isFocused && styles.h0],
row: [styles.pv4, styles.ph3, styles.overflowAuto],
description: [styles.googleSearchText],
- separator: [styles.googleSearchSeparator],
+ separator: [styles.googleSearchSeparator, styles.overflowAuto],
+ container: [styles.mh100],
}}
numberOfLines={2}
isRowScrollable={false}
@@ -447,11 +473,13 @@ function AddressSearch(
)
}
placeholder=""
- />
- setLocationErrorCode(null)}
- locationErrorCode={locationErrorCode}
- />
+ listViewDisplayed
+ >
+ setLocationErrorCode(null)}
+ locationErrorCode={locationErrorCode}
+ />
+
{isFetchingCurrentLocation && }
diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts
index 82e4c3c3fc37..b654fcad99da 100644
--- a/src/components/AddressSearch/types.ts
+++ b/src/components/AddressSearch/types.ts
@@ -23,6 +23,10 @@ type StreetValue = {
street: string;
};
+type PredefinedPlace = Place & {
+ name?: string;
+};
+
type AddressSearchProps = {
/** The ID used to uniquely identify the input in a Form */
inputID?: string;
@@ -30,6 +34,9 @@ type AddressSearchProps = {
/** Saves a draft of the input value when used in a form */
shouldSaveDraft?: boolean;
+ /** Callback that is called when the text input is focused */
+ onFocus?: () => void;
+
/** Callback that is called when the text input is blurred */
onBlur?: () => void;
@@ -64,7 +71,7 @@ type AddressSearchProps = {
canUseCurrentLocation?: boolean;
/** A list of predefined places that can be shown when the user isn't searching for something */
- predefinedPlaces?: Place[] | null;
+ predefinedPlaces?: PredefinedPlace[] | null;
/** A map of inputID key names */
renamedInputKeys?: Address;
@@ -84,4 +91,4 @@ type AddressSearchProps = {
type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean;
-export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue};
+export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue, PredefinedPlace};
diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx
index 3319a28c58b9..1eb272dce49a 100644
--- a/src/components/AmountForm.tsx
+++ b/src/components/AmountForm.tsx
@@ -135,7 +135,7 @@ function AmountForm(
setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount));
// we want to update only when decimals change (setNewAmount also changes when decimals change).
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [decimals]);
/**
diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
index 48604ec364c7..b9aeceeb3621 100644
--- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
+++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
@@ -41,7 +41,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow
return (
- {({anchor, report, action, checkIfContextMenuActive}) => (
+ {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => (
{
@@ -53,7 +53,9 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow
}}
onPressIn={onPressIn}
onPressOut={onPressOut}
- onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ onLongPress={(event) =>
+ showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs))
+ }
shouldUseHapticsOnLongPress
accessibilityLabel={displayName}
role={CONST.ROLE.BUTTON}
diff --git a/src/components/AnimatedStep/index.tsx b/src/components/AnimatedStep/index.tsx
index 2fb3e3167ff8..6de7d0c2b013 100644
--- a/src/components/AnimatedStep/index.tsx
+++ b/src/components/AnimatedStep/index.tsx
@@ -37,6 +37,7 @@ function AnimatedStep({onAnimationEnd, direction = CONST.ANIMATION_DIRECTION.IN,
}}
duration={CONST.ANIMATED_TRANSITION}
animation={animationStyle}
+ // eslint-disable-next-line react-compiler/react-compiler
useNativeDriver={useNativeDriver}
style={style}
>
diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index df027ed6edb4..b6ea09f32436 100644
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -265,7 +265,7 @@ function AttachmentModal({
}
setIsModalOpen(false);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isModalOpen, isConfirmButtonDisabled, onConfirm, file, sourceState]);
/**
@@ -320,7 +320,7 @@ function AttachmentModal({
}
let fileObject = data;
if ('getAsFile' in data && typeof data.getAsFile === 'function') {
- fileObject = data.getAsFile();
+ fileObject = data.getAsFile() as FileObject;
}
if (!fileObject) {
return;
@@ -367,7 +367,7 @@ function AttachmentModal({
onModalClose();
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [onModalClose]);
/**
@@ -428,7 +428,7 @@ function AttachmentModal({
});
}
return menuItems;
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isReceiptAttachment, transaction, file, sourceState, iouType]);
// There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment.
@@ -529,6 +529,7 @@ function AttachmentModal({
fallbackSource={fallbackSource}
isUsedInAttachmentModal
transactionID={transaction?.transactionID}
+ isUploaded={!isEmptyObject(report)}
/>
)
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index 154fcf838c86..7d4fbd97f4f7 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -222,6 +222,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s
* @param onCanceledHandler A callback that will be called without a selected attachment
*/
const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => {
+ // eslint-disable-next-line react-compiler/react-compiler
completeAttachmentSelection.current = onPickedHandler;
onCanceled.current = onCanceledHandler;
setIsVisible(true);
diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx
index a6ff9cb8d27a..669b26724a02 100644
--- a/src/components/AttachmentPicker/index.tsx
+++ b/src/components/AttachmentPicker/index.tsx
@@ -46,6 +46,7 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE}:
// Cleanup after selecting a file to start from a fresh state
if (fileInput.current) {
+ // eslint-disable-next-line react-compiler/react-compiler
fileInput.current.value = '';
}
}}
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
index 2ec1883fd7de..a9b5dfb7feb6 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
+++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
@@ -74,6 +74,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr
;
+
+ /** Ref to the active attachment */
+ pagerRef?: ForwardedRef;
+
+ /** Indicates if the pager is currently scrolling */
isPagerScrolling: SharedValue;
+
+ /** Indicates if scrolling is enabled for the attachment */
isScrollEnabled: SharedValue;
+
+ /** Function to call after a tap event */
onTap: () => void;
+
+ /** Function to call when the scale changes */
onScaleChanged: (scale: number) => void;
+
+ /** Function to call after a swipe down event */
onSwipeDown: () => void;
};
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
index b7ef9309eb10..0b6b67b38110 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
@@ -1,4 +1,4 @@
-import type {ForwardedRef} from 'react';
+import type {ForwardedRef, SetStateAction} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent} from 'react-native';
import {View} from 'react-native';
@@ -8,6 +8,7 @@ import type {PagerViewProps} from 'react-native-pager-view';
import PagerView from 'react-native-pager-view';
import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
import CarouselItem from '@components/Attachments/AttachmentCarousel/CarouselItem';
+import useCarouselContextEvents from '@components/Attachments/AttachmentCarousel/useCarouselContextEvents';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import useThemeStyles from '@hooks/useThemeStyles';
import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
@@ -41,24 +42,21 @@ type AttachmentCarouselPagerProps = {
>,
) => void;
- /**
- * A callback that can be used to toggle the attachment carousel arrows, when the scale of the image changes.
- * @param showArrows If set, it will show/hide the arrows. If not set, it will toggle the arrows.
- */
- onRequestToggleArrows: (showArrows?: boolean) => void;
-
/** A callback that is called when swipe-down-to-close gesture happens */
onClose: () => void;
+
+ /** Sets the visibility of the arrows. */
+ setShouldShowArrows: (show?: SetStateAction) => void;
};
function AttachmentCarouselPager(
- {items, activeSource, initialPage, onPageSelected, onRequestToggleArrows, onClose}: AttachmentCarouselPagerProps,
+ {items, activeSource, initialPage, setShouldShowArrows, onPageSelected, onClose}: AttachmentCarouselPagerProps,
ref: ForwardedRef,
) {
+ const {handleTap, handleScaleChange} = useCarouselContextEvents(setShouldShowArrows);
const styles = useThemeStyles();
const pagerRef = useRef(null);
- const scale = useRef(1);
const isPagerScrolling = useSharedValue(false);
const isScrollEnabled = useSharedValue(true);
@@ -68,6 +66,7 @@ function AttachmentCarouselPager(
const pageScrollHandler = usePageScrollHandler((e) => {
'worklet';
+ // eslint-disable-next-line react-compiler/react-compiler
activePage.value = e.position;
isPagerScrolling.value = e.offset !== 0;
}, []);
@@ -78,44 +77,11 @@ function AttachmentCarouselPager(
}, [activePage, initialPage]);
/** The `pagerItems` object that passed down to the context. Later used to detect current page, whether it's a single image gallery etc. */
- const pagerItems = useMemo(() => items.map((item, index) => ({source: item.source, index, isActive: index === activePageIndex})), [activePageIndex, items]);
-
- /**
- * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
- * It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager,
- * as well as enabling/disabling the carousel buttons.
- */
- const handleScaleChange = useCallback(
- (newScale: number) => {
- if (newScale === scale.current) {
- return;
- }
-
- scale.current = newScale;
-
- const newIsScrollEnabled = newScale === 1;
- if (isScrollEnabled.value === newIsScrollEnabled) {
- return;
- }
-
- isScrollEnabled.value = newIsScrollEnabled;
- onRequestToggleArrows(newIsScrollEnabled);
- },
- [isScrollEnabled, onRequestToggleArrows],
+ const pagerItems = useMemo(
+ () => items.map((item, index) => ({source: item.source, previewSource: item.previewSource, index, isActive: index === activePageIndex})),
+ [activePageIndex, items],
);
- /**
- * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
- * It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox.
- */
- const handleTap = useCallback(() => {
- if (!isScrollEnabled.value) {
- return;
- }
-
- onRequestToggleArrows();
- }, [isScrollEnabled.value, onRequestToggleArrows]);
-
const extractItemKey = useCallback(
(item: Attachment, index: number) =>
typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}` : `reportActionID-${item.reportActionID}` ?? `index-${index}`,
diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts
index 1e9c67cf84ac..40438d47ecc7 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts
@@ -52,6 +52,7 @@ function extractAttachments(
if (name === 'img' && attribs.src) {
const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src);
+ const previewSource = tryResolveUrlFromApiRoot(attribs.src);
if (uniqueSources.has(source)) {
return;
}
@@ -59,6 +60,9 @@ function extractAttachments(
uniqueSources.add(source);
let fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`);
+ const width = (attribs['data-expensify-width'] && parseInt(attribs['data-expensify-width'], 10)) || undefined;
+ const height = (attribs['data-expensify-height'] && parseInt(attribs['data-expensify-height'], 10)) || undefined;
+
// Public image URLs might lack a file extension in the source URL, without an extension our
// AttachmentView fails to recognize them as images and renders fallback content instead.
// We apply this small hack to add an image extension and ensure AttachmentView renders the image.
@@ -72,8 +76,9 @@ function extractAttachments(
attachments.unshift({
reportActionID: attribs['data-id'],
source,
+ previewSource,
isAuthTokenRequired: !!expensifySource,
- file: {name: fileName},
+ file: {name: fileName, width, height},
isReceipt: false,
hasBeenFlagged: attribs['data-flagged'] === 'true',
});
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx
index 15740725c42e..e0f7571af8c7 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx
@@ -57,7 +57,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
onNavigate(targetAttachments[initialPage]);
}
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [reportActions, compareImage]);
/** Updates the page state when the user navigates between attachments */
@@ -96,22 +96,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[autoHideArrows, page, updatePage],
);
- /**
- * Toggles the arrows visibility
- * @param {Boolean} showArrows if showArrows is passed, it will set the visibility to the passed value
- */
- const toggleArrows = useCallback(
- (showArrows?: boolean) => {
- if (showArrows === undefined) {
- setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows);
- return;
- }
-
- setShouldShowArrows(showArrows);
- },
- [setShouldShowArrows],
- );
-
const containerStyles = [styles.flex1, styles.attachmentCarouselContainer];
if (page == null) {
@@ -147,7 +131,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
items={attachments}
initialPage={page}
activeSource={activeSource}
- onRequestToggleArrows={toggleArrows}
+ setShouldShowArrows={setShouldShowArrows}
onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
onClose={onClose}
ref={pagerRef}
diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx
index eeac97bc5fa5..f7ef2c6529ce 100644
--- a/src/components/Attachments/AttachmentCarousel/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -1,10 +1,12 @@
import isEqual from 'lodash/isEqual';
-import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import type {MutableRefObject} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {ListRenderItemInfo} from 'react-native';
import {Keyboard, PixelRatio, View} from 'react-native';
+import type {GestureType} from 'react-native-gesture-handler';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {withOnyx} from 'react-native-onyx';
-import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated';
+import Animated, {scrollTo, useAnimatedRef, useSharedValue} from 'react-native-reanimated';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
import * as Illustrations from '@components/Icon/Illustrations';
@@ -22,8 +24,10 @@ import CarouselActions from './CarouselActions';
import CarouselButtons from './CarouselButtons';
import CarouselItem from './CarouselItem';
import extractAttachments from './extractAttachments';
+import AttachmentCarouselPagerContext from './Pager/AttachmentCarouselPagerContext';
import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps, UpdatePageProps} from './types';
import useCarouselArrows from './useCarouselArrows';
+import useCarouselContextEvents from './useCarouselContextEvents';
const viewabilityConfig = {
// To facilitate paging through the attachments, we want to consider an item "viewable" when it is
@@ -33,13 +37,15 @@ const viewabilityConfig = {
const MIN_FLING_VELOCITY = 500;
-function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) {
+function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose}: AttachmentCarouselProps) {
const theme = useTheme();
const {translate} = useLocalize();
const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
const styles = useThemeStyles();
const {isFullScreenRef} = useFullScreenContext();
const scrollRef = useAnimatedRef>>();
+ const nope = useSharedValue(false);
+ const pagerRef = useRef(null);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
@@ -52,6 +58,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
const [attachments, setAttachments] = useState([]);
const [activeSource, setActiveSource] = useState(source);
const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
+ const {handleTap, handleScaleChange, scale} = useCarouselContextEvents(setShouldShowArrows);
+
+ useEffect(() => {
+ if (!canUseTouchScreen) {
+ return;
+ }
+ setShouldShowArrows(true);
+ }, [canUseTouchScreen, page, setShouldShowArrows]);
const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]);
@@ -101,7 +115,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
scrollRef.current.scrollToIndex({index: page, animated: false});
// The hook is not supposed to run on page change, so we keep the page out of the dependencies
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [cellWidth]);
/** Updates the page state when the user navigates between attachments */
@@ -121,7 +135,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
return;
}
- const item: Attachment = entry.item;
+ const item = entry.item as Attachment;
if (entry.index !== null) {
setPage(entry.index);
setActiveSource(item.source);
@@ -169,6 +183,20 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[cellWidth],
);
+ const context = useMemo(
+ () => ({
+ pagerItems: [{source, index: 0, isActive: true}],
+ activePage: 0,
+ pagerRef,
+ isPagerScrolling: nope,
+ isScrollEnabled: nope,
+ onTap: handleTap,
+ onScaleChanged: handleScaleChange,
+ onSwipeDown: onClose,
+ }),
+ [source, nope, handleTap, handleScaleChange, onClose],
+ );
+
/** Defines how a single attachment should be rendered */
const renderItem = useCallback(
({item}: ListRenderItemInfo) => (
@@ -176,20 +204,30 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
setShouldShowArrows((oldState) => !oldState) : undefined}
+ onPress={canUseTouchScreen ? handleTap : undefined}
isModalHovered={shouldShowArrows}
/>
),
- [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100],
+ [activeSource, canUseTouchScreen, cellWidth, handleTap, shouldShowArrows, styles.h100],
);
/** Pan gesture handing swiping through attachments on touch screen devices */
const pan = useMemo(
() =>
Gesture.Pan()
.enabled(canUseTouchScreen)
- .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false))
+ .onUpdate(({translationX}) => {
+ if (scale.current !== 1) {
+ return;
+ }
+
+ scrollTo(scrollRef, page * cellWidth - translationX, 0, false);
+ })
.onEnd(({translationX, velocityX}) => {
+ if (scale.current !== 1) {
+ return;
+ }
+
let newIndex;
if (velocityX > MIN_FLING_VELOCITY) {
// User flung to the right
@@ -204,8 +242,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
}
scrollTo(scrollRef, newIndex * cellWidth, 0, true);
- }),
- [attachments.length, canUseTouchScreen, cellWidth, page, scrollRef],
+ })
+ .withRef(pagerRef as MutableRefObject),
+ [attachments.length, canUseTouchScreen, cellWidth, page, scale, scrollRef],
);
return (
@@ -233,27 +272,28 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
autoHideArrow={autoHideArrows}
cancelAutoHideArrow={cancelAutoHideArrows}
/>
-
-
-
-
+
+
+
+
+
>
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
index 12ca3db4e2ff..ed195fd943f1 100644
--- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
@@ -32,6 +32,9 @@ function useCarouselArrows() {
}, CONST.ARROW_HIDE_DELAY);
}, [canUseTouchScreen, cancelAutoHideArrows]);
+ /**
+ * Sets the visibility of the arrows.
+ */
const setShouldShowArrows = useCallback(
(show: SetStateAction = true) => {
setShouldShowArrowsInternal(show);
@@ -42,7 +45,7 @@ function useCarouselArrows() {
useEffect(() => {
autoHideArrows();
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows};
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts
new file mode 100644
index 000000000000..cc2c3c5c8229
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts
@@ -0,0 +1,64 @@
+import {useCallback, useRef} from 'react';
+import type {SetStateAction} from 'react';
+import {useSharedValue} from 'react-native-reanimated';
+
+function useCarouselContextEvents(setShouldShowArrows: (show?: SetStateAction) => void) {
+ const scale = useRef(1);
+ const isScrollEnabled = useSharedValue(true);
+
+ /**
+ * Toggles the arrows visibility
+ */
+ const onRequestToggleArrows = useCallback(
+ (showArrows?: boolean) => {
+ if (showArrows === undefined) {
+ setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows);
+ return;
+ }
+
+ setShouldShowArrows(showArrows);
+ },
+ [setShouldShowArrows],
+ );
+
+ /**
+ * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
+ * It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager,
+ * as well as enabling/disabling the carousel buttons.
+ */
+ const handleScaleChange = useCallback(
+ (newScale: number) => {
+ if (newScale === scale.current) {
+ return;
+ }
+
+ scale.current = newScale;
+
+ const newIsScrollEnabled = newScale === 1;
+ if (isScrollEnabled.value === newIsScrollEnabled) {
+ return;
+ }
+
+ // eslint-disable-next-line react-compiler/react-compiler
+ isScrollEnabled.value = newIsScrollEnabled;
+ onRequestToggleArrows(newIsScrollEnabled);
+ },
+ [isScrollEnabled, onRequestToggleArrows],
+ );
+
+ /**
+ * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
+ * It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox.
+ */
+ const handleTap = useCallback(() => {
+ if (!isScrollEnabled.value) {
+ return;
+ }
+
+ onRequestToggleArrows();
+ }, [isScrollEnabled.value, onRequestToggleArrows]);
+
+ return {handleTap, handleScaleChange, scale};
+}
+
+export default useCarouselContextEvents;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
index 765bc2b2a4f2..742b1213fa31 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
@@ -25,7 +25,7 @@ function BaseAttachmentViewPdf({
return;
}
attachmentCarouselPagerContext.onScaleChanged(1);
- // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we just want to call this function when component is mounted
}, []);
/**
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
index 9f74b5c7bfe8..8c4af3275bd8 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
@@ -41,6 +41,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
// enable the pager scroll so that the user
// can swipe to the next attachment otherwise disable it.
if (translateX > translateY && translateX > SCROLL_THRESHOLD && scale.value === 1 && allowEnablingScroll) {
+ // eslint-disable-next-line react-compiler/react-compiler
isScrollEnabled.value = true;
} else if (translateY > SCROLL_THRESHOLD) {
isScrollEnabled.value = false;
diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
index e06ea3064150..ee594f66aabc 100644
--- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
@@ -8,6 +8,7 @@ import Tooltip from '@components/Tooltip';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import type IconAsset from '@src/types/utils/IconAsset';
type DefaultAttachmentViewProps = {
/** The name of the file */
@@ -21,9 +22,11 @@ type DefaultAttachmentViewProps = {
/** Additional styles for the container */
containerStyles?: StyleProp;
+
+ icon?: IconAsset;
};
-function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles}: DefaultAttachmentViewProps) {
+function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon}: DefaultAttachmentViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -33,7 +36,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa
diff --git a/src/components/Attachments/AttachmentView/HighResolutionInfo.tsx b/src/components/Attachments/AttachmentView/HighResolutionInfo.tsx
new file mode 100644
index 000000000000..7ea3c83aa96f
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/HighResolutionInfo.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import {View} from 'react-native';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+
+function HighResolutionInfo({isUploaded}: {isUploaded: boolean}) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const stylesUtils = useStyleUtils();
+ const {translate} = useLocalize();
+
+ return (
+
+
+ {isUploaded ? translate('attachmentPicker.attachmentImageResized') : translate('attachmentPicker.attachmentImageTooLarge')}
+
+ );
+}
+
+export default HighResolutionInfo;
diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index a7409e57f846..39c25706bbfe 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -8,6 +8,7 @@ import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceipt from '@components/EReceipt';
import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
import ScrollView from '@components/ScrollView';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import useLocalize from '@hooks/useLocalize';
@@ -17,6 +18,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
+import * as FileUtils from '@libs/fileDownload/FileUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import type {ColorValue} from '@styles/utils/types';
import variables from '@styles/variables';
@@ -26,6 +28,7 @@ import AttachmentViewImage from './AttachmentViewImage';
import AttachmentViewPdf from './AttachmentViewPdf';
import AttachmentViewVideo from './AttachmentViewVideo';
import DefaultAttachmentView from './DefaultAttachmentView';
+import HighResolutionInfo from './HighResolutionInfo';
type AttachmentViewOnyxProps = {
transaction: OnyxEntry;
@@ -70,10 +73,14 @@ type AttachmentViewProps = AttachmentViewOnyxProps &
/** Whether the attachment is used as a chat attachment */
isUsedAsChatAttachment?: boolean;
+
+ /* Flag indicating whether the attachment has been uploaded. */
+ isUploaded?: boolean;
};
function AttachmentView({
source,
+ previewSource,
file,
isAuthTokenRequired,
onPress,
@@ -92,6 +99,7 @@ function AttachmentView({
isHovered,
duration,
isUsedAsChatAttachment,
+ isUploaded = true,
}: AttachmentViewProps) {
const {translate} = useLocalize();
const {updateCurrentlyPlayingURL} = usePlaybackContext();
@@ -99,6 +107,7 @@ function AttachmentView({
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [loadComplete, setLoadComplete] = useState(false);
+ const [isHighResolution, setIsHighResolution] = useState(false);
const [hasPDFFailedToLoad, setHasPDFFailedToLoad] = useState(false);
const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file?.name && Str.isVideo(file.name));
@@ -113,6 +122,12 @@ function AttachmentView({
useNetwork({onReconnect: () => setImageError(false)});
+ useEffect(() => {
+ FileUtils.getFileResolution(file).then((resolution) => {
+ setIsHighResolution(FileUtils.isHighResolutionImage(resolution));
+ });
+ }, [file]);
+
// Handles case where source is a component (ex: SVG) or a number
// Number may represent a SVG or an image
if (typeof source === 'function' || (maybeIcon && typeof source === 'number')) {
@@ -196,35 +211,61 @@ function AttachmentView({
// For this check we use both source and file.name since temporary file source is a blob
// both PDFs and images will appear as images when pasted into the text field.
// We also check for numeric source since this is how static images (used for preview) are represented in RN.
- const isImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
- if (isImage || (file?.name && Str.isImage(file.name))) {
- if (imageError) {
- // AttachmentViewImage can't handle icon fallbacks, so we need to handle it here
- if (typeof fallbackSource === 'number' || typeof fallbackSource === 'function') {
+ const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
+ const isFileNameImage = file?.name && Str.isImage(file.name);
+ const isFileImage = isSourceImage || isFileNameImage;
+
+ if (isFileImage) {
+ if (imageError && (typeof fallbackSource === 'number' || typeof fallbackSource === 'function')) {
+ return (
+
+ );
+ }
+ let imageSource = imageError && fallbackSource ? (fallbackSource as string) : (source as string);
+
+ if (isHighResolution) {
+ if (!isUploaded) {
return (
-
+ <>
+
+
+
+
+ >
);
}
+ imageSource = previewSource?.toString() ?? imageSource;
}
return (
- {
- setImageError(true);
- }}
- />
+ <>
+
+ {
+ setImageError(true);
+ }}
+ />
+
+ {isHighResolution && }
+ >
);
}
diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts
index 835482ca99d9..8bac4cc53af6 100644
--- a/src/components/Attachments/types.ts
+++ b/src/components/Attachments/types.ts
@@ -13,6 +13,9 @@ type Attachment = {
/** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
source: AttachmentSource;
+ /** URL to preview-sized attachment that is also used for the thumbnail */
+ previewSource?: AttachmentSource;
+
/** File object can be an instance of File or Object */
file?: FileObject;
diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx
new file mode 100644
index 000000000000..c761faccad39
--- /dev/null
+++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx
@@ -0,0 +1,47 @@
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import type {PointerEvent} from 'react-native';
+import type PressableProps from '@components/Pressable/GenericPressable/types';
+import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+type TransparentOverlayProps = {
+ resetSuggestions: () => void;
+};
+
+type OnPressHandler = PressableProps['onPress'];
+
+function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const onResetSuggestions = useCallback>(
+ (event) => {
+ event?.preventDefault();
+ resetSuggestions();
+ },
+ [resetSuggestions],
+ );
+
+ const handlePointerDown = useCallback((e: PointerEvent) => {
+ e?.preventDefault();
+ }, []);
+
+ return (
+
+
+
+ );
+}
+
+export default TransparentOverlay;
diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts
index 3ad9bbe7b152..acdc643b6b70 100644
--- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts
+++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts
@@ -1,5 +1,5 @@
function getBottomSuggestionPadding(): number {
- return 0;
+ return 6;
}
export default getBottomSuggestionPadding;
diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx
index 9848d77e479e..9ac43c4d8830 100644
--- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx
+++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx
@@ -4,9 +4,10 @@ import {View} from 'react-native';
import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions';
import useStyleUtils from '@hooks/useStyleUtils';
import getBottomSuggestionPadding from './getBottomSuggestionPadding';
+import TransparentOverlay from './TransparentOverlay/TransparentOverlay';
import type {AutoCompleteSuggestionsPortalProps} from './types';
-function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps) {
+function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, resetSuggestions = () => {}, ...props}: AutoCompleteSuggestionsPortalProps) {
const StyleUtils = useStyleUtils();
const styles = useMemo(() => StyleUtils.getBaseAutoCompleteSuggestionContainerStyle({left, width, bottom: bottom + getBottomSuggestionPadding()}), [StyleUtils, left, width, bottom]);
@@ -16,6 +17,7 @@ function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom
return (
+
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx
index 2d1d533c2859..d26dd0422368 100644
--- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx
+++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx
@@ -5,6 +5,7 @@ import {View} from 'react-native';
import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions';
import useStyleUtils from '@hooks/useStyleUtils';
import getBottomSuggestionPadding from './getBottomSuggestionPadding';
+import TransparentOverlay from './TransparentOverlay/TransparentOverlay';
import type {AutoCompleteSuggestionsPortalProps} from './types';
/**
@@ -14,7 +15,13 @@ import type {AutoCompleteSuggestionsPortalProps} from './types';
* On the native platform, tapping on auto-complete suggestions will not blur the main input.
*/
-function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps): ReactElement | null | false {
+function AutoCompleteSuggestionsPortal({
+ left = 0,
+ width = 0,
+ bottom = 0,
+ resetSuggestions = () => {},
+ ...props
+}: AutoCompleteSuggestionsPortalProps): ReactElement | null | false {
const StyleUtils = useStyleUtils();
const bodyElement = document.querySelector('body');
@@ -31,7 +38,10 @@ function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom
!!width &&
bodyElement &&
ReactDOM.createPortal(
- {componentToRender} ,
+ <>
+
+ {componentToRender}
+ >,
bodyElement,
)
);
diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
index 70d70a8c1844..2d22a2560bb0 100644
--- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
+++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
@@ -56,6 +56,7 @@ function BaseAutoCompleteSuggestions({
useEffect(() => {
if (measuredHeightOfSuggestionRows === prevRowHeightRef.current) {
+ // eslint-disable-next-line react-compiler/react-compiler
fadeInOpacity.value = withTiming(1, {
duration: 70,
easing: Easing.inOut(Easing.ease),
diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx
index 8634d6dd0ca0..41a01fa27c46 100644
--- a/src/components/AutoCompleteSuggestions/index.tsx
+++ b/src/components/AutoCompleteSuggestions/index.tsx
@@ -22,10 +22,25 @@ const measureHeightOfSuggestionRows = (numRows: number, canBeBig: boolean): numb
}
return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT;
};
-function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSpaceAboveForSmall: boolean): boolean {
- return isEnoughSpaceAboveForBig || isEnoughSpaceAboveForSmall;
+function isSuggestionMenuRenderedAbove(isEnoughSpaceAboveForBigMenu: boolean, isEnoughSpaceAboveForSmallMenu: boolean): boolean {
+ return isEnoughSpaceAboveForBigMenu || isEnoughSpaceAboveForSmallMenu;
}
+type IsEnoughSpaceToRenderMenuAboveCursor = Pick & {
+ contentHeight: number;
+ topInset: number;
+};
+function isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight, topInset}: IsEnoughSpaceToRenderMenuAboveCursor): boolean {
+ return y + (cursorCoordinates.y - scrollValue) > contentHeight + topInset + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE;
+}
+
+const initialContainerState = {
+ width: 0,
+ left: 0,
+ bottom: 0,
+ cursorCoordinates: {x: 0, y: 0},
+};
+
/**
* On the mobile-web platform, when long-pressing on auto-complete suggestions,
* we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback).
@@ -35,20 +50,16 @@ function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSp
function AutoCompleteSuggestions({measureParentContainerAndReportCursor = () => {}, ...props}: AutoCompleteSuggestionsProps) {
const containerRef = React.useRef(null);
const isInitialRender = React.useRef(true);
- const isSuggestionAboveRef = React.useRef(false);
+ const isSuggestionMenuAboveRef = React.useRef(false);
const leftValue = React.useRef(0);
const prevLeftValue = React.useRef(0);
const {windowHeight, windowWidth, isSmallScreenWidth} = useWindowDimensions();
const [suggestionHeight, setSuggestionHeight] = React.useState(0);
- const [containerState, setContainerState] = React.useState({
- width: 0,
- left: 0,
- bottom: 0,
- });
+ const [containerState, setContainerState] = React.useState(initialContainerState);
const StyleUtils = useStyleUtils();
const insets = useSafeAreaInsets();
const {keyboardHeight} = useKeyboardState();
- const {paddingBottom: bottomInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined);
+ const {paddingBottom: bottomInset, paddingTop: topInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined);
useEffect(() => {
const container = containerRef.current;
@@ -71,53 +82,58 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu
return;
}
+ if (!windowHeight || !windowWidth || !suggestionsLength) {
+ setContainerState(initialContainerState);
+ return;
+ }
+
measureParentContainerAndReportCursor(({x, y, width, scrollValue, cursorCoordinates}: MeasureParentContainerAndCursor) => {
const xCoordinatesOfCursor = x + cursorCoordinates.x;
- const leftValueForBigScreen =
+ const bigScreenLeftOffset =
xCoordinatesOfCursor + CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH > windowWidth
? windowWidth - CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH
: xCoordinatesOfCursor;
-
- let bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - (keyboardHeight || bottomInset);
- const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH;
-
const contentMaxHeight = measureHeightOfSuggestionRows(suggestionsLength, true);
const contentMinHeight = measureHeightOfSuggestionRows(suggestionsLength, false);
- const isEnoughSpaceAboveForBig = windowHeight - bottomValue - contentMaxHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE;
- const isEnoughSpaceAboveForSmall = windowHeight - bottomValue - contentMinHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE;
+ let bottomValue = windowHeight - (cursorCoordinates.y - scrollValue + y) - keyboardHeight;
+ const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH;
+
+ const isEnoughSpaceToRenderMenuAboveForBig = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMaxHeight, topInset});
+ const isEnoughSpaceToRenderMenuAboveForSmall = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMinHeight, topInset});
- const newLeftValue = isSmallScreenWidth ? x : leftValueForBigScreen;
+ const newLeftOffset = isSmallScreenWidth ? x : bigScreenLeftOffset;
// If the suggested word is longer than 150 (approximately half the width of the suggestion popup), then adjust a new position of popup
- const isAdjustmentNeeded = Math.abs(prevLeftValue.current - leftValueForBigScreen) > 150;
+ const isAdjustmentNeeded = Math.abs(prevLeftValue.current - bigScreenLeftOffset) > 150;
if (isInitialRender.current || isAdjustmentNeeded) {
- isSuggestionAboveRef.current = isSuggestionRenderedAbove(isEnoughSpaceAboveForBig, isEnoughSpaceAboveForSmall);
- leftValue.current = newLeftValue;
+ isSuggestionMenuAboveRef.current = isSuggestionMenuRenderedAbove(isEnoughSpaceToRenderMenuAboveForBig, isEnoughSpaceToRenderMenuAboveForSmall);
+ leftValue.current = newLeftOffset;
isInitialRender.current = false;
- prevLeftValue.current = newLeftValue;
+ prevLeftValue.current = newLeftOffset;
}
let measuredHeight = 0;
- if (isSuggestionAboveRef.current && isEnoughSpaceAboveForBig) {
+ if (isSuggestionMenuAboveRef.current && isEnoughSpaceToRenderMenuAboveForBig) {
// calculation for big suggestion box above the cursor
measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true);
- } else if (isSuggestionAboveRef.current && isEnoughSpaceAboveForSmall) {
+ } else if (isSuggestionMenuAboveRef.current && isEnoughSpaceToRenderMenuAboveForSmall) {
// calculation for small suggestion box above the cursor
measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, false);
} else {
// calculation for big suggestion box below the cursor
measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true);
- bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT;
+ bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT - keyboardHeight;
}
setSuggestionHeight(measuredHeight);
setContainerState({
left: leftValue.current,
bottom: bottomValue,
width: widthValue,
+ cursorCoordinates,
});
});
- }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset]);
+ }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset, topInset]);
- if (containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) {
+ if ((containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) || (containerState.cursorCoordinates.x === 0 && containerState.cursorCoordinates.y === 0)) {
return null;
}
return (
diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts
index 48bb6b713032..57347cd65abe 100644
--- a/src/components/AutoCompleteSuggestions/types.ts
+++ b/src/components/AutoCompleteSuggestions/types.ts
@@ -42,6 +42,9 @@ type AutoCompleteSuggestionsProps = {
/** Measures the parent container's position and dimensions. Also add a cursor coordinates */
measureParentContainerAndReportCursor?: (props: MeasureParentContainerAndCursorCallback) => void;
+
+ /** Reset the emoji suggestions */
+ resetSuggestions?: () => void;
};
export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps, MeasureParentContainerAndCursorCallback, MeasureParentContainerAndCursor};
diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx
index 66814a44cf95..1a606b35f6d2 100644
--- a/src/components/AvatarCropModal/AvatarCropModal.tsx
+++ b/src/components/AvatarCropModal/AvatarCropModal.tsx
@@ -123,6 +123,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose
ImageSize.getSize(imageUri).then(({width, height, rotation: orginalRotation}) => {
// On Android devices ImageSize library returns also rotation parameter.
if (orginalRotation === 90 || orginalRotation === 270) {
+ // eslint-disable-next-line react-compiler/react-compiler
originalImageHeight.value = width;
originalImageWidth.value = height;
} else {
diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx
index b7f2a64090d6..5bfb0d5f6557 100644
--- a/src/components/AvatarCropModal/ImageCropView.tsx
+++ b/src/components/AvatarCropModal/ImageCropView.tsx
@@ -57,6 +57,8 @@ function ImageCropView({imageUri = '', containerSize = 0, panGesture = Gesture.P
// A reanimated memoized style, which updates when the image's size or scale changes.
const imageStyle = useAnimatedStyle(() => {
+ 'worklet';
+
const height = originalImageHeight.value;
const width = originalImageWidth.value;
const aspectRatio = height > width ? height / width : width / height;
diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx
index 9a9da65befa0..bac581da25e6 100644
--- a/src/components/AvatarCropModal/Slider.tsx
+++ b/src/components/AvatarCropModal/Slider.tsx
@@ -7,6 +7,7 @@ import type {SharedValue} from 'react-native-reanimated';
import Tooltip from '@components/Tooltip';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as Browser from '@libs/Browser';
import ControlSelection from '@libs/ControlSelection';
type SliderProps = {
@@ -29,9 +30,13 @@ function Slider({sliderValue, gestureCallbacks}: SliderProps) {
// A reanimated memoized style, which tracks
// a translateX shared value and updates the slider position.
- const rSliderStyle = useAnimatedStyle(() => ({
- transform: [{translateX: sliderValue.value}],
- }));
+ const rSliderStyle = useAnimatedStyle(() => {
+ 'worklet';
+
+ return {
+ transform: [{translateX: sliderValue.value}],
+ };
+ });
const panGesture = Gesture.Pan()
.minDistance(5)
@@ -62,7 +67,7 @@ function Slider({sliderValue, gestureCallbacks}: SliderProps) {
shiftVertical={-2}
>
{/* pointerEventsNone is a workaround to make sure the pan gesture works correctly on mobile safari */}
-
+
)}
diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx
index 15a004ba9b87..eac60bc4ad6d 100644
--- a/src/components/AvatarWithImagePicker.tsx
+++ b/src/components/AvatarWithImagePicker.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useRef, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import type {ImageStyle, StyleProp, ViewStyle} from 'react-native';
import useLocalize from '@hooks/useLocalize';
@@ -121,9 +121,6 @@ type AvatarWithImagePickerProps = {
/** Allows to open an image without Attachment Picker. */
enablePreview?: boolean;
- /** Hard disables the "View photo" option */
- shouldDisableViewPhoto?: boolean;
-
/** Optionally override the default "Edit" icon */
editIcon?: IconAsset;
@@ -157,7 +154,6 @@ function AvatarWithImagePicker({
disabled = false,
onViewPhotoPress,
enablePreview = false,
- shouldDisableViewPhoto = false,
editIcon = Expensicons.Pencil,
shouldUseStyleUtilityForAnchorPosition = false,
}: AvatarWithImagePickerProps) {
@@ -195,15 +191,15 @@ function AvatarWithImagePicker({
/**
* Check if the attachment extension is allowed.
*/
- const isValidExtension = (image: FileObject): boolean => {
+ const isValidExtension = useCallback((image: FileObject): boolean => {
const {fileExtension} = FileUtils.splitExtensionFromFileName(image?.name ?? '');
return CONST.AVATAR_ALLOWED_EXTENSIONS.some((extension) => extension === fileExtension.toLowerCase());
- };
+ }, []);
/**
* Check if the attachment size is less than allowed size.
*/
- const isValidSize = (image: FileObject): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE;
+ const isValidSize = useCallback((image: FileObject): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE, []);
/**
* Check if the attachment resolution matches constraints.
@@ -216,37 +212,40 @@ function AvatarWithImagePicker({
/**
* Validates if an image has a valid resolution and opens an avatar crop modal
*/
- const showAvatarCropModal = (image: FileObject) => {
- if (!isValidExtension(image)) {
- setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS});
- return;
- }
- if (!isValidSize(image)) {
- setError('avatarWithImagePicker.sizeExceeded', {maxUploadSizeInMB: CONST.AVATAR_MAX_ATTACHMENT_SIZE / (1024 * 1024)});
- return;
- }
-
- isValidResolution(image).then((isValid) => {
- if (!isValid) {
- setError('avatarWithImagePicker.resolutionConstraints', {
- minHeightInPx: CONST.AVATAR_MIN_HEIGHT_PX,
- minWidthInPx: CONST.AVATAR_MIN_WIDTH_PX,
- maxHeightInPx: CONST.AVATAR_MAX_HEIGHT_PX,
- maxWidthInPx: CONST.AVATAR_MAX_WIDTH_PX,
- });
+ const showAvatarCropModal = useCallback(
+ (image: FileObject) => {
+ if (!isValidExtension(image)) {
+ setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS});
+ return;
+ }
+ if (!isValidSize(image)) {
+ setError('avatarWithImagePicker.sizeExceeded', {maxUploadSizeInMB: CONST.AVATAR_MAX_ATTACHMENT_SIZE / (1024 * 1024)});
return;
}
- setIsAvatarCropModalOpen(true);
- setError(null, {});
- setIsMenuVisible(false);
- setImageData({
- uri: image.uri ?? '',
- name: image.name ?? '',
- type: image.type ?? '',
+ isValidResolution(image).then((isValid) => {
+ if (!isValid) {
+ setError('avatarWithImagePicker.resolutionConstraints', {
+ minHeightInPx: CONST.AVATAR_MIN_HEIGHT_PX,
+ minWidthInPx: CONST.AVATAR_MIN_WIDTH_PX,
+ maxHeightInPx: CONST.AVATAR_MAX_HEIGHT_PX,
+ maxWidthInPx: CONST.AVATAR_MAX_WIDTH_PX,
+ });
+ return;
+ }
+
+ setIsAvatarCropModalOpen(true);
+ setError(null, {});
+ setIsMenuVisible(false);
+ setImageData({
+ uri: image.uri ?? '',
+ name: image.name ?? '',
+ type: image.type ?? '',
+ });
});
- });
- };
+ },
+ [isValidExtension, isValidSize],
+ );
const hideAvatarCropModal = () => {
setIsAvatarCropModalOpen(false);
@@ -302,61 +301,26 @@ function AvatarWithImagePicker({
});
}, [isMenuVisible, windowWidth]);
+ const onPressAvatar = useCallback(
+ (openPicker: OpenPicker) => {
+ if (isUsingDefaultAvatar) {
+ openPicker({
+ onPicked: showAvatarCropModal,
+ });
+ return;
+ }
+ if (disabled && enablePreview && onViewPhotoPress) {
+ onViewPhotoPress();
+ return;
+ }
+ setIsMenuVisible((prev) => !prev);
+ },
+ [disabled, enablePreview, isUsingDefaultAvatar, onViewPhotoPress, showAvatarCropModal],
+ );
+
return (
-
-
- {
- if (disabled && enablePreview && onViewPhotoPress) {
- onViewPhotoPress();
- return;
- }
- setIsMenuVisible((prev) => !prev);
- }}
- accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
- accessibilityLabel={translate('avatarWithImagePicker.editImage')}
- disabled={isAvatarCropModalOpen || (disabled && !enablePreview)}
- disabledStyle={disabledStyle}
- style={[styles.pRelative, avatarStyle, type === CONST.ICON_TYPE_AVATAR && styles.alignSelfCenter]}
- ref={anchorRef}
- >
-
- {source ? (
-
- ) : (
-
- )}
-
- {!disabled && (
-
-
-
- )}
-
-
-
setIsMenuVisible(false)}
- onItemSelected={(item, index) => {
- setIsMenuVisible(false);
- // In order for the file picker to open dynamically, the click
- // function must be called from within an event handler that was initiated
- // by the user on Safari.
- if (index === 0 && Browser.isSafari()) {
- openPicker({
- onPicked: showAvatarCropModal,
- });
- }
- }}
- menuItems={menuItems}
- anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverMenuOffset(windowWidth) : popoverPosition}
- anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
- withoutOverlay
- anchorRef={anchorRef}
- />
+ <>
+
+
+ onPressAvatar(openPicker)}
+ accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
+ accessibilityLabel={translate('avatarWithImagePicker.editImage')}
+ disabled={isAvatarCropModalOpen || (disabled && !enablePreview)}
+ disabledStyle={disabledStyle}
+ style={[styles.pRelative, avatarStyle, type === CONST.ICON_TYPE_AVATAR && styles.alignSelfCenter]}
+ ref={anchorRef}
+ >
+
+ {source ? (
+
+ ) : (
+
+ )}
+
+ {!disabled && (
+
+
+
+ )}
+
+
+
+ setIsMenuVisible(false)}
+ onItemSelected={(item, index) => {
+ setIsMenuVisible(false);
+ // In order for the file picker to open dynamically, the click
+ // function must be called from within an event handler that was initiated
+ // by the user on Safari.
+ if (index === 0 && Browser.isSafari()) {
+ openPicker({
+ onPicked: showAvatarCropModal,
+ });
+ }
+ }}
+ menuItems={menuItems}
+ anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverMenuOffset(windowWidth) : popoverPosition}
+ anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
+ withoutOverlay
+ anchorRef={anchorRef}
+ />
+ >
);
}}
diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx
index e641a0c2218a..4cbf85cb0014 100644
--- a/src/components/Breadcrumbs.tsx
+++ b/src/components/Breadcrumbs.tsx
@@ -36,10 +36,11 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) {
const theme = useTheme();
const styles = useThemeStyles();
const [primaryBreadcrumb, secondaryBreadcrumb] = breadcrumbs;
+ const isRootBreadcrumb = primaryBreadcrumb.type === CONST.BREADCRUMB_TYPE.ROOT;
const fontScale = PixelRatio.getFontScale() > CONST.LOGO_MAX_SCALE ? CONST.LOGO_MAX_SCALE : PixelRatio.getFontScale();
return (
- {primaryBreadcrumb.type === CONST.BREADCRUMB_TYPE.ROOT ? (
+ {isRootBreadcrumb ? (
/
{secondaryBreadcrumb.text}
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 88ae8d48a871..126c81961cee 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -1,6 +1,6 @@
import {useIsFocused} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
-import React, {useCallback, useMemo} from 'react';
+import React, {useCallback, useMemo, useState} from 'react';
import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
import Icon from '@components/Icon';
@@ -89,6 +89,9 @@ type ButtonProps = Partial & {
/** Whether we should use the danger theme color */
danger?: boolean;
+ /** Whether we should display the button as a link */
+ link?: boolean;
+
/** Should we remove the right border radius top + bottom? */
shouldRemoveRightBorderRadius?: boolean;
@@ -118,6 +121,9 @@ type ButtonProps = Partial & {
/** Whether the button should use split style or not */
isSplitButton?: boolean;
+
+ /** Whether button's content should be centered */
+ isContentCentered?: boolean;
};
type KeyboardShortcutComponentProps = Pick;
@@ -147,7 +153,7 @@ function KeyboardShortcutComponent({isDisabled = false, isLoading = false, onPre
priority: enterKeyEventListenerPriority,
shouldPreventDefault: false,
}),
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[shouldDisableEnterShortcut, isFocused],
);
@@ -202,6 +208,8 @@ function Button(
id = '',
accessibilityLabel = '',
isSplitButton = false,
+ link = false,
+ isContentCentered = false,
...rest
}: ButtonProps,
ref: ForwardedRef,
@@ -209,6 +217,7 @@ function Button(
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const [isHovered, setIsHovered] = useState(false);
const renderContent = () => {
if ('children' in rest) {
@@ -229,6 +238,10 @@ function Button(
danger && styles.buttonDangerText,
!!icon && styles.textAlignLeft,
textStyles,
+ link && styles.link,
+ link && isHovered && StyleUtils.getColorStyle(theme.linkHover),
+ link && styles.fontWeightNormal,
+ link && styles.fontSizeLabel,
]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
@@ -239,7 +252,7 @@ function Button(
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (icon || shouldShowRightIcon) {
return (
-
+
{icon && (
@@ -339,6 +352,7 @@ function Button(
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
text && shouldShowRightIcon ? styles.alignItemsStretch : undefined,
innerStyles,
+ link && styles.bgTransparent,
]}
hoverStyle={[
shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined,
@@ -349,6 +363,8 @@ function Button(
accessibilityLabel={accessibilityLabel}
role={CONST.ROLE.BUTTON}
hoverDimmingValue={1}
+ onHoverIn={() => setIsHovered(true)}
+ onHoverOut={() => setIsHovered(false)}
>
{renderContent()}
{isLoading && (
diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx
index 094c26a2b387..d1e36327a1a3 100644
--- a/src/components/ButtonWithDropdownMenu/index.tsx
+++ b/src/components/ButtonWithDropdownMenu/index.tsx
@@ -4,6 +4,7 @@ import Button from '@components/Button';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PopoverMenu from '@components/PopoverMenu';
+import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -33,6 +34,7 @@ function ButtonWithDropdownMenu({
onOptionSelected,
enterKeyEventListenerPriority = 0,
wrapperStyle,
+ useKeyboardShortcuts = false,
}: ButtonWithDropdownMenuProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -65,6 +67,26 @@ function ButtonWithDropdownMenu({
});
}
}, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]);
+
+ useKeyboardShortcut(
+ CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER,
+ (e) => {
+ onPress(e, selectedItem.value);
+ },
+ {
+ captureOnInputs: true,
+ shouldBubble: false,
+ isActive: useKeyboardShortcuts,
+ },
+ );
+
+ useEffect(() => {
+ if (!!caretButton.current || !buttonRef?.current || !(shouldAlwaysShowDropdownMenu || options.length > 1)) {
+ return;
+ }
+ caretButton.current = buttonRef.current;
+ }, [buttonRef, options.length, shouldAlwaysShowDropdownMenu]);
+
return (
{shouldAlwaysShowDropdownMenu || options.length > 1 ? (
@@ -72,9 +94,7 @@ function ButtonWithDropdownMenu({
{
- caretButton.current = ref;
- }}
+ ref={buttonRef}
onPress={(event) => (!isSplitButton ? setIsMenuVisible(!isMenuVisible) : onPress(event, selectedItem.value))}
text={customText ?? selectedItem.text}
isDisabled={isDisabled || !!selectedItem.disabled}
diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts
index 1ad2ccb0d717..bf9cdc33cb81 100644
--- a/src/components/ButtonWithDropdownMenu/types.ts
+++ b/src/components/ButtonWithDropdownMenu/types.ts
@@ -10,9 +10,11 @@ type PaymentType = DeepValueOf;
-type WorkspaceDistanceRatesBulkActionType = DeepValueOf;
+type WorkspaceDistanceRatesBulkActionType = DeepValueOf;
-type WorkspaceTaxRatesBulkActionType = DeepValueOf;
+type WorkspaceTaxRatesBulkActionType = DeepValueOf;
+
+type ReportExportType = DeepValueOf;
type DropdownOption = {
value: TValueType;
@@ -23,6 +25,11 @@ type DropdownOption = {
iconDescription?: string;
onSelected?: () => void;
disabled?: boolean;
+ iconFill?: string;
+ interactive?: boolean;
+ numberOfLinesTitle?: number;
+ titleStyle?: ViewStyle;
+ shouldCloseModalOnSelect?: boolean;
};
type ButtonWithDropdownMenuProps = {
@@ -77,6 +84,17 @@ type ButtonWithDropdownMenuProps = {
/** Whether the button should use split style or not */
isSplitButton?: boolean;
+
+ /** Whether to use keyboard shortcuts for confirmation or not */
+ useKeyboardShortcuts?: boolean;
};
-export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps, WorkspaceTaxRatesBulkActionType};
+export type {
+ PaymentType,
+ WorkspaceMemberBulkActionType,
+ WorkspaceDistanceRatesBulkActionType,
+ DropdownOption,
+ ButtonWithDropdownMenuProps,
+ WorkspaceTaxRatesBulkActionType,
+ ReportExportType,
+};
diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx
index 5445816f067b..6da170da3a67 100644
--- a/src/components/Checkbox.tsx
+++ b/src/components/Checkbox.tsx
@@ -44,6 +44,9 @@ type CheckboxProps = Partial & {
/** An accessibility label for the checkbox */
accessibilityLabel: string;
+
+ /** stop propagation of the mouse down event */
+ shouldStopMouseDownPropagation?: boolean;
};
function Checkbox(
@@ -60,6 +63,7 @@ function Checkbox(
caretSize = 14,
onPress,
accessibilityLabel,
+ shouldStopMouseDownPropagation,
}: CheckboxProps,
ref: ForwardedRef,
) {
@@ -89,7 +93,12 @@ function Checkbox(
{
+ if (shouldStopMouseDownPropagation) {
+ e.stopPropagation();
+ }
+ onMouseDown?.(e);
+ }}
ref={ref}
style={[StyleUtils.getCheckboxPressableStyle(containerBorderRadius + 2), style]} // to align outline on focus, border-radius of pressable should be 2px more than Checkbox
onKeyDown={handleSpaceKey}
diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx
index 9e66c0b20c99..c5f2e07eef80 100644
--- a/src/components/Composer/index.native.tsx
+++ b/src/components/Composer/index.native.tsx
@@ -50,6 +50,7 @@ function Composer(
* @param {Element} el
*/
const setTextInputRef = useCallback((el: AnimatedMarkdownTextInputRef) => {
+ // eslint-disable-next-line react-compiler/react-compiler
textInput.current = el;
if (typeof ref !== 'function' || textInput.current === null) {
return;
@@ -60,7 +61,7 @@ function Composer(
// this.textInput = el} /> this will not
// return a ref to the component, but rather the HTML element by default
ref(textInput.current);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
useEffect(() => {
@@ -93,6 +94,7 @@ function Composer(
readOnly={isDisabled}
onBlur={(e) => {
if (!isFocused) {
+ // eslint-disable-next-line react-compiler/react-compiler
shouldResetFocus.current = true; // detect the input is blurred when the page is hidden
}
props?.onBlur?.(e);
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index 3a8a4e724948..3889c8597843 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -5,7 +5,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {flushSync} from 'react-dom';
// eslint-disable-next-line no-restricted-imports
import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native';
-import {StyleSheet, View} from 'react-native';
+import {DeviceEventEmitter, StyleSheet, View} from 'react-native';
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
import Text from '@components/Text';
@@ -74,7 +74,7 @@ function Composer(
},
isReportActionCompose = false,
isComposerFullSize = false,
- shouldContainScroll = false,
+ shouldContainScroll = true,
isGroupPolicyReport = false,
...props
}: ComposerProps,
@@ -105,6 +105,7 @@ function Composer(
const [isRendered, setIsRendered] = useState(false);
const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? '');
const [prevScroll, setPrevScroll] = useState();
+ const isReportFlatListScrolling = useRef(false);
useEffect(() => {
if (!shouldClear) {
@@ -115,12 +116,11 @@ function Composer(
}, [shouldClear, onClear]);
useEffect(() => {
- setSelection((prevSelection) => {
- if (!!prevSelection && selectionProp.start === prevSelection.start && selectionProp.end === prevSelection.end) {
- return;
- }
- return selectionProp;
- });
+ if (!!selection && selectionProp.start === selection.start && selectionProp.end === selection.end) {
+ return;
+ }
+ setSelection(selectionProp);
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [selectionProp]);
/**
@@ -249,12 +249,36 @@ function Composer(
};
}, []);
+ useEffect(() => {
+ const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling: boolean) => {
+ isReportFlatListScrolling.current = scrolling;
+ });
+
+ return () => scrollingListener.remove();
+ }, []);
+
+ useEffect(() => {
+ const handleWheel = (e: MouseEvent) => {
+ if (isReportFlatListScrolling.current) {
+ e.preventDefault();
+ return;
+ }
+ e.stopPropagation();
+ };
+ textInput.current?.addEventListener('wheel', handleWheel, {passive: false});
+
+ return () => {
+ textInput.current?.removeEventListener('wheel', handleWheel);
+ };
+ }, []);
+
useEffect(() => {
if (!textInput.current || prevScroll === undefined) {
return;
}
+ // eslint-disable-next-line react-compiler/react-compiler
textInput.current.scrollTop = prevScroll;
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isComposerFullSize]);
useHtmlPaste(textInput, handlePaste, true);
@@ -271,7 +295,7 @@ function Composer(
}
ReportActionComposeFocusManager.clear();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
const handleKeyPress = useCallback(
diff --git a/src/components/ConfirmContent.tsx b/src/components/ConfirmContent.tsx
index 26331f92401c..36f24c2a3477 100644
--- a/src/components/ConfirmContent.tsx
+++ b/src/components/ConfirmContent.tsx
@@ -14,8 +14,11 @@ import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import Header from './Header';
import Icon from './Icon';
+import {Close} from './Icon/Expensicons';
import ImageSVG from './ImageSVG';
+import {PressableWithoutFeedback} from './Pressable';
import Text from './Text';
+import Tooltip from './Tooltip';
type ConfirmContentProps = {
/** Title of the modal */
@@ -51,15 +54,36 @@ type ConfirmContentProps = {
/** Icon to display above the title */
iconSource?: IconAsset;
+ /** Fill color for the Icon */
+ iconFill?: string | false;
+
+ /** Icon width */
+ iconWidth?: number;
+
+ /** Icon height */
+ iconHeight?: number;
+
+ /** Should the icon be centered? */
+ shouldCenterIcon?: boolean;
+
/** Whether to center the icon / text content */
shouldCenterContent?: boolean;
+ /** Whether to show the dismiss icon */
+ shouldShowDismissIcon?: boolean;
+
/** Whether to stack the buttons */
shouldStackButtons?: boolean;
+ /** Whether to reverse the order of the stacked buttons */
+ shouldReverseStackedButtons?: boolean;
+
/** Styles for title */
titleStyles?: StyleProp;
+ /** Styles for title container */
+ titleContainerStyles?: StyleProp;
+
/** Styles for prompt */
promptStyles?: StyleProp;
@@ -85,13 +109,20 @@ function ConfirmContent({
shouldDisableConfirmButtonWhenOffline = false,
shouldShowCancelButton = false,
iconSource,
+ iconFill,
shouldCenterContent = false,
shouldStackButtons = true,
titleStyles,
promptStyles,
contentStyles,
iconAdditionalStyles,
+ iconWidth = variables.appModalAppIconSize,
+ iconHeight = variables.appModalAppIconSize,
+ shouldCenterIcon = false,
+ shouldShowDismissIcon = false,
image,
+ titleContainerStyles,
+ shouldReverseStackedButtons = false,
}: ConfirmContentProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -116,19 +147,35 @@ function ConfirmContent({
)}
+ {shouldShowDismissIcon && (
+
+
+
+
+
+
+
+ )}
- {typeof iconSource === 'function' && (
-
+ {iconSource && (
+
)}
-
+
+ {shouldShowCancelButton && shouldReverseStackedButtons && (
+
+ )}
- {shouldShowCancelButton && (
+ {shouldShowCancelButton && !shouldReverseStackedButtons && (
;
+
/** Styles for title */
titleStyles?: StyleProp;
@@ -67,6 +85,9 @@ type ConfirmModalProps = {
/** Whether to stack the buttons */
shouldStackButtons?: boolean;
+ /** Whether to reverse the order of the stacked buttons */
+ shouldReverseStackedButtons?: boolean;
+
/** Image to display with content */
image?: IconAsset;
@@ -101,6 +122,13 @@ function ConfirmModal({
isVisible,
onConfirm,
image,
+ iconWidth,
+ iconHeight,
+ iconFill,
+ shouldCenterIcon,
+ shouldShowDismissIcon,
+ titleContainerStyles,
+ shouldReverseStackedButtons,
shouldEnableNewFocusManagement,
restoreFocusType,
}: ConfirmModalProps) {
@@ -134,10 +162,18 @@ function ConfirmModal({
shouldShowCancelButton={shouldShowCancelButton}
shouldCenterContent={shouldCenterContent}
iconSource={iconSource}
+ contentStyles={isSmallScreenWidth && shouldShowDismissIcon ? styles.mt2 : undefined}
+ iconFill={iconFill}
+ iconHeight={iconHeight}
+ iconWidth={iconWidth}
+ shouldCenterIcon={shouldCenterIcon}
+ shouldShowDismissIcon={shouldShowDismissIcon}
+ titleContainerStyles={titleContainerStyles}
iconAdditionalStyles={iconAdditionalStyles}
titleStyles={titleStyles}
promptStyles={promptStyles}
shouldStackButtons={shouldStackButtons}
+ shouldReverseStackedButtons={shouldReverseStackedButtons}
image={image}
/>
diff --git a/src/components/ConfirmationPage.tsx b/src/components/ConfirmationPage.tsx
index d1a73b7933fe..a95cf9bf87d2 100644
--- a/src/components/ConfirmationPage.tsx
+++ b/src/components/ConfirmationPage.tsx
@@ -1,9 +1,12 @@
import React from 'react';
-import type {TextStyle} from 'react-native';
+import type {TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
+import isIllustrationLottieAnimation from '@libs/isIllustrationLottieAnimation';
+import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import FixedFooter from './FixedFooter';
+import ImageSVG from './ImageSVG';
import Lottie from './Lottie';
import LottieAnimations from './LottieAnimations';
import type DotLottieAnimation from './LottieAnimations/types';
@@ -11,13 +14,13 @@ import Text from './Text';
type ConfirmationPageProps = {
/** The asset to render */
- animation?: DotLottieAnimation;
+ illustration?: DotLottieAnimation | IconAsset;
/** Heading of the confirmation page */
heading: string;
/** Description of the confirmation page */
- description: string;
+ description: React.ReactNode;
/** The text for the button label */
buttonText?: string;
@@ -31,31 +34,45 @@ type ConfirmationPageProps = {
/** Additional style for the heading */
headingStyle?: TextStyle;
+ /** Additional style for the animation */
+ illustrationStyle?: ViewStyle;
+
/** Additional style for the description */
descriptionStyle?: TextStyle;
};
function ConfirmationPage({
- animation = LottieAnimations.Fireworks,
+ illustration = LottieAnimations.Fireworks,
heading,
description,
buttonText = '',
onButtonPress = () => {},
shouldShowButton = false,
headingStyle,
+ illustrationStyle,
descriptionStyle,
}: ConfirmationPageProps) {
const styles = useThemeStyles();
+ const isLottie = isIllustrationLottieAnimation(illustration);
return (
<>
-
+ {isLottie ? (
+
+ ) : (
+
+
+
+ )}
{heading}
{description}
diff --git a/src/components/ConnectToNetSuiteButton/index.tsx b/src/components/ConnectToNetSuiteButton/index.tsx
index fc948503a127..928bc01f12c1 100644
--- a/src/components/ConnectToNetSuiteButton/index.tsx
+++ b/src/components/ConnectToNetSuiteButton/index.tsx
@@ -1,12 +1,21 @@
-import React, {useState} from 'react';
+import React, {useRef, useState} from 'react';
+import type {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import AccountingConnectionConfirmationModal from '@components/AccountingConnectionConfirmationModal';
import Button from '@components/Button';
+import * as Expensicons from '@components/Icon/Expensicons';
+import PopoverMenu from '@components/PopoverMenu';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import {removePolicyConnection} from '@libs/actions/connections';
+import {getAdminPoliciesConnectedToNetSuite} from '@libs/actions/Policy/Policy';
import Navigation from '@libs/Navigation/Navigation';
+import {isControlPolicy} from '@libs/PolicyUtils';
+import type {AnchorPosition} from '@styles/index';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {ConnectToNetSuiteButtonProps} from './types';
@@ -14,34 +23,105 @@ function ConnectToNetSuiteButton({policyID, shouldDisconnectIntegrationBeforeCon
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
+ const hasPoliciesConnectedToNetSuite = !!getAdminPoliciesConnectedToNetSuite()?.length;
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const [isReuseConnectionsPopoverOpen, setIsReuseConnectionsPopoverOpen] = useState(false);
+ const [reuseConnectionPopoverPosition, setReuseConnectionPopoverPosition] = useState({horizontal: 0, vertical: 0});
+ const threeDotsMenuContainerRef = useRef(null);
+ const connectionOptions = [
+ {
+ icon: Expensicons.LinkCopy,
+ text: translate('workspace.common.createNewConnection'),
+ onSelected: () => {
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT.getRoute(policyID));
+ setIsReuseConnectionsPopoverOpen(false);
+ },
+ },
+ {
+ icon: Expensicons.Copy,
+ text: translate('workspace.common.reuseExistingConnection'),
+ onSelected: () => {
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXISTING_CONNECTIONS.getRoute(policyID));
+ setIsReuseConnectionsPopoverOpen(false);
+ },
+ },
+ ];
+
return (
<>
{
+ if (!isControlPolicy(policy)) {
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.netsuite.alias));
+ return;
+ }
+
if (shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect) {
setIsDisconnectModalOpen(true);
return;
}
- // TODO: Will be updated to new token input page
- Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR.getRoute(policyID));
+ if (!hasPoliciesConnectedToNetSuite) {
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT.getRoute(policyID));
+ return;
+ }
+
+ if (!isSmallScreenWidth) {
+ threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => {
+ setReuseConnectionPopoverPosition({
+ horizontal: x + width,
+ vertical: y + height,
+ });
+ });
+ }
+ setIsReuseConnectionsPopoverOpen(true);
}}
text={translate('workspace.accounting.setup')}
style={styles.justifyContentCenter}
small
isDisabled={isOffline}
+ ref={threeDotsMenuContainerRef}
+ />
+ {
+ setIsReuseConnectionsPopoverOpen(false);
+ }}
+ withoutOverlay
+ menuItems={connectionOptions}
+ onItemSelected={(item) => {
+ if (!item?.onSelected) {
+ return;
+ }
+ item.onSelected();
+ }}
+ anchorPosition={reuseConnectionPopoverPosition}
+ anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
+ anchorRef={threeDotsMenuContainerRef}
/>
{shouldDisconnectIntegrationBeforeConnecting && isDisconnectModalOpen && integrationToDisconnect && (
{
removePolicyConnection(policyID, integrationToDisconnect);
-
- // TODO: Will be updated to new token input page
- Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR.getRoute(policyID));
setIsDisconnectModalOpen(false);
+
+ if (!hasPoliciesConnectedToNetSuite) {
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT.getRoute(policyID));
+ return;
+ }
+ if (!isSmallScreenWidth) {
+ threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => {
+ setReuseConnectionPopoverPosition({
+ horizontal: x + width,
+ vertical: y + height,
+ });
+ });
+ }
+ setIsReuseConnectionsPopoverOpen(true);
}}
integrationToConnect={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
onCancel={() => setIsDisconnectModalOpen(false)}
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/ConnectToQuickbooksOnlineButton/index.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.tsx
index 71f1fba91187..50ee9165b8a3 100644
--- a/src/components/ConnectToQuickbooksOnlineButton/index.tsx
+++ b/src/components/ConnectToQuickbooksOnlineButton/index.tsx
@@ -40,6 +40,8 @@ function ConnectToQuickbooksOnlineButton({policyID, shouldDisconnectIntegrationB
{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);
Link.openLink(getQuickBooksOnlineSetupLink(policyID), environmentURL);
setIsDisconnectModalOpen(false);
diff --git a/src/components/ConnectToSageIntacctButton/index.tsx b/src/components/ConnectToSageIntacctButton/index.tsx
index 460647838ec3..6c6523ad6e75 100644
--- a/src/components/ConnectToSageIntacctButton/index.tsx
+++ b/src/components/ConnectToSageIntacctButton/index.tsx
@@ -1,5 +1,6 @@
import React, {useRef, useState} from 'react';
import type {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import AccountingConnectionConfirmationModal from '@components/AccountingConnectionConfirmationModal';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -9,10 +10,12 @@ import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {removePolicyConnection} from '@libs/actions/connections';
-import {getPoliciesConnectedToSageIntacct} from '@libs/actions/Policy/Policy';
+import {getAdminPoliciesConnectedToSageIntacct} from '@libs/actions/Policy/Policy';
import Navigation from '@libs/Navigation/Navigation';
+import {isControlPolicy} from '@libs/PolicyUtils';
import type {AnchorPosition} from '@styles/index';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {PolicyConnectionName} from '@src/types/onyx/Policy';
@@ -27,9 +30,11 @@ function ConnectToSageIntacctButton({policyID, shouldDisconnectIntegrationBefore
const {translate} = useLocalize();
const {isOffline} = useNetwork();
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
- const hasPoliciesConnectedToSageIntacct = !!getPoliciesConnectedToSageIntacct().length;
+ const hasPoliciesConnectedToSageIntacct = !!getAdminPoliciesConnectedToSageIntacct().length;
const {isSmallScreenWidth} = useWindowDimensions();
const [isReuseConnectionsPopoverOpen, setIsReuseConnectionsPopoverOpen] = useState(false);
const [reuseConnectionPopoverPosition, setReuseConnectionPopoverPosition] = useState({horizontal: 0, vertical: 0});
@@ -37,7 +42,7 @@ function ConnectToSageIntacctButton({policyID, shouldDisconnectIntegrationBefore
const connectionOptions = [
{
icon: Expensicons.LinkCopy,
- text: translate('workspace.intacct.createNewConnection'),
+ text: translate('workspace.common.createNewConnection'),
onSelected: () => {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES.getRoute(policyID));
setIsReuseConnectionsPopoverOpen(false);
@@ -45,7 +50,7 @@ function ConnectToSageIntacctButton({policyID, shouldDisconnectIntegrationBefore
},
{
icon: Expensicons.Copy,
- text: translate('workspace.intacct.reuseExistingConnection'),
+ text: translate('workspace.common.reuseExistingConnection'),
onSelected: () => {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.getRoute(policyID));
setIsReuseConnectionsPopoverOpen(false);
@@ -57,6 +62,11 @@ function ConnectToSageIntacctButton({policyID, shouldDisconnectIntegrationBefore
<>
{
+ if (!isControlPolicy(policy)) {
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.intacct.alias));
+ return;
+ }
+
if (shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect) {
setIsDisconnectModalOpen(true);
return;
diff --git a/src/components/ConnectToXeroButton/index.native.tsx b/src/components/ConnectToXeroButton/index.native.tsx
index 15fe201f2ac9..04b5f8722ea5 100644
--- a/src/components/ConnectToXeroButton/index.native.tsx
+++ b/src/components/ConnectToXeroButton/index.native.tsx
@@ -1,6 +1,6 @@
import React, {useRef, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx, withOnyx} from 'react-native-onyx';
import {WebView} from 'react-native-webview';
import AccountingConnectionConfirmationModal from '@components/AccountingConnectionConfirmationModal';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
@@ -8,14 +8,17 @@ import Button from '@components/Button';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
+import RequireTwoFactorAuthenticationModal from '@components/RequireTwoFactorAuthenticationModal';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {removePolicyConnection} from '@libs/actions/connections';
import {getXeroSetupLink} from '@libs/actions/connections/ConnectToXero';
import getUAForWebView from '@libs/getUAForWebView';
+import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type {Session} from '@src/types/onyx';
import type {ConnectToXeroButtonProps} from './types';
@@ -33,13 +36,22 @@ function ConnectToXeroButton({policyID, session, shouldDisconnectIntegrationBefo
const authToken = session?.authToken ?? null;
const {isOffline} = useNetwork();
+ const [account] = useOnyx(ONYXKEYS.ACCOUNT);
+ const is2FAEnabled = account?.requiresTwoFactorAuth ?? false;
+
const renderLoading = () => ;
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
+ const [isRequire2FAModalOpen, setIsRequire2FAModalOpen] = useState(false);
return (
<>
{
+ if (!is2FAEnabled) {
+ setIsRequire2FAModalOpen(true);
+ return;
+ }
+
if (shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect) {
setIsDisconnectModalOpen(true);
return;
@@ -62,6 +74,18 @@ function ConnectToXeroButton({policyID, session, shouldDisconnectIntegrationBefo
onCancel={() => setIsDisconnectModalOpen(false)}
/>
)}
+ {isRequire2FAModalOpen && (
+ {
+ setIsRequire2FAModalOpen(false);
+ Navigation.dismissModal();
+ Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.POLICY_ACCOUNTING.getRoute(policyID), getXeroSetupLink(policyID)));
+ }}
+ onCancel={() => setIsRequire2FAModalOpen(false)}
+ isVisible
+ description={translate('twoFactorAuth.twoFactorAuthIsRequiredDescription')}
+ />
+ )}
setWebViewOpen(false)}
fullscreen
diff --git a/src/components/ConnectToXeroButton/index.tsx b/src/components/ConnectToXeroButton/index.tsx
index fd8e7919bcb1..9d398060fe00 100644
--- a/src/components/ConnectToXeroButton/index.tsx
+++ b/src/components/ConnectToXeroButton/index.tsx
@@ -1,14 +1,19 @@
import React, {useState} from 'react';
+import {useOnyx} from 'react-native-onyx';
import AccountingConnectionConfirmationModal from '@components/AccountingConnectionConfirmationModal';
import Button from '@components/Button';
+import RequireTwoFactorAuthenticationModal from '@components/RequireTwoFactorAuthenticationModal';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {removePolicyConnection} from '@libs/actions/connections';
import {getXeroSetupLink} from '@libs/actions/connections/ConnectToXero';
+import Navigation from '@libs/Navigation/Navigation';
import * as Link from '@userActions/Link';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type {ConnectToXeroButtonProps} from './types';
function ConnectToXeroButton({policyID, shouldDisconnectIntegrationBeforeConnecting, integrationToDisconnect}: ConnectToXeroButtonProps) {
@@ -17,12 +22,21 @@ function ConnectToXeroButton({policyID, shouldDisconnectIntegrationBeforeConnect
const {environmentURL} = useEnvironment();
const {isOffline} = useNetwork();
+ const [account] = useOnyx(ONYXKEYS.ACCOUNT);
+ const is2FAEnabled = account?.requiresTwoFactorAuth;
+
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
+ const [isRequire2FAModalOpen, setIsRequire2FAModalOpen] = useState(false);
return (
<>
{
+ if (!is2FAEnabled) {
+ setIsRequire2FAModalOpen(true);
+ return;
+ }
+
if (shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect) {
setIsDisconnectModalOpen(true);
return;
@@ -45,6 +59,18 @@ function ConnectToXeroButton({policyID, shouldDisconnectIntegrationBeforeConnect
onCancel={() => setIsDisconnectModalOpen(false)}
/>
)}
+ {isRequire2FAModalOpen && (
+ {
+ setIsRequire2FAModalOpen(false);
+ Navigation.dismissModal();
+ Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.POLICY_ACCOUNTING.getRoute(policyID), getXeroSetupLink(policyID)));
+ }}
+ onCancel={() => setIsRequire2FAModalOpen(false)}
+ isVisible
+ description={translate('twoFactorAuth.twoFactorAuthIsRequiredDescription')}
+ />
+ )}
>
);
}
diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx
index 7e29da7763c5..3809b4f4f110 100644
--- a/src/components/ConnectionLayout.tsx
+++ b/src/components/ConnectionLayout.tsx
@@ -60,6 +60,15 @@ type ConnectionLayoutProps = {
/** Name of the current connection */
connectionName: ConnectionName;
+
+ /** Whether the screen should load for an empty connection */
+ shouldLoadForEmptyConnection?: boolean;
+
+ /** Handler for back button press */
+ onBackButtonPress?: () => void;
+
+ /** Whether or not to block user from accessing the page */
+ shouldBeBlocked?: boolean;
};
type ConnectionLayoutContentProps = Pick;
@@ -91,6 +100,9 @@ function ConnectionLayout({
shouldUseScrollView = true,
headerTitleAlreadyTranslated,
titleAlreadyTranslated,
+ shouldLoadForEmptyConnection = false,
+ onBackButtonPress = () => Navigation.goBack(),
+ shouldBeBlocked = false,
}: ConnectionLayoutProps) {
const {translate} = useLocalize();
@@ -110,12 +122,14 @@ function ConnectionLayout({
[title, titleStyle, children, titleAlreadyTranslated],
);
+ const shouldBlockByConnection = shouldLoadForEmptyConnection ? !isConnectionEmpty : isConnectionEmpty;
+
return (
Navigation.goBack()}
+ onBackButtonPress={onBackButtonPress}
/>
{shouldUseScrollView ? (
{renderSelectionContent}
diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx
index 62fdc85687e1..9ff04874c6da 100644
--- a/src/components/CountrySelector.tsx
+++ b/src/components/CountrySelector.tsx
@@ -2,6 +2,7 @@ import {useIsFocused} from '@react-navigation/native';
import React, {forwardRef, useEffect, useRef} from 'react';
import type {ForwardedRef} from 'react';
import type {View} from 'react-native';
+import useGeographicalStateAndCountryFromRoute from '@hooks/useGeographicalStateAndCountryFromRoute';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
@@ -31,6 +32,7 @@ type CountrySelectorProps = {
function CountrySelector({errorText = '', value: countryCode, onInputChange = () => {}, onBlur}: CountrySelectorProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const {country: countryFromUrl} = useGeographicalStateAndCountryFromRoute();
const title = countryCode ? translate(`allCountries.${countryCode}`) : '';
const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null;
@@ -38,18 +40,30 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange = ()
const didOpenContrySelector = useRef(false);
const isFocused = useIsFocused();
useEffect(() => {
- if (!isFocused || !didOpenContrySelector.current) {
+ // Check if the country selector was opened and no value was selected, triggering onBlur to display an error
+ if (isFocused && didOpenContrySelector.current) {
+ didOpenContrySelector.current = false;
+ if (!countryFromUrl) {
+ onBlur?.();
+ }
+ }
+
+ // If no country is selected from the URL, exit the effect early to avoid further processing.
+ if (!countryFromUrl) {
return;
}
- didOpenContrySelector.current = false;
- onBlur?.();
- }, [isFocused, onBlur]);
- useEffect(() => {
- // This will cause the form to revalidate and remove any error related to country name
- onInputChange(countryCode);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [countryCode]);
+ // If a country is selected, invoke `onInputChange` to update the form and clear any validation errors related to the country selection.
+ if (onInputChange) {
+ onInputChange(countryFromUrl);
+ }
+
+ // Clears the `country` parameter from the URL to ensure the component country is driven by the parent component rather than URL parameters.
+ // This helps prevent issues where the component might not update correctly if the country is controlled by both the parent and the URL.
+ Navigation.setParams({country: undefined});
+
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, [countryFromUrl, isFocused, onBlur]);
return (
{
// This will cause the form to revalidate and remove any error related to currency
onInputChange(currency);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [currency]);
return (
diff --git a/src/components/DecisionModal.tsx b/src/components/DecisionModal.tsx
index 065099867e14..a9bd0b204d79 100644
--- a/src/components/DecisionModal.tsx
+++ b/src/components/DecisionModal.tsx
@@ -21,7 +21,7 @@ type DecisionModalProps = {
secondOptionText: string;
/** onSubmit callback fired after clicking on first button */
- onFirstOptionSubmit: () => void;
+ onFirstOptionSubmit?: () => void;
/** onSubmit callback fired after clicking on second button */
onSecondOptionSubmit: () => void;
diff --git a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
index 430c00cf8804..b206d4bcf51d 100644
--- a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
+++ b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
@@ -64,7 +64,7 @@ function DisplayNamesTooltipItem({
if (!childRefs.current?.[index] || !el) {
return;
}
- // eslint-disable-next-line no-param-reassign
+ // eslint-disable-next-line react-compiler/react-compiler, no-param-reassign
childRefs.current[index] = el;
}}
style={[textStyles, styles.pre]}
diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx
index f4216dcc9f8a..a8d636db460b 100644
--- a/src/components/EReceiptThumbnail.tsx
+++ b/src/components/EReceiptThumbnail.tsx
@@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportUtils from '@libs/ReportUtils';
+import * as TripReservationUtils from '@libs/TripReservationUtils';
import colors from '@styles/theme/colors';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -56,7 +57,8 @@ const backgroundImages = {
function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction);
+ const {tripIcon, tripBGColor} = TripReservationUtils.getTripEReceiptData(transaction);
+ const colorCode = tripBGColor ?? (isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction));
const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]);
@@ -141,6 +143,14 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT
fill={primaryColor}
/>
) : null}
+ {tripIcon ? (
+
+ ) : null}
diff --git a/src/components/EmojiPicker/EmojiPicker.tsx b/src/components/EmojiPicker/EmojiPicker.tsx
index 2be8ea4aea7a..a1b496838529 100644
--- a/src/components/EmojiPicker/EmojiPicker.tsx
+++ b/src/components/EmojiPicker/EmojiPicker.tsx
@@ -13,6 +13,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import type {AnchorOrigin, EmojiPickerRef, EmojiPopoverAnchor, OnEmojiSelected, OnModalHideValue, OnWillShowPicker} from '@libs/actions/EmojiPickerAction';
import * as Browser from '@libs/Browser';
import calculateAnchorPosition from '@libs/calculateAnchorPosition';
+import * as Modal from '@userActions/Modal';
import CONST from '@src/CONST';
import EmojiPickerMenu from './EmojiPickerMenu';
@@ -87,19 +88,23 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef {
- onWillShow?.();
- setIsEmojiPickerVisible(true);
- setEmojiPopoverAnchorPosition({
- horizontal: value.horizontal,
- vertical: value.vertical,
+ Modal.close(() => {
+ onWillShow?.();
+ setIsEmojiPickerVisible(true);
+ setEmojiPopoverAnchorPosition({
+ horizontal: value.horizontal,
+ vertical: value.vertical,
+ });
+ emojiAnchorDimension.current = {
+ width: value.width,
+ height: value.height,
+ };
+ setEmojiPopoverAnchorOrigin(anchorOriginValue);
+ setActiveID(id);
});
- emojiAnchorDimension.current = {
- width: value.width,
- height: value.height,
- };
- setEmojiPopoverAnchorOrigin(anchorOriginValue);
- setActiveID(id);
});
};
@@ -115,6 +120,7 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef {
const emojiListRef = useAnimatedRef>();
const frequentlyUsedEmojis = useFrequentlyUsedEmojis();
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]);
const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]);
const headerRowIndices = useMemo(() => headerEmojis.map((headerEmoji) => headerEmoji.index), [headerEmojis]);
diff --git a/src/components/EmojiPicker/EmojiSkinToneList.tsx b/src/components/EmojiPicker/EmojiSkinToneList.tsx
index fb798f1c02c4..3a1832ac40a7 100644
--- a/src/components/EmojiPicker/EmojiSkinToneList.tsx
+++ b/src/components/EmojiPicker/EmojiSkinToneList.tsx
@@ -38,7 +38,7 @@ function EmojiSkinToneList() {
return;
}
toggleIsSkinToneListVisible();
- // eslint-disable-next-line react-hooks/exhaustive-deps -- only run when preferredSkinTone updates
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- only run when preferredSkinTone updates
}, [preferredSkinTone]);
const currentSkinTone = getSkinToneEmojiFromIndex(preferredSkinTone);
diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx
index 3781507b544c..a2996482be6a 100644
--- a/src/components/EmojiSuggestions.tsx
+++ b/src/components/EmojiSuggestions.tsx
@@ -34,6 +34,9 @@ type EmojiSuggestionsProps = {
/** Measures the parent container's position and dimensions. Also add cursor coordinates */
measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void;
+
+ /** Reset the emoji suggestions */
+ resetSuggestions: () => void;
};
/**
@@ -49,6 +52,7 @@ function EmojiSuggestions({
preferredSkinToneIndex,
highlightedEmojiIndex = 0,
measureParentContainerAndReportCursor = () => {},
+ resetSuggestions,
}: EmojiSuggestionsProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -93,6 +97,7 @@ function EmojiSuggestions({
isSuggestionPickerLarge={isEmojiPickerLarge}
accessibilityLabelExtractor={keyExtractor}
measureParentContainerAndReportCursor={measureParentContainerAndReportCursor}
+ resetSuggestions={resetSuggestions}
/>
);
}
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
new file mode 100644
index 000000000000..a8ad9d0f3154
--- /dev/null
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -0,0 +1,100 @@
+import type {VideoReadyForDisplayEvent} from 'expo-av';
+import React, {useMemo, useState} from 'react';
+import {View} from 'react-native';
+import Button from '@components/Button';
+import ImageSVG from '@components/ImageSVG';
+import Lottie from '@components/Lottie';
+import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
+import VideoPlayer from '@components/VideoPlayer';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import CONST from '@src/CONST';
+import type {EmptyStateComponentProps, VideoLoadedEventType} from './types';
+
+const VIDEO_ASPECT_RATIO = 400 / 225;
+
+function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, buttonText, buttonAction, title, subtitle, headerStyles, headerContentStyles}: EmptyStateComponentProps) {
+ const styles = useThemeStyles();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO);
+
+ const setAspectRatio = (event: VideoReadyForDisplayEvent | VideoLoadedEventType | undefined) => {
+ if (!event) {
+ return;
+ }
+
+ if ('naturalSize' in event) {
+ setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height);
+ } else {
+ setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight);
+ }
+ };
+
+ const HeaderComponent = useMemo(() => {
+ switch (headerMediaType) {
+ case CONST.EMPTY_STATE_MEDIA.VIDEO:
+ return (
+
+ );
+ case CONST.EMPTY_STATE_MEDIA.ANIMATION:
+ return (
+
+ );
+ case CONST.EMPTY_STATE_MEDIA.ILLUSTRATION:
+ return (
+
+ );
+ default:
+ return null;
+ }
+ }, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo]);
+
+ return (
+
+
+
+
+
+
+ {HeaderComponent}
+
+ {title}
+ {subtitle}
+ {!!buttonText && !!buttonAction && (
+
+ {buttonText}
+
+ )}
+
+
+
+
+ );
+}
+
+EmptyStateComponent.displayName = 'EmptyStateComponent';
+export default EmptyStateComponent;
diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts
new file mode 100644
index 000000000000..326b25542f42
--- /dev/null
+++ b/src/components/EmptyStateComponent/types.ts
@@ -0,0 +1,41 @@
+import type {ImageStyle} from 'expo-image';
+import type {StyleProp, ViewStyle} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import type DotLottieAnimation from '@components/LottieAnimations/types';
+import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
+import type TableRowSkeleton from '@components/Skeletons/TableRowSkeleton';
+import type CONST from '@src/CONST';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type ValidSkeletons = typeof SearchRowSkeleton | typeof TableRowSkeleton;
+type MediaTypes = ValueOf;
+
+type SharedProps = {
+ SkeletonComponent: ValidSkeletons;
+ title: string;
+ subtitle: string;
+ buttonText?: string;
+ buttonAction?: () => void;
+ headerStyles?: StyleProp;
+ headerMediaType: T;
+ headerContentStyles?: StyleProp;
+};
+
+type MediaType = SharedProps & {
+ headerMedia: HeaderMedia;
+};
+
+type VideoProps = MediaType;
+type IllustrationProps = MediaType;
+type AnimationProps = MediaType;
+
+type EmptyStateComponentProps = VideoProps | IllustrationProps | AnimationProps;
+
+type VideoLoadedEventType = {
+ srcElement: {
+ videoWidth: number;
+ videoHeight: number;
+ };
+};
+
+export type {EmptyStateComponentProps, VideoLoadedEventType};
diff --git a/src/components/ErrorMessageRow.tsx b/src/components/ErrorMessageRow.tsx
new file mode 100644
index 000000000000..2e6e41449274
--- /dev/null
+++ b/src/components/ErrorMessageRow.tsx
@@ -0,0 +1,43 @@
+import {mapValues} from 'lodash';
+import React from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
+import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import MessagesRow from './MessagesRow';
+
+type ErrorMessageRowProps = {
+ /** 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;
+
+ /** Whether we can dismiss the error message */
+ canDismissError?: boolean;
+};
+
+function ErrorMessageRow({errors, errorRowStyles, onClose, canDismissError = true}: ErrorMessageRowProps) {
+ // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages.
+ const errorEntries = Object.entries(errors ?? {});
+ const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError] => errorEntry[1] !== null);
+ const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => error);
+ const hasErrorMessages = !isEmptyObject(errorMessages);
+
+ return hasErrorMessages ? (
+
+ ) : null;
+}
+
+ErrorMessageRow.displayName = 'ErrorMessageRow';
+
+export default ErrorMessageRow;
diff --git a/src/components/FlatList/index.android.tsx b/src/components/FlatList/index.android.tsx
index 1246367d29e8..c8ce7ee10d6b 100644
--- a/src/components/FlatList/index.android.tsx
+++ b/src/components/FlatList/index.android.tsx
@@ -22,7 +22,7 @@ function CustomFlatList(props: FlatListProps, ref: ForwardedRef)
}
}, [scrollPosition?.offset, ref]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
const onMomentumScrollEnd = useCallback((event: NativeSyntheticEvent) => setScrollPosition({offset: event.nativeEvent.contentOffset.y}), []);
useFocusEffect(
diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx
index f54eddcbeb79..b45a8418d9a3 100644
--- a/src/components/FlatList/index.tsx
+++ b/src/components/FlatList/index.tsx
@@ -54,7 +54,6 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
return horizontal ? getScrollableNode(scrollRef.current)?.scrollLeft ?? 0 : getScrollableNode(scrollRef.current)?.scrollTop ?? 0;
}, [horizontal]);
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
const getContentView = useCallback(() => getScrollableNode(scrollRef.current)?.childNodes[0], []);
const scrollToOffset = useCallback(
@@ -151,10 +150,13 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
if (!isListRenderedRef.current) {
return;
}
- requestAnimationFrame(() => {
+ const animationFrame = requestAnimationFrame(() => {
prepareForMaintainVisibleContentPosition();
setupMutationObserver();
});
+ return () => {
+ cancelAnimationFrame(animationFrame);
+ };
}, [prepareForMaintainVisibleContentPosition, setupMutationObserver]);
const setMergedRef = useMergeRefs(scrollRef, ref);
@@ -177,6 +179,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
const mutationObserver = mutationObserverRef.current;
return () => {
mutationObserver?.disconnect();
+ mutationObserverRef.current = null;
};
}, []);
@@ -200,6 +203,10 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
ref={onRef}
onLayout={(e) => {
isListRenderedRef.current = true;
+ if (!mutationObserverRef.current) {
+ prepareForMaintainVisibleContentPosition();
+ setupMutationObserver();
+ }
props.onLayout?.(e);
}}
/>
diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx
index 93ffa52bc80b..7c2f5579332a 100644
--- a/src/components/FloatingActionButton.tsx
+++ b/src/components/FloatingActionButton.tsx
@@ -63,6 +63,7 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
const buttonRef = ref;
useEffect(() => {
+ // eslint-disable-next-line react-compiler/react-compiler
sharedValue.value = withTiming(isActive ? 1 : 0, {
duration: 340,
easing: Easing.inOut(Easing.ease),
diff --git a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
index be5da8c49a78..00dcedd32aa2 100644
--- a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
+++ b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
@@ -1,6 +1,7 @@
import FocusTrap from 'focus-trap-react';
import React from 'react';
import sharedTrapStack from '@components/FocusTrap/sharedTrapStack';
+import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import type FocusTrapForModalProps from './FocusTrapForModalProps';
function FocusTrapForModal({children, active}: FocusTrapForModalProps) {
@@ -12,6 +13,12 @@ function FocusTrapForModal({children, active}: FocusTrapForModalProps) {
clickOutsideDeactivates: true,
initialFocus: false,
fallbackFocus: document.body,
+ setReturnFocus: (element) => {
+ if (ReportActionComposeFocusManager.isFocused()) {
+ return false;
+ }
+ return element;
+ },
}}
>
{children}
diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
index 6a1409ab4a93..e7fe135c952c 100644
--- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
+++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
@@ -1,15 +1,14 @@
-import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native';
+import {useIsFocused, useRoute} from '@react-navigation/native';
import FocusTrap from 'focus-trap-react';
-import React, {useCallback, useMemo} from 'react';
+import React, {useMemo} from 'react';
import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS';
-import SCREENS_WITH_AUTOFOCUS from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS';
import sharedTrapStack from '@components/FocusTrap/sharedTrapStack';
import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS';
import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import CONST from '@src/CONST';
import type FocusTrapProps from './FocusTrapProps';
-let activeRouteName = '';
function FocusTrapForScreen({children}: FocusTrapProps) {
const isFocused = useIsFocused();
const route = useRoute();
@@ -33,12 +32,6 @@ function FocusTrapForScreen({children}: FocusTrapProps) {
return true;
}, [isFocused, isSmallScreenWidth, route.name]);
- useFocusEffect(
- useCallback(() => {
- activeRouteName = route.name;
- }, [route]),
- );
-
return (
{
- if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) {
+ delayInitialFocus: CONST.ANIMATED_TRANSITION,
+ initialFocus: (focusTrapContainers) => {
+ const isFocusedElementInsideContainer = focusTrapContainers?.some((container) => container.contains(document.activeElement));
+ if (isFocusedElementInsideContainer) {
return false;
}
return undefined;
},
setReturnFocus: (element) => {
- if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) {
+ if (document.activeElement && document.activeElement !== document.body) {
return false;
}
return element;
diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts
deleted file mode 100644
index 2a77b52e3116..000000000000
--- a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import {CENTRAL_PANE_WORKSPACE_SCREENS} from '@libs/Navigation/AppNavigator/Navigators/FullScreenNavigator';
-import SCREENS from '@src/SCREENS';
-
-const SCREENS_WITH_AUTOFOCUS: string[] = [
- ...Object.keys(CENTRAL_PANE_WORKSPACE_SCREENS),
- SCREENS.REPORT,
- SCREENS.REPORT_DESCRIPTION_ROOT,
- SCREENS.PRIVATE_NOTES.EDIT,
- SCREENS.SETTINGS.PROFILE.STATUS,
- SCREENS.SETTINGS.PROFILE.PRONOUNS,
- SCREENS.NEW_TASK.DETAILS,
- SCREENS.MONEY_REQUEST.CREATE,
-];
-
-export default SCREENS_WITH_AUTOFOCUS;
diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
index be772c6ae10c..551a30cba46e 100644
--- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
+++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
@@ -31,6 +31,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [
SCREENS.WORKSPACE.TAGS,
SCREENS.WORKSPACE.TAXES,
SCREENS.WORKSPACE.REPORT_FIELDS,
+ SCREENS.WORKSPACE.EXPENSIFY_CARD,
SCREENS.WORKSPACE.DISTANCE_RATES,
SCREENS.SEARCH.CENTRAL_PANE,
SCREENS.SETTINGS.TROUBLESHOOT,
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index 9df94e4c6114..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(
@@ -176,7 +179,7 @@ function FormProvider(
onValidate(trimmedStringValues, !hasServerError);
// Only run when locales change
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [preferredLocale]);
/** @param inputID - The inputID of the input being touched */
@@ -239,6 +242,7 @@ function FormProvider(
inputRefs.current[inputID] = newRef;
}
if (inputProps.value !== undefined) {
+ // eslint-disable-next-line react-compiler/react-compiler
inputValues[inputID] = inputProps.value;
} else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) {
inputValues[inputID] = draftValues[inputID];
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/SafariFormWrapper.tsx b/src/components/Form/SafariFormWrapper.tsx
new file mode 100644
index 000000000000..8ad411e547be
--- /dev/null
+++ b/src/components/Form/SafariFormWrapper.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import {isSafari} from '@libs/Browser';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+
+type SafariFormWrapperProps = ChildrenProps;
+
+/**
+ * If we used any without ;
+ }
+
+ return children;
+}
+
+export default SafariFormWrapper;
diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts
index 6245fdcf7b49..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,14 +52,19 @@ type ValidInputs =
| typeof AmountPicker
| typeof TextPicker
| typeof AddPlaidBankAccount
- | typeof EmojiPickerButtonDropdown;
+ | typeof EmojiPickerButtonDropdown
+ | typeof NetSuiteCustomListPicker
+ | typeof NetSuiteCustomFieldMappingPicker
+ | typeof NetSuiteMenuWithTopDescriptionForm;
-type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country';
+type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues';
type ValueTypeMap = {
string: string;
boolean: boolean;
date: Date;
country: Country | '';
+ reportFields: string[];
+ disabledListValues: boolean[];
};
type FormValue = ValueOf;
@@ -124,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/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
index d95fc9e11a31..bab66dfab911 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
@@ -42,6 +42,11 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
mixedUAStyles: {...styles.colorMuted, ...styles.mb0},
contentModel: HTMLContentModel.block,
}),
+ 'muted-text-label': HTMLElementModel.fromCustomModel({
+ tagName: 'muted-text-label',
+ mixedUAStyles: {...styles.mutedNormalTextLabel, ...styles.mb0},
+ contentModel: HTMLContentModel.block,
+ }),
comment: HTMLElementModel.fromCustomModel({
tagName: 'comment',
mixedUAStyles: {whiteSpace: 'pre'},
@@ -83,7 +88,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
contentModel: HTMLContentModel.block,
}),
}),
- [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4],
+ [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4, styles.mutedNormalTextLabel],
);
/* eslint-enable @typescript-eslint/naming-convention */
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
index 67bbc7986bc7..1e73cce1630f 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
@@ -78,7 +78,7 @@ function ImageRenderer({tnode}: ImageRendererProps) {
thumbnailImageComponent
) : (
- {({anchor, report, action, checkIfContextMenuActive}) => (
+ {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => (
{({reportID, accountID, type}) => (
showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ onLongPress={(event) =>
+ showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs))
+ }
shouldUseHapticsOnLongPress
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
index e5481c5d9094..ffab2434c83c 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
@@ -83,10 +83,12 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona
return (
- {({anchor, report, action, checkIfContextMenuActive}) => (
+ {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => (
showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ onLongPress={(event) =>
+ showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs))
+ }
onPress={(event) => {
event.preventDefault();
Navigation.navigate(navigationRoute);
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx
index 4d1e58a42830..14666798e8c7 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx
@@ -34,12 +34,14 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d
return (
- {({anchor, report, action, checkIfContextMenuActive}) => (
+ {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => (
{})}
onPressIn={onPressIn}
onPressOut={onPressOut}
- onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ onLongPress={(event) =>
+ showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs))
+ }
shouldUseHapticsOnLongPress
role={CONST.ROLE.PRESENTATION}
accessibilityLabel={translate('accessibilityHints.prestyledText')}
diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx
index abd48d432953..fd3d4f3d19e8 100644
--- a/src/components/Hoverable/ActiveHoverable.tsx
+++ b/src/components/Hoverable/ActiveHoverable.tsx
@@ -48,7 +48,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez
return;
}
- const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => {
+ const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling: boolean) => {
isScrollingRef.current = scrolling;
if (!isScrollingRef.current) {
setIsHovered(isHoveredRef.current);
@@ -102,7 +102,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez
const child = useMemo(() => getReturnValue(children, !isScrollingRef.current && isHovered), [children, isHovered]);
- const {onMouseEnter, onMouseLeave, onMouseMove, onBlur}: OnMouseEvents = child.props;
+ const {onMouseEnter, onMouseLeave, onMouseMove, onBlur} = child.props as OnMouseEvents;
const hoverAndForwardOnMouseEnter = useCallback(
(e: MouseEvent) => {
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..1ea8e62ab17e
--- /dev/null
+++ b/src/components/HybridAppMiddleware/index.ios.tsx
@@ -0,0 +1,161 @@
+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 type {OnyxEntry} 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';
+import type {TryNewDot} from '@src/types/onyx';
+
+type HybridAppMiddlewareProps = {
+ authenticated: boolean;
+ children: React.ReactNode;
+};
+
+const onboardingStatusSelector = (tryNewDot: OnyxEntry) => {
+ let completedHybridAppOnboarding = tryNewDot?.classicRedirect?.completedHybridAppOnboarding;
+
+ if (typeof completedHybridAppOnboarding === 'string') {
+ completedHybridAppOnboarding = completedHybridAppOnboarding === 'true';
+ }
+
+ return completedHybridAppOnboarding;
+};
+
+/*
+ * 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});
+ const [completedHybridAppOnboarding] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {selector: onboardingStatusSelector});
+
+ /**
+ * This useEffect tracks changes of `nvp_tryNewDot` value.
+ * We propagate it from OldDot to NewDot with native method due to limitations of old app.
+ */
+ useEffect(() => {
+ if (completedHybridAppOnboarding === undefined) {
+ return;
+ }
+
+ if (!NativeModules.HybridAppModule) {
+ Log.hmmm(`[HybridApp] Onboarding status has changed, but the HybridAppModule is not defined`);
+ return;
+ }
+
+ Log.info(`[HybridApp] Onboarding status has changed. Propagating new value to OldDot`, true, {completedHybridAppOnboarding});
+ NativeModules.HybridAppModule.completeOnboarding(completedHybridAppOnboarding);
+ }, [completedHybridAppOnboarding]);
+
+ // 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..5a8d8d6dfebe
--- /dev/null
+++ b/src/components/HybridAppMiddleware/index.tsx
@@ -0,0 +1,133 @@
+import type React from 'react';
+import {useContext, useEffect, useState} from 'react';
+import {NativeModules} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type {OnyxEntry} 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';
+import type {TryNewDot} from '@src/types/onyx';
+
+type HybridAppMiddlewareProps = {
+ authenticated: boolean;
+ children: React.ReactNode;
+};
+
+const onboardingStatusSelector = (tryNewDot: OnyxEntry) => {
+ let completedHybridAppOnboarding = tryNewDot?.classicRedirect?.completedHybridAppOnboarding;
+
+ if (typeof completedHybridAppOnboarding === 'string') {
+ completedHybridAppOnboarding = completedHybridAppOnboarding === 'true';
+ }
+
+ return completedHybridAppOnboarding;
+};
+
+/*
+ * 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});
+ const [completedHybridAppOnboarding] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {selector: onboardingStatusSelector});
+
+ /**
+ * This useEffect tracks changes of `nvp_tryNewDot` value.
+ * We propagate it from OldDot to NewDot with native method due to limitations of old app.
+ */
+ useEffect(() => {
+ if (completedHybridAppOnboarding === undefined || !NativeModules.HybridAppModule) {
+ return;
+ }
+
+ Log.info(`[HybridApp] Onboarding status has changed. Propagating new value to OldDot`, true, {completedHybridAppOnboarding});
+ NativeModules.HybridAppModule.completeOnboarding(completedHybridAppOnboarding);
+ }, [completedHybridAppOnboarding]);
+
+ // 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/IFrame.tsx b/src/components/IFrame.tsx
index 05da3a1edb9c..f492df0f3866 100644
--- a/src/components/IFrame.tsx
+++ b/src/components/IFrame.tsx
@@ -17,7 +17,7 @@ function getNewDotURL(url: string): string {
let params: Record;
try {
- params = JSON.parse(paramString);
+ params = JSON.parse(paramString) as Record;
} catch {
params = {};
}
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index a0d7a5cb8883..487df5594212 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -42,6 +42,7 @@ import ChatBubbles from '@assets/images/chatbubbles.svg';
import CheckCircle from '@assets/images/check-circle.svg';
import CheckmarkCircle from '@assets/images/checkmark-circle.svg';
import Checkmark from '@assets/images/checkmark.svg';
+import CircularArrowBackwards from '@assets/images/circular-arrow-backwards.svg';
import Close from '@assets/images/close.svg';
import ClosedSign from '@assets/images/closed-sign.svg';
import Coins from '@assets/images/coins.svg';
@@ -201,6 +202,7 @@ export {
Wrench,
BackArrow,
Bank,
+ CircularArrowBackwards,
Bill,
Bell,
BellSlash,
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index e699badc43ec..6499e8eceb6e 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -1,3 +1,5 @@
+import ExpensifyCardIllustration from '@assets/images/expensifyCard/cardIllustration.svg';
+import LaptopwithSecondScreenandHourglass from '@assets/images/LaptopwithSecondScreenandHourglass.svg';
import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg';
import BankArrowPink from '@assets/images/product-illustrations/bank-arrow--pink.svg';
import BankMouseGreen from '@assets/images/product-illustrations/bank-mouse--green.svg';
@@ -7,6 +9,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';
@@ -51,6 +54,7 @@ import ConciergeNew from '@assets/images/simple-illustrations/simple-illustratio
import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg';
import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg';
import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg';
+import EmptyState from '@assets/images/simple-illustrations/simple-illustration__empty-state.svg';
import FolderOpen from '@assets/images/simple-illustrations/simple-illustration__folder-open.svg';
import Gears from '@assets/images/simple-illustrations/simple-illustration__gears.svg';
import HandCard from '@assets/images/simple-illustrations/simple-illustration__handcard.svg';
@@ -76,6 +80,7 @@ import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__
import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg';
import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg';
import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg';
+import ReceiptLocationMarker from '@assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg';
import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg';
import ReceiptUpload from '@assets/images/simple-illustrations/simple-illustration__receiptupload.svg';
import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg';
@@ -88,9 +93,11 @@ import SubscriptionPPU from '@assets/images/simple-illustrations/simple-illustra
import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg';
import TeachersUnite from '@assets/images/simple-illustrations/simple-illustration__teachers-unite.svg';
import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg';
+import Tire from '@assets/images/simple-illustrations/simple-illustration__tire.svg';
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';
@@ -140,6 +147,7 @@ export {
PinkBill,
CreditCardsNew,
InvoiceBlue,
+ LaptopwithSecondScreenandHourglass,
LockOpen,
Luggage,
MoneyIntoWallet,
@@ -176,6 +184,7 @@ export {
Binoculars,
CompanyCard,
ReceiptUpload,
+ ExpensifyCardIllustration,
SplitBill,
PiggyBank,
Accounting,
@@ -184,6 +193,7 @@ export {
Pencil,
Tag,
CarIce,
+ ReceiptLocationMarker,
Lightbulb,
EmptyStateTravel,
SubscriptionAnnual,
@@ -194,4 +204,8 @@ export {
CheckmarkCircle,
CreditCardEyes,
LockClosedOrange,
+ EmptyState,
+ FolderWithPapers,
+ VirtualCard,
+ Tire,
};
diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx
index f3cbc332c995..5fe1ba306400 100644
--- a/src/components/Image/index.tsx
+++ b/src/components/Image/index.tsx
@@ -58,7 +58,7 @@ function Image({source: propsSource, isAuthTokenRequired = false, session, onLoa
}
return propsSource;
// The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034.
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [propsSource, isAuthTokenRequired]);
/**
diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx
index c74d9bd5aa52..e12be53d01ae 100644
--- a/src/components/ImageView/index.tsx
+++ b/src/components/ImageView/index.tsx
@@ -7,6 +7,7 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import Image from '@components/Image';
import RESIZE_MODES from '@components/Image/resizeModes';
import type {ImageOnLoadEvent} from '@components/Image/types';
+import Lightbox from '@components/Lightbox';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -200,25 +201,11 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
if (canUseTouchScreen) {
return (
-
- 1 ? RESIZE_MODES.center : RESIZE_MODES.contain}
- onLoadStart={imageLoadingStart}
- onLoad={imageLoad}
- onError={onError}
- />
- {((isLoading && (!isOffline || isLocalFile)) || (!isLoading && zoomScale === 0)) && }
- {isLoading && !isLocalFile && }
-
+
);
}
return (
diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx
index 8830681bc55f..0d269d1ca593 100644
--- a/src/components/Indicator.tsx
+++ b/src/components/Indicator.tsx
@@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as PolicyUtils from '@libs/PolicyUtils';
+import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import * as UserUtils from '@libs/UserUtils';
import * as PaymentMethods from '@userActions/PaymentMethods';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -54,13 +55,14 @@ function Indicator({reimbursementAccount, policies, bankAccountList, fundList, u
() => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError),
() => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError),
() => Object.values(cleanPolicies).some(PolicyUtils.hasEmployeeListError),
+ () => SubscriptionUtils.hasSubscriptionRedDotError(),
() => Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
() => !!loginList && UserUtils.hasLoginListError(loginList),
// Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
() => Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID,
];
- const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList)];
+ const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList), () => SubscriptionUtils.hasSubscriptionGreenDotInfo()];
const shouldShowErrorIndicator = errorCheckingMethods.some((errorCheckingMethod) => errorCheckingMethod());
const shouldShowInfoIndicator = !shouldShowErrorIndicator && infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod());
diff --git a/src/components/InteractiveStepSubHeader.tsx b/src/components/InteractiveStepSubHeader.tsx
index 20b3f6bc79a4..d8899a317df5 100644
--- a/src/components/InteractiveStepSubHeader.tsx
+++ b/src/components/InteractiveStepSubHeader.tsx
@@ -25,6 +25,9 @@ type InteractiveStepSubHeaderProps = {
type InteractiveStepSubHeaderHandle = {
/** Move to the next step */
moveNext: () => void;
+
+ /** Move to the previous step */
+ movePrevious: () => void;
};
const MIN_AMOUNT_FOR_EXPANDING = 3;
@@ -45,6 +48,9 @@ function InteractiveStepSubHeader({stepNames, startStepIndex = 0, onStepSelected
moveNext: () => {
setCurrentStep((actualStep) => actualStep + 1);
},
+ movePrevious: () => {
+ setCurrentStep((actualStep) => actualStep - 1);
+ },
}),
[],
);
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index c7797a37fd12..431a12d00106 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -3,6 +3,7 @@ import React, {useCallback, useRef, useState} from 'react';
import type {GestureResponderEvent, ViewStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
+import Badge from '@components/Badge';
import DisplayNames from '@components/DisplayNames';
import Hoverable from '@components/Hoverable';
import Icon from '@components/Icon';
@@ -20,11 +21,12 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import DateUtils from '@libs/DateUtils';
import DomUtils from '@libs/DomUtils';
-import {parseHtmlToText} from '@libs/OnyxAwareParser';
import * as OptionsListUtils from '@libs/OptionsListUtils';
+import Parser from '@libs/Parser';
import Performance from '@libs/Performance';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportUtils from '@libs/ReportUtils';
+import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -227,6 +229,13 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
ReportUtils.isSystemChat(report)
}
/>
+ {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && (
+
+ )}
{isStatusVisible && (
{}, opti
numberOfLines={1}
accessibilityLabel={translate('accessibilityHints.lastChatMessagePreview')}
>
- {parseHtmlToText(optionItem.alternateText)}
+ {Parser.htmlToText(optionItem.alternateText)}
) : null}
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index 8d61058ed5be..bb5fdb580aa7 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -47,6 +47,7 @@ function OptionRowLHNData({
policy,
parentReportAction,
hasViolations: !!shouldDisplayViolations,
+ transactionViolations,
});
if (deepEqual(item, optionItemRef.current)) {
return optionItemRef.current;
@@ -57,7 +58,7 @@ function OptionRowLHNData({
return item;
// Listen parentReportAction to update title of thread report when parentReportAction changed
// Listen to transaction to update title of transaction report when transaction changed
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [
fullReport,
lastReportActionTransaction,
diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx
index ea10e104a59d..c5a77f9d5ec4 100644
--- a/src/components/Lightbox/index.tsx
+++ b/src/components/Lightbox/index.tsx
@@ -78,7 +78,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
};
}
- const foundPage = attachmentCarouselPagerContext.pagerItems.findIndex((item) => item.source === uri);
+ const foundPage = attachmentCarouselPagerContext.pagerItems.findIndex((item) => item.source === uri || item.previewSource === uri);
return {
...attachmentCarouselPagerContext,
isUsedInCarousel: !!attachmentCarouselPagerContext.pagerRef,
diff --git a/src/components/LocationPermissionModal/index.android.tsx b/src/components/LocationPermissionModal/index.android.tsx
new file mode 100644
index 000000000000..811537e00e67
--- /dev/null
+++ b/src/components/LocationPermissionModal/index.android.tsx
@@ -0,0 +1,90 @@
+import React, {useEffect, useState} from 'react';
+import {Linking} from 'react-native';
+import {RESULTS} from 'react-native-permissions';
+import ConfirmModal from '@components/ConfirmModal';
+import * as Illustrations from '@components/Icon/Illustrations';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getLocationPermission, requestLocationPermission} from '@pages/iou/request/step/IOURequestStepScan/LocationPermission';
+import type {LocationPermissionModalProps} from './types';
+
+function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDeny, onGrant}: LocationPermissionModalProps) {
+ const [hasError, setHasError] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ useEffect(() => {
+ if (!startPermissionFlow) {
+ return;
+ }
+
+ getLocationPermission().then((status) => {
+ if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) {
+ return onGrant();
+ }
+
+ setShowModal(true);
+ setHasError(status === RESULTS.BLOCKED);
+ });
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes
+ }, [startPermissionFlow]);
+
+ const handledBlockedPermission = (cb: () => void) => () => {
+ if (hasError && Linking.openSettings) {
+ Linking.openSettings();
+ setShowModal(false);
+ setHasError(false);
+ resetPermissionFlow();
+ return;
+ }
+ cb();
+ };
+
+ const grantLocationPermission = handledBlockedPermission(() => {
+ requestLocationPermission().then((status) => {
+ if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) {
+ onGrant();
+ } else if (status === RESULTS.BLOCKED) {
+ setHasError(true);
+ return;
+ } else {
+ onDeny(status);
+ }
+ setShowModal(false);
+ setHasError(false);
+ });
+ });
+
+ const skipLocationPermission = () => {
+ onDeny(RESULTS.DENIED);
+ setShowModal(false);
+ setHasError(false);
+ };
+
+ return (
+
+ );
+}
+
+LocationPermissionModal.displayName = 'LocationPermissionModal';
+
+export default LocationPermissionModal;
diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx
new file mode 100644
index 000000000000..2bc4a7393822
--- /dev/null
+++ b/src/components/LocationPermissionModal/index.tsx
@@ -0,0 +1,90 @@
+import React, {useEffect, useState} from 'react';
+import {Linking} from 'react-native';
+import {RESULTS} from 'react-native-permissions';
+import ConfirmModal from '@components/ConfirmModal';
+import * as Illustrations from '@components/Icon/Illustrations';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getLocationPermission, requestLocationPermission} from '@pages/iou/request/step/IOURequestStepScan/LocationPermission';
+import type {LocationPermissionModalProps} from './types';
+
+function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDeny, onGrant}: LocationPermissionModalProps) {
+ const [hasError, setHasError] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ useEffect(() => {
+ if (!startPermissionFlow) {
+ return;
+ }
+
+ getLocationPermission().then((status) => {
+ if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) {
+ return onGrant();
+ }
+
+ setShowModal(true);
+ setHasError(status === RESULTS.BLOCKED);
+ });
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes
+ }, [startPermissionFlow]);
+
+ const handledBlockedPermission = (cb: () => void) => () => {
+ if (hasError && Linking.openSettings) {
+ Linking.openSettings();
+ setShowModal(false);
+ setHasError(false);
+ resetPermissionFlow();
+ return;
+ }
+ cb();
+ };
+
+ const grantLocationPermission = handledBlockedPermission(() => {
+ requestLocationPermission()
+ .then((status) => {
+ if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) {
+ onGrant();
+ } else {
+ onDeny(status);
+ }
+ })
+ .finally(() => {
+ setShowModal(false);
+ setHasError(false);
+ });
+ });
+
+ const skipLocationPermission = () => {
+ onDeny(RESULTS.DENIED);
+ setShowModal(false);
+ setHasError(false);
+ };
+
+ return (
+
+ );
+}
+
+LocationPermissionModal.displayName = 'LocationPermissionModal';
+
+export default LocationPermissionModal;
diff --git a/src/components/LocationPermissionModal/types.ts b/src/components/LocationPermissionModal/types.ts
new file mode 100644
index 000000000000..ec603bfdb8c1
--- /dev/null
+++ b/src/components/LocationPermissionModal/types.ts
@@ -0,0 +1,19 @@
+import type {PermissionStatus} from 'react-native-permissions';
+
+type LocationPermissionModalProps = {
+ /** A callback to call when the permission has been granted */
+ onGrant: () => void;
+
+ /** A callback to call when the permission has been denied */
+ onDeny: (permission: PermissionStatus) => void;
+
+ /** Should start the permission flow? */
+ startPermissionFlow: boolean;
+
+ /** Reset the permission flow */
+ resetPermissionFlow: () => void;
+};
+
+export default {};
+
+export type {LocationPermissionModalProps};
diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx
index 6395a715f339..a9b223a87a54 100644
--- a/src/components/Lottie/index.tsx
+++ b/src/components/Lottie/index.tsx
@@ -1,7 +1,7 @@
-import type {LottieViewProps} from 'lottie-react-native';
+import type {AnimationObject, LottieViewProps} from 'lottie-react-native';
import LottieView from 'lottie-react-native';
import type {ForwardedRef} from 'react';
-import React, {forwardRef} from 'react';
+import React, {forwardRef, useEffect, useState} from 'react';
import {View} from 'react-native';
import type DotLottieAnimation from '@components/LottieAnimations/types';
import useAppState from '@hooks/useAppState';
@@ -19,6 +19,12 @@ function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef setIsError(false)});
+ const [animationFile, setAnimationFile] = useState();
+
+ useEffect(() => {
+ setAnimationFile(source.file);
+ }, [setAnimationFile, source.file]);
+
const aspectRatioStyle = styles.aspectRatioLottie(source);
// If the image fails to load or app is in background state, we'll just render an empty view
@@ -28,17 +34,17 @@ function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef ;
}
- return (
+ return animationFile ? (
setIsError(true)}
/>
- );
+ ) : null;
}
Lottie.displayName = 'Lottie';
diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx
index 477ce02cd740..afbc9cd56e28 100644
--- a/src/components/LottieAnimations/index.tsx
+++ b/src/components/LottieAnimations/index.tsx
@@ -1,80 +1,81 @@
+import type {LottieViewProps} from 'lottie-react-native';
import colors from '@styles/theme/colors';
import variables from '@styles/variables';
import type DotLottieAnimation from './types';
const DotLottieAnimations = {
Abracadabra: {
- file: require('@assets/animations/Abracadabra.lottie'),
+ file: require('@assets/animations/Abracadabra.lottie'),
w: 375,
h: 400,
},
FastMoney: {
- file: require('@assets/animations/FastMoney.lottie'),
+ file: require('@assets/animations/FastMoney.lottie'),
w: 375,
h: 240,
},
Fireworks: {
- file: require('@assets/animations/Fireworks.lottie'),
+ file: require('@assets/animations/Fireworks.lottie'),
w: 360,
h: 360,
},
Hands: {
- file: require('@assets/animations/Hands.lottie'),
+ file: require('@assets/animations/Hands.lottie'),
w: 375,
h: 375,
},
PreferencesDJ: {
- file: require('@assets/animations/PreferencesDJ.lottie'),
+ file: require('@assets/animations/PreferencesDJ.lottie'),
w: 375,
h: 240,
backgroundColor: colors.blue500,
},
ReviewingBankInfo: {
- file: require('@assets/animations/ReviewingBankInfo.lottie'),
+ file: require('@assets/animations/ReviewingBankInfo.lottie'),
w: 280,
h: 280,
},
WorkspacePlanet: {
- file: require('@assets/animations/WorkspacePlanet.lottie'),
+ file: require('@assets/animations/WorkspacePlanet.lottie'),
w: 375,
h: 240,
backgroundColor: colors.pink800,
},
SaveTheWorld: {
- file: require('@assets/animations/SaveTheWorld.lottie'),
+ file: require('@assets/animations/SaveTheWorld.lottie'),
w: 375,
h: 240,
},
Safe: {
- file: require('@assets/animations/Safe.lottie'),
+ file: require('@assets/animations/Safe.lottie'),
w: 625,
h: 400,
backgroundColor: colors.ice500,
},
Magician: {
- file: require('@assets/animations/Magician.lottie'),
+ file: require('@assets/animations/Magician.lottie'),
w: 853,
h: 480,
},
Update: {
- file: require('@assets/animations/Update.lottie'),
+ file: require('@assets/animations/Update.lottie'),
w: variables.updateAnimationW,
h: variables.updateAnimationH,
},
Coin: {
- file: require('@assets/animations/Coin.lottie'),
+ file: require('@assets/animations/Coin.lottie'),
w: 375,
h: 240,
backgroundColor: colors.yellow600,
},
Desk: {
- file: require('@assets/animations/Desk.lottie'),
+ file: require('@assets/animations/Desk.lottie'),
w: 200,
h: 120,
backgroundColor: colors.blue700,
},
Plane: {
- file: require('@assets/animations/Plane.lottie'),
+ file: require('@assets/animations/Plane.lottie'),
w: 180,
h: 200,
},
diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx
index 6239243cb5ab..2fae3cc89597 100644
--- a/src/components/MagicCodeInput.tsx
+++ b/src/components/MagicCodeInput.tsx
@@ -192,7 +192,7 @@ function MagicCodeInput(
// We have not added:
// + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code.
// + the onFulfill as the dependency because onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed.
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [value, shouldSubmitOnComplete]);
/**
@@ -298,7 +298,7 @@ function MagicCodeInput(
// Fill the array with empty characters if there are no inputs.
if (focusedIndex === 0 && !hasInputs) {
- numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR);
+ numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR);
// Deletes the value of the previous input and focuses on it.
} else if (focusedIndex && focusedIndex !== 0) {
@@ -353,7 +353,7 @@ function MagicCodeInput(
// We have not added:
// + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes.
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [lastPressedDigit, isDisableKeyboard]);
return (
diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx
index 283f7c396edb..974f58636977 100644
--- a/src/components/MapView/MapView.tsx
+++ b/src/components/MapView/MapView.tsx
@@ -37,6 +37,7 @@ const MapView = forwardRef(
const currentPosition = userLocation ?? initialLocation;
const [userInteractedWithMap, setUserInteractedWithMap] = useState(false);
const shouldInitializeCurrentPosition = useRef(true);
+ const [isAccessTokenSet, setIsAccessTokenSet] = useState(false);
// Determines if map can be panned to user's detected
// location without bothering the user. It will return
@@ -138,7 +139,12 @@ const MapView = forwardRef(
}, [navigation]);
useEffect(() => {
- setAccessToken(accessToken);
+ setAccessToken(accessToken).then((token) => {
+ if (!token) {
+ return;
+ }
+ setIsAccessTokenSet(true);
+ });
}, [accessToken]);
const setMapIdle = (e: MapState) => {
@@ -151,7 +157,8 @@ const MapView = forwardRef(
}
};
const centerMap = useCallback(() => {
- if (directionCoordinates && directionCoordinates.length > 1) {
+ const waypointCoordinates = waypoints?.map((waypoint) => waypoint.coordinate) ?? [];
+ if (waypointCoordinates.length > 1 || (directionCoordinates ?? []).length > 1) {
const {southWest, northEast} = utils.getBounds(waypoints?.map((waypoint) => waypoint.coordinate) ?? [], directionCoordinates);
cameraRef.current?.fitBounds(southWest, northEast, mapPadding, CONST.MAPBOX.ANIMATION_DURATION_ON_CENTER_ME);
return;
@@ -198,7 +205,7 @@ const MapView = forwardRef(
const initCenterCoordinate = useMemo(() => (interactive ? centerCoordinate : undefined), [interactive, centerCoordinate]);
const initBounds = useMemo(() => (interactive ? undefined : waypointsBounds), [interactive, waypointsBounds]);
- return !isOffline && !!accessToken && !!defaultSettings ? (
+ return !isOffline && isAccessTokenSet && !!defaultSettings ? (
(
pitchEnabled={pitchEnabled}
attributionPosition={{...styles.r2, ...styles.b2}}
scaleBarEnabled={false}
+ // We use scaleBarPosition with top: -32 to hide the scale bar on iOS because scaleBarEnabled={false} not work on iOS
+ scaleBarPosition={{...styles.tn8, left: 0}}
+ compassEnabled
+ compassPosition={{...styles.l2, ...styles.t5}}
logoPosition={{...styles.l2, ...styles.b2}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...responder.panHandlers}
diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx
index 01d7134a96da..618dd5b24cf5 100644
--- a/src/components/MapView/MapView.website.tsx
+++ b/src/components/MapView/MapView.website.tsx
@@ -157,7 +157,7 @@ const MapView = forwardRef(
resetBoundaries();
setShouldResetBoundaries(false);
- // eslint-disable-next-line react-hooks/exhaustive-deps -- this effect only needs to run when the boundaries reset is forced
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this effect only needs to run when the boundaries reset is forced
}, [shouldResetBoundaries]);
useEffect(() => {
@@ -194,7 +194,8 @@ const MapView = forwardRef(
if (!mapRef) {
return;
}
- if (directionCoordinates && directionCoordinates.length > 1) {
+ const waypointCoordinates = waypoints?.map((waypoint) => waypoint.coordinate) ?? [];
+ if (waypointCoordinates.length > 1 || (directionCoordinates ?? []).length > 1) {
const {northEast, southWest} = utils.getBounds(waypoints?.map((waypoint) => waypoint.coordinate) ?? [], directionCoordinates);
const map = mapRef?.getMap();
map?.fitBounds([southWest, northEast], {padding: mapPadding, animate: true, duration: CONST.MAPBOX.ANIMATION_DURATION_ON_CENTER_ME});
diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx
index 1142a90c87d1..bdc19316d491 100644
--- a/src/components/MentionSuggestions.tsx
+++ b/src/components/MentionSuggestions.tsx
@@ -55,6 +55,9 @@ type MentionSuggestionsProps = {
/** Measures the parent container's position and dimensions. Also add cursor coordinates */
measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void;
+
+ /** Reset the emoji suggestions */
+ resetSuggestions: () => void;
};
/**
@@ -62,7 +65,15 @@ type MentionSuggestionsProps = {
*/
const keyExtractor = (item: Mention) => item.alternateText;
-function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSelect, isMentionPickerLarge, measureParentContainerAndReportCursor = () => {}}: MentionSuggestionsProps) {
+function MentionSuggestions({
+ prefix,
+ mentions,
+ highlightedMentionIndex = 0,
+ onSelect,
+ isMentionPickerLarge,
+ measureParentContainerAndReportCursor = () => {},
+ resetSuggestions,
+}: MentionSuggestionsProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -149,6 +160,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe
isSuggestionPickerLarge={isMentionPickerLarge}
accessibilityLabelExtractor={keyExtractor}
measureParentContainerAndReportCursor={measureParentContainerAndReportCursor}
+ resetSuggestions={resetSuggestions}
/>
);
}
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 8d5fe98bb99d..473806aac3af 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -1,4 +1,3 @@
-import {ExpensiMark} from 'expensify-common';
import type {ImageContentFit} from 'expo-image';
import type {ReactElement, ReactNode} from 'react';
import React, {forwardRef, useContext, useMemo} from 'react';
@@ -14,6 +13,7 @@ import ControlSelection from '@libs/ControlSelection';
import convertToLTR from '@libs/convertToLTR';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import getButtonState from '@libs/getButtonState';
+import Parser from '@libs/Parser';
import type {AvatarSource} from '@libs/UserUtils';
import variables from '@styles/variables';
import * as Session from '@userActions/Session';
@@ -254,12 +254,18 @@ type MenuItemBaseProps = {
/** Whether should render title as HTML or as Text */
shouldParseTitle?: boolean;
+ /** Whether should render helper text as HTML or as Text */
+ shouldParseHelperText?: boolean;
+
/** Should check anonymous user in onPress function */
shouldCheckActionAllowedOnPress?: boolean;
/** Text to display under the main item */
furtherDetails?: string;
+ /** Render custom content under the main item */
+ furtherDetailsComponent?: ReactElement;
+
/** The function that should be called when this component is LongPressed or right-clicked. */
onSecondaryInteraction?: (event: GestureResponderEvent | MouseEvent) => void;
@@ -335,6 +341,7 @@ function MenuItem(
iconRight = Expensicons.ArrowRight,
furtherDetailsIcon,
furtherDetails,
+ furtherDetailsComponent,
description,
helperText,
helperTextStyle,
@@ -372,6 +379,7 @@ function MenuItem(
isAnonymousAction = false,
shouldBlockSelection = false,
shouldParseTitle = false,
+ shouldParseHelperText = false,
shouldCheckActionAllowedOnPress = true,
onSecondaryInteraction,
titleWithTooltips,
@@ -425,10 +433,16 @@ function MenuItem(
if (!title || !shouldParseTitle) {
return '';
}
- const parser = new ExpensiMark();
- return parser.replace(title, {shouldEscapeText});
+ return Parser.replace(title, {shouldEscapeText});
}, [title, shouldParseTitle, shouldEscapeText]);
+ const helperHtml = useMemo(() => {
+ if (!helperText || !shouldParseHelperText) {
+ return '';
+ }
+ return Parser.replace(helperText, {shouldEscapeText});
+ }, [helperText, shouldParseHelperText, shouldEscapeText]);
+
const processedTitle = useMemo(() => {
let titleToWrap = '';
if (shouldRenderAsHTML) {
@@ -442,6 +456,16 @@ function MenuItem(
return titleToWrap ? `${titleToWrap} ` : '';
}, [title, shouldRenderAsHTML, shouldParseTitle, html]);
+ const processedHelperText = useMemo(() => {
+ let textToWrap = '';
+
+ if (shouldParseHelperText) {
+ textToWrap = helperHtml;
+ }
+
+ return textToWrap ? `${textToWrap} ` : '';
+ }, [shouldParseHelperText, helperHtml]);
+
const hasPressableRightComponent = iconRight || (shouldShowRightComponent && rightComponent);
const renderTitleContent = () => {
@@ -680,6 +704,7 @@ function MenuItem(
)}
+ {!!furtherDetailsComponent && {furtherDetailsComponent} }
{titleComponent}
@@ -767,7 +792,14 @@ function MenuItem(
)}
- {!!helperText && {helperText} }
+ {!!helperText &&
+ (shouldParseHelperText ? (
+
+
+
+ ) : (
+ {helperText}
+ ))}
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index 2eb073bb39be..d88dde545f3b 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -118,7 +118,7 @@ function BaseModal(
}
hideModal(true);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[],
);
@@ -242,7 +242,9 @@ function BaseModal(
deviceWidth={windowWidth}
animationIn={animationIn ?? modalStyleAnimationIn}
animationOut={animationOut ?? modalStyleAnimationOut}
+ // eslint-disable-next-line react-compiler/react-compiler
useNativeDriver={useNativeDriverProp && useNativeDriver}
+ // eslint-disable-next-line react-compiler/react-compiler
useNativeDriverForBackdrop={useNativeDriverForBackdrop && useNativeDriver}
hideModalContentWhileAnimating={hideModalContentWhileAnimating}
animationInTiming={animationInTiming}
diff --git a/src/components/Modal/ModalContent.tsx b/src/components/Modal/ModalContent.tsx
index 49d3b049220f..f71affe760ac 100644
--- a/src/components/Modal/ModalContent.tsx
+++ b/src/components/Modal/ModalContent.tsx
@@ -14,7 +14,7 @@ type ModalContentProps = {
};
function ModalContent({children, onDismiss = () => {}}: ModalContentProps) {
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
React.useEffect(() => () => onDismiss?.(), []);
return children;
}
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index fa3454ed9e61..1acee187c67b 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -8,8 +8,8 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import * as HeaderUtils from '@libs/HeaderUtils';
import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
@@ -22,7 +22,6 @@ import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import Button from './Button';
@@ -36,6 +35,7 @@ import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusB
import type {ActionHandledType} from './ProcessMoneyReportHoldMenu';
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu';
+import ExportWithDropdownMenu from './ReportActionItem/ExportWithDropdownMenu';
import SettlementButton from './SettlementButton';
type MoneyReportHeaderProps = {
@@ -86,29 +86,22 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false);
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
+ const {isSmallScreenWidth} = useWindowDimensions();
const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport);
- const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
- const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
const isOnHold = TransactionUtils.isOnHold(transaction);
- const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
const isDeletedParentAction = !!requestParentReportAction && ReportActionsUtils.isDeletedAction(requestParentReportAction);
- const canHoldOrUnholdRequest = !isEmptyObject(transaction) && !isSettled && !isApproved && !isDeletedParentAction;
// Only the requestor can delete the request, admins can only edit it.
const isActionOwner =
typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID;
const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction;
- const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
- const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID;
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [paymentType, setPaymentType] = useState();
const [requestType, setRequestType] = useState();
const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy);
const policyType = policy?.type;
- const isPayer = ReportUtils.isPayer(session, moneyRequestReport);
const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport);
- const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
+ const connectedIntegration = PolicyUtils.getConnectedIntegration(policy);
const navigateBackToAfterDelete = useRef();
const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t));
@@ -117,29 +110,26 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
// allTransactions in TransactionUtils might have stale data
const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID, transactions);
- const cancelPayment = useCallback(() => {
- if (!chatReport) {
- return;
- }
- IOU.cancelPayment(moneyRequestReport, chatReport);
- setIsConfirmModalVisible(false);
- }, [moneyRequestReport, chatReport]);
-
const shouldShowPayButton = useMemo(() => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy), [moneyRequestReport, chatReport, policy]);
const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, chatReport, policy), [moneyRequestReport, chatReport, policy]);
const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);
- const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation;
-
const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation;
+
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+ const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin;
+
+ const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation && !shouldShowExportIntegrationButton;
+
const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation;
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt;
const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
- const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation;
+ const shouldShowAnyButton =
+ shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation || shouldShowExportIntegrationButton;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency);
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy);
@@ -197,22 +187,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
TransactionActions.markAsCash(iouTransactionID, reportID);
}, [requestParentReportAction, transactionThreadReport?.reportID]);
- const changeMoneyRequestStatus = () => {
- if (!transactionThreadReport) {
- return;
- }
- const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction)
- ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID ?? '-1'
- : '-1';
-
- if (isOnHold) {
- IOU.unholdRequest(iouTransactionID, transactionThreadReport.reportID);
- } else {
- const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams());
- Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, iouTransactionID, transactionThreadReport.reportID, activeRoute));
- }
- };
-
const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => (
changeMoneyRequestStatus(),
- });
- }
- if (!isOnHold && (isRequestIOU || canModifyStatus) && !isScanning && !isInvoiceReport) {
- threeDotsMenuItems.push({
- icon: Expensicons.Stopwatch,
- text: translate('iou.hold'),
- onSelected: () => changeMoneyRequestStatus(),
- });
- }
- }
-
useEffect(() => {
if (isLoadingHoldUseExplained) {
return;
@@ -290,23 +241,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
IOU.dismissHoldUseExplanation();
};
- if (isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport)) {
- threeDotsMenuItems.push({
- icon: Expensicons.Trashcan,
- text: translate('iou.cancelPayment'),
- onSelected: () => setIsConfirmModalVisible(true),
- });
- }
-
- // If the report supports adding transactions to it, then it also supports deleting transactions from it.
- if (canDeleteRequest && !isEmptyObject(transactionThreadReport)) {
- threeDotsMenuItems.push({
- icon: Expensicons.Trashcan,
- text: translate('reportActionContextMenu.deleteAction', {action: requestParentReportAction}),
- onSelected: () => setIsDeleteRequestModalVisible(true),
- });
- }
-
useEffect(() => {
if (canDeleteRequest) {
return;
@@ -327,9 +261,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
onBackButtonPress={onBackButtonPress}
// Shows border if no buttons or banners are showing below the header
shouldShowBorderBottom={!isMoreContentShown}
- shouldShowThreeDotsButton
- threeDotsMenuItems={threeDotsMenuItems}
- threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
>
{shouldShowSettlementButton && !shouldUseNarrowLayout && (
@@ -352,6 +283,15 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
/>
)}
+ {shouldShowExportIntegrationButton && !shouldUseNarrowLayout && (
+
+
+
+ )}
{shouldShowSubmitButton && !shouldUseNarrowLayout && (
)}
-
- {shouldShowSettlementButton && shouldUseNarrowLayout && (
-
- )}
- {shouldShowSubmitButton && shouldUseNarrowLayout && (
- IOU.submitReport(moneyRequestReport)}
- isDisabled={shouldDisableSubmitButton}
- />
- )}
- {shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
-
- )}
- {shouldShowNextStep && }
- {statusBarProps && (
-
- )}
-
+ {isMoreContentShown && (
+
+ {shouldShowSettlementButton && shouldUseNarrowLayout && (
+
+ )}
+ {shouldShowExportIntegrationButton && shouldUseNarrowLayout && (
+
+ )}
+ {shouldShowSubmitButton && shouldUseNarrowLayout && (
+ IOU.submitReport(moneyRequestReport)}
+ isDisabled={shouldDisableSubmitButton}
+ />
+ )}
+ {shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
+
+ )}
+ {shouldShowNextStep && }
+ {statusBarProps && (
+
+ )}
+
+ )}
{isHoldMenuVisible && requestType !== undefined && (
)}
- setIsConfirmModalVisible(false)}
- prompt={translate('iou.cancelPaymentConfirmation')}
- confirmText={translate('iou.cancelPayment')}
- cancelText={translate('common.dismiss')}
- danger
- shouldEnableNewFocusManagement
- />
CurrencyUtils.convertToFrontendAmountAsString(amount);
+const defaultOnFormatAmount = (amount: number, currency?: string) => CurrencyUtils.convertToFrontendAmountAsString(amount, currency ?? CONST.CURRENCY.USD);
function MoneyRequestAmountInput(
{
@@ -123,6 +126,7 @@ function MoneyRequestAmountInput(
hideFocusedState = true,
shouldKeepUserInput = false,
autoGrow = true,
+ contentWidth,
...props
}: MoneyRequestAmountInputProps,
forwardedRef: ForwardedRef,
@@ -218,7 +222,7 @@ function MoneyRequestAmountInput(
}
// we want to re-initialize the state only when the amount changes
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [amount, shouldKeepUserInput]);
// Modifies the amount to match the decimals for changed currency.
@@ -232,7 +236,7 @@ function MoneyRequestAmountInput(
setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount));
// we want to update only when decimals change (setNewAmount also changes when decimals change).
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [setNewAmount]);
/**
@@ -295,6 +299,7 @@ function MoneyRequestAmountInput(
// eslint-disable-next-line no-param-reassign
forwardedRef.current = ref;
}
+ // eslint-disable-next-line react-compiler/react-compiler
textInput.current = ref;
}}
selectedCurrencyCode={currency}
@@ -325,6 +330,7 @@ function MoneyRequestAmountInput(
hideFocusedState={hideFocusedState}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
+ contentWidth={contentWidth}
/>
);
}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 8bfcbbeb779e..ab62febbf950 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -1,7 +1,6 @@
-import {useIsFocused} from '@react-navigation/native';
+import {useFocusEffect, useIsFocused} from '@react-navigation/native';
import lodashIsEqual from 'lodash/isEqual';
-import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
-import type {TextStyle} from 'react-native';
+import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
@@ -11,7 +10,6 @@ import useLocalize from '@hooks/useLocalize';
import {MouseProvider} from '@hooks/useMouseContext';
import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
-import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
@@ -162,6 +160,9 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
/** The action to take */
action?: IOUAction;
+
+ /** Should play sound on confirmation */
+ shouldPlaySound?: boolean;
};
type MoneyRequestConfirmationListItem = Participant | ReportUtils.OptionData;
@@ -204,12 +205,12 @@ function MoneyRequestConfirmationList({
action = CONST.IOU.ACTION.CREATE,
currencyList,
shouldDisplayReceipt = false,
+ shouldPlaySound = true,
}: MoneyRequestConfirmationListProps) {
const policy = policyReal ?? policyDraft;
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;
const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
const {translate, toLocaleDigit} = useLocalize();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const {canUseP2PDistanceRequests} = usePermissions(iouType);
@@ -240,10 +241,7 @@ function MoneyRequestConfirmationList({
const {unit, rate} = mileageRate ?? {};
- const distance = TransactionUtils.getDistance(transaction);
const prevRate = usePrevious(rate);
- const prevDistance = usePrevious(distance);
- const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance);
const currency = (mileageRate as MileageRate)?.currency ?? policyCurrency;
@@ -257,6 +255,18 @@ function MoneyRequestConfirmationList({
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest) && !isTypeInvoice;
const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action);
+
+ const distance = useMemo(() => {
+ const value = TransactionUtils.getDistance(transaction);
+ if (canUseP2PDistanceRequests && isMovingTransactionFromTrackExpense && unit && !TransactionUtils.isFetchingWaypointsFromServer(transaction)) {
+ return DistanceRequestUtils.convertToDistanceInMeters(value, unit);
+ }
+ return value;
+ }, [isMovingTransactionFromTrackExpense, unit, transaction, canUseP2PDistanceRequests]);
+ const prevDistance = usePrevious(distance);
+
+ const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance);
+
const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest);
const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense;
const formattedAmount = isDistanceRequestWithPendingRoute
@@ -284,10 +294,22 @@ function MoneyRequestConfirmationList({
}, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]);
const isMerchantEmpty = useMemo(() => !iouMerchant || TransactionUtils.isMerchantMissing(transaction), [transaction, iouMerchant]);
- const isMerchantRequired = (isPolicyExpenseChat || isTypeInvoice) && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant;
+ const isMerchantRequired = isPolicyExpenseChat && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant;
const isCategoryRequired = !!policy?.requiresCategory;
+ const shouldDisableParticipant = (participant: Participant): boolean => {
+ if (ReportUtils.isDraftReport(participant.reportID)) {
+ return true;
+ }
+
+ if (!participant.isInvoiceRoom && !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1)) {
+ return true;
+ }
+
+ return false;
+ };
+
useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
@@ -300,7 +322,7 @@ function MoneyRequestConfirmationList({
// reset the form error whenever the screen gains or loses focus
setFormError('');
- // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes
}, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]);
useEffect(() => {
@@ -329,10 +351,10 @@ function MoneyRequestConfirmationList({
taxCode = transaction?.taxCode ?? TransactionUtils.getDefaultTaxCode(policy, transaction) ?? '';
}
const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? '';
- const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount);
+ const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, currency);
const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString()));
IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', taxAmountInSmallestCurrencyUnits);
- }, [policy, shouldShowTax, previousTransactionAmount, previousTransactionCurrency, transaction, isDistanceRequest, customUnitRateID]);
+ }, [policy, shouldShowTax, previousTransactionAmount, previousTransactionCurrency, transaction, isDistanceRequest, customUnitRateID, currency]);
// If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again
if (isEditingSplitBill && didConfirm) {
@@ -430,6 +452,7 @@ function MoneyRequestConfirmationList({
return {
...participantOption,
isSelected: false,
+ isInteractive: !shouldDisableParticipant(participantOption),
rightElement: (
{amount ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''}
@@ -440,14 +463,13 @@ function MoneyRequestConfirmationList({
}
const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode;
- const prefixPadding = StyleUtils.getCharacterPadding(currencySymbol ?? '');
const formattedTotalAmount = CurrencyUtils.convertToDisplayStringWithoutCurrency(iouAmount, iouCurrencyCode);
- const amountWidth = StyleUtils.getWidthStyle(formattedTotalAmount.length * 9 + prefixPadding);
return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({
...participantOption,
tabIndex: -1,
isSelected: false,
+ isInteractive: !shouldDisableParticipant(participantOption),
rightElement: (
onSplitShareChange(participantOption.accountID ?? -1, Number(value))}
maxLength={formattedTotalAmount.length}
+ contentWidth={formattedTotalAmount.length * 8}
/>
),
}));
@@ -475,7 +498,6 @@ function MoneyRequestConfirmationList({
shouldShowReadOnlySplits,
currencyList,
iouCurrencyCode,
- StyleUtils,
iouAmount,
selectedParticipants,
styles.flexWrap,
@@ -553,7 +575,7 @@ function MoneyRequestConfirmationList({
const formattedSelectedParticipants = selectedParticipants.map((participant) => ({
...participant,
isSelected: false,
- isDisabled: !participant.isInvoiceRoom && !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
+ isInteractive: !shouldDisableParticipant(participant),
}));
options.push({
title: translate('common.to'),
@@ -603,7 +625,7 @@ function MoneyRequestConfirmationList({
}
IOU.setMoneyRequestCategory(transactionID, enabledCategories[0].name);
// Keep 'transaction' out to ensure that we autoselect the option only once
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [shouldShowCategories, policyCategories, isCategoryRequired]);
// Auto select the tag if there is only one enabled tag and it is required
@@ -621,7 +643,7 @@ function MoneyRequestConfirmationList({
IOU.setMoneyRequestTag(transactionID, updatedTagsString);
}
// Keep 'transaction' out to ensure that we autoselect the option only once
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [policyTagLists, policyTags]);
/**
@@ -677,7 +699,9 @@ function MoneyRequestConfirmationList({
return;
}
- playSound(SOUNDS.DONE);
+ if (shouldPlaySound) {
+ playSound(SOUNDS.DONE);
+ }
setDidConfirm(true);
onConfirm?.(selectedParticipants);
} else {
@@ -711,9 +735,21 @@ function MoneyRequestConfirmationList({
isDistanceRequestWithPendingRoute,
iouAmount,
onConfirm,
+ shouldPlaySound,
],
);
+ const focusTimeoutRef = useRef(null);
+ const confirmButtonRef = useRef(null);
+ useFocusEffect(
+ useCallback(() => {
+ focusTimeoutRef.current = setTimeout(() => {
+ confirmButtonRef.current?.focus();
+ }, CONST.ANIMATED_TRANSITION);
+ return () => focusTimeoutRef.current && clearTimeout(focusTimeoutRef.current);
+ }, []),
+ );
+
const footerContent = useMemo(() => {
if (isReadOnly) {
return;
@@ -742,9 +778,11 @@ function MoneyRequestConfirmationList({
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
}}
enterKeyEventListenerPriority={1}
+ useKeyboardShortcuts
/>
) : (
);
@@ -814,6 +853,7 @@ function MoneyRequestConfirmationList({
isTypeInvoice={isTypeInvoice}
onToggleBillable={onToggleBillable}
policy={policy}
+ policyTags={policyTags}
policyTagLists={policyTagLists}
rate={rate}
receiptFilename={receiptFilename}
@@ -845,6 +885,7 @@ function MoneyRequestConfirmationList({
footerContent={footerContent}
listFooterContent={listFooterContent}
containerStyle={[styles.flexBasisAuto]}
+ disableKeyboardShortcuts
removeClippedSubviews={false}
/>
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index cda43938a18f..48190fb3c759 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -117,6 +117,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** The policy */
policy: OnyxEntry;
+ /** The policy tag lists */
+ policyTags: OnyxEntry;
+
/** The policy tag lists */
policyTagLists: Array>;
@@ -193,6 +196,7 @@ function MoneyRequestConfirmationListFooter({
isTypeInvoice,
onToggleBillable,
policy,
+ policyTags,
policyTagLists,
rate,
receiptFilename,
@@ -226,6 +230,7 @@ function MoneyRequestConfirmationListFooter({
// A flag for showing the tags field
// TODO: remove the !isTypeInvoice from this condition after BE supports tags for invoices: https://github.com/Expensify/App/issues/41281
const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists) && !isTypeInvoice, [isPolicyExpenseChat, isTypeInvoice, policyTagLists]);
+ const isMultilevelTags = useMemo(() => PolicyUtils.isMultiLevelTags(policyTags), [policyTags]);
const senderWorkspace = useMemo(() => {
const senderWorkspaceParticipant = selectedParticipants.find((participant) => participant.isSender);
@@ -251,7 +256,8 @@ function MoneyRequestConfirmationListFooter({
// Do not hide fields in case of paying someone
const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || !!isEditingSplitBill;
// Calculate the formatted tax amount based on the transaction's tax amount and the IOU currency code
- const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode);
+ const taxAmount = TransactionUtils.getTaxAmount(transaction, false);
+ const formattedTaxAmount = CurrencyUtils.convertToDisplayString(taxAmount, iouCurrencyCode);
// Get the tax rate title based on the policy and transaction
const taxRateTitle = TransactionUtils.getTaxName(policy, transaction);
// Determine if the merchant error should be displayed
@@ -437,8 +443,9 @@ function MoneyRequestConfirmationListFooter({
shouldShow: shouldShowCategories,
isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired,
},
- ...policyTagLists.map(({name, required}, index) => {
+ ...policyTagLists.map(({name, required, tags}, index) => {
const isTagRequired = required ?? false;
+ const shouldShow = shouldShowTags && (!isMultilevelTags || OptionsListUtils.hasEnabledOptions(tags));
return {
item: (
),
- shouldShow: shouldShowTags,
+ shouldShow,
isSupplementary: !isTagRequired,
};
}),
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index d55d3cc19fe9..b30e9da50701 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -1,5 +1,5 @@
import type {ReactNode} from 'react';
-import React, {useCallback, useEffect, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
@@ -7,7 +7,6 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import * as HeaderUtils from '@libs/HeaderUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -15,15 +14,12 @@ import * as TransactionUtils from '@libs/TransactionUtils';
import variables from '@styles/variables';
import * as IOU from '@userActions/IOU';
import * as TransactionActions from '@userActions/Transaction';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {Route} from '@src/ROUTES';
import type {Policy, Report, ReportAction} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import Button from './Button';
-import ConfirmModal from './ConfirmModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
@@ -56,43 +52,21 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
}`,
);
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
- const [session] = useOnyx(ONYXKEYS.SESSION);
const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true});
const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult);
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
- const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false);
const isSelfDMTrackExpenseReport = ReportUtils.isTrackExpenseReport(report) && ReportUtils.isSelfDM(parentReport);
const moneyRequestReport = !isSelfDMTrackExpenseReport ? parentReport : undefined;
- const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID);
- const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport);
const isOnHold = TransactionUtils.isOnHold(transaction);
const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? '');
- const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
+ const {isSmallScreenWidth} = useWindowDimensions();
- const navigateBackToAfterDelete = useRef();
-
- // Only the requestor can take delete the request, admins can only edit it.
- const isActionOwner = typeof parentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && parentReportAction.actorAccountID === session?.accountID;
- const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
- const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID;
const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']);
const shouldShowMarkAsCashButton = isDraft && hasAllPendingRTERViolations;
- const deleteTransaction = useCallback(() => {
- if (parentReportAction) {
- const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
- if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) {
- navigateBackToAfterDelete.current = IOU.deleteTrackExpense(parentReport?.reportID ?? '-1', iouTransactionID, parentReportAction, true);
- } else {
- navigateBackToAfterDelete.current = IOU.deleteMoneyRequest(iouTransactionID, parentReportAction, true);
- }
- }
-
- setIsDeleteModalVisible(false);
- }, [parentReport?.reportID, parentReportAction, setIsDeleteModalVisible]);
const markAsCash = useCallback(() => {
TransactionActions.markAsCash(transaction?.transactionID ?? '-1', report.reportID);
@@ -100,23 +74,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
- const isDeletedParentAction = ReportActionsUtils.isDeletedAction(parentReportAction);
- const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction && !ReportUtils.isArchivedRoom(parentReport);
-
- // If the report supports adding transactions to it, then it also supports deleting transactions from it.
- const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || isSelfDMTrackExpenseReport) && !isDeletedParentAction;
-
- const changeMoneyRequestStatus = () => {
- const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
-
- if (isOnHold) {
- IOU.unholdRequest(iouTransactionID, report?.reportID);
- } else {
- const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams());
- Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, iouTransactionID, report?.reportID, activeRoute));
- }
- };
-
const getStatusIcon: (src: IconAsset) => ReactNode = (src) => (
{
- if (canDeleteRequest) {
- return;
- }
-
- setIsDeleteModalVisible(false);
- }, [canDeleteRequest]);
-
- const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)];
- if (canHoldOrUnholdRequest) {
- const isRequestIOU = parentReport?.type === 'iou';
- const isHoldCreator = ReportUtils.isHoldCreator(transaction, report?.reportID) && isRequestIOU;
- const isTrackExpenseReport = ReportUtils.isTrackExpenseReport(report);
- const canModifyStatus = !isTrackExpenseReport && (isPolicyAdmin || isActionOwner || isApprover);
- if (isOnHold && !isDuplicate && (isHoldCreator || (!isRequestIOU && canModifyStatus))) {
- threeDotsMenuItems.push({
- icon: Expensicons.Stopwatch,
- text: translate('iou.unholdExpense'),
- onSelected: () => changeMoneyRequestStatus(),
- });
- }
- if (!isOnHold && (isRequestIOU || canModifyStatus) && !isScanning) {
- threeDotsMenuItems.push({
- icon: Expensicons.Stopwatch,
- text: translate('iou.hold'),
- onSelected: () => changeMoneyRequestStatus(),
- });
- }
- }
-
useEffect(() => {
if (isLoadingHoldUseExplained) {
return;
@@ -199,14 +126,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
IOU.dismissHoldUseExplanation();
};
- if (canDeleteRequest) {
- threeDotsMenuItems.push({
- icon: Expensicons.Trashcan,
- text: translate('reportActionContextMenu.deleteAction', {action: parentReportAction}),
- onSelected: () => setIsDeleteModalVisible(true),
- });
- }
-
return (
<>
@@ -215,9 +134,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
shouldShowReportAvatarWithDisplay
shouldEnableDetailPageNavigation
shouldShowPinButton={false}
- shouldShowThreeDotsButton
- threeDotsMenuItems={threeDotsMenuItems}
- threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
report={{
...report,
ownerAccountID: parentReport?.ownerAccountID,
@@ -281,18 +197,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
)}
- setIsDeleteModalVisible(false)}
- onModalHide={() => ReportUtils.navigateBackAfterDeleteTransaction(navigateBackToAfterDelete.current)}
- prompt={translate('iou.deleteConfirmation')}
- confirmText={translate('common.delete')}
- cancelText={translate('common.cancel')}
- danger
- shouldEnableNewFocusManagement
- />
{isSmallScreenWidth && shouldShowHoldMenu && (
;
/** If there is a pager wrapping the canvas, we need to disable the pan gesture in case the pager is swiping */
- pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude
+ pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude
/** Handles scale changed event */
onScaleChanged?: OnScaleChangedCallback;
@@ -48,6 +49,7 @@ type MultiGestureCanvasProps = ChildrenProps & {
/** Handles scale changed event */
onTap?: OnTapCallback;
+ /** Handles swipe down event */
onSwipeDown?: OnSwipeDownCallback;
};
@@ -119,6 +121,7 @@ function MultiGestureCanvas({
const reset = useWorkletCallback((animated: boolean, callback?: () => void) => {
stopAnimation();
+ // eslint-disable-next-line react-compiler/react-compiler
offsetX.value = 0;
offsetY.value = 0;
pinchScale.value = 1;
@@ -242,11 +245,12 @@ function MultiGestureCanvas({
e.preventDefault()}
style={StyleUtils.getFullscreenCenteredContentStyles()}
>
{children}
diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts
index 903f384dd525..fa27e48eea4c 100644
--- a/src/components/MultiGestureCanvas/usePanGesture.ts
+++ b/src/components/MultiGestureCanvas/usePanGesture.ts
@@ -3,6 +3,7 @@ import {Dimensions} from 'react-native';
import type {PanGesture} from 'react-native-gesture-handler';
import {Gesture} from 'react-native-gesture-handler';
import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated';
+import * as Browser from '@libs/Browser';
import {SPRING_CONFIG} from './constants';
import type {MultiGestureCanvasVariables} from './types';
import * as MultiGestureCanvasUtils from './utils';
@@ -57,6 +58,8 @@ const usePanGesture = ({
const panVelocityX = useSharedValue(0);
const panVelocityY = useSharedValue(0);
+ const isMobileBrowser = Browser.isMobile();
+
// Disable "swipe down to close" gesture when content is bigger than the canvas
const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.value, [canvasSize.height]);
@@ -113,6 +116,7 @@ const usePanGesture = ({
// If the (absolute) velocity is 0, we don't need to run an animation
if (Math.abs(panVelocityX.value) !== 0) {
// Phase out the pan animation
+ // eslint-disable-next-line react-compiler/react-compiler
offsetX.value = withDecay({
velocity: panVelocityX.value,
clamp: [horizontalBoundaries.min, horizontalBoundaries.max],
@@ -207,7 +211,9 @@ const usePanGesture = ({
panVelocityY.value = evt.velocityY;
if (!isSwipingDownToClose.value) {
- panTranslateX.value += evt.changeX;
+ if (!isMobileBrowser || (isMobileBrowser && zoomScale.value !== 1)) {
+ panTranslateX.value += evt.changeX;
+ }
}
if (enableSwipeDownToClose.value || isSwipingDownToClose.value) {
diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts
index 87d3bdada6a2..46a5e28e5732 100644
--- a/src/components/MultiGestureCanvas/usePinchGesture.ts
+++ b/src/components/MultiGestureCanvas/usePinchGesture.ts
@@ -68,6 +68,7 @@ const usePinchGesture = ({
useAnimatedReaction(
() => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value],
([translateX, translateY, bounceX, bounceY]) => {
+ // eslint-disable-next-line react-compiler/react-compiler
totalPinchTranslateX.value = translateX + bounceX;
totalPinchTranslateY.value = translateY + bounceY;
},
diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts
index f550e93d6be2..e4bb02bd5d34 100644
--- a/src/components/MultiGestureCanvas/useTapGestures.ts
+++ b/src/components/MultiGestureCanvas/useTapGestures.ts
@@ -111,6 +111,7 @@ const useTapGestures = ({
offsetAfterZooming.y = 0;
}
+ // eslint-disable-next-line react-compiler/react-compiler
offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG);
offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG);
zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG, callback);
diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx
index 28e1d81b30e4..1f4b56d87f17 100644
--- a/src/components/OfflineWithFeedback.tsx
+++ b/src/components/OfflineWithFeedback.tsx
@@ -1,6 +1,5 @@
-import {mapValues} from 'lodash';
import React, {useCallback} from 'react';
-import type {ImageStyle, StyleProp, TextStyle, ViewStyle} from 'react-native';
+import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -10,11 +9,11 @@ import shouldRenderOffscreen from '@libs/shouldRenderOffscreen';
import type {AllStyles} from '@styles/utils/types';
import CONST from '@src/CONST';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
-import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction';
+import type {ReceiptErrors} from '@src/types/onyx/Transaction';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import CustomStylesForChildrenProvider from './CustomStylesForChildrenProvider';
-import MessagesRow from './MessagesRow';
+import ErrorMessageRow from './ErrorMessageRow';
/**
* This component should be used when we are using the offline pattern B (offline with feedback).
@@ -60,7 +59,7 @@ type OfflineWithFeedbackProps = ChildrenProps & {
canDismissError?: boolean;
};
-type StrikethroughProps = Partial & {style: Array};
+type StrikethroughProps = Partial & {style: AllStyles[]};
function OfflineWithFeedback({
pendingAction,
@@ -83,12 +82,6 @@ function OfflineWithFeedback({
const hasErrors = !isEmptyObject(errors ?? {});
- // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages.
- const errorEntries = Object.entries(errors ?? {});
- const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError] => errorEntry[1] !== null);
- const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => error);
-
- const hasErrorMessages = !isEmptyObject(errorMessages);
const isOfflinePendingAction = !!isOffline && !!pendingAction;
const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE);
const isAddError = hasErrors && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;
@@ -107,9 +100,10 @@ function OfflineWithFeedback({
return child;
}
- const childProps: {children: React.ReactNode | undefined; style: AllStyles} = child.props;
+ type ChildComponentProps = ChildrenProps & {style?: AllStyles};
+ const childProps = child.props as ChildComponentProps;
const props: StrikethroughProps = {
- style: StyleUtils.combineStyles(childProps.style, styles.offlineFeedback.deleted, styles.userSelectNone),
+ style: StyleUtils.combineStyles(childProps.style ?? [], styles.offlineFeedback.deleted, styles.userSelectNone),
};
if (childProps.children) {
@@ -138,13 +132,12 @@ function OfflineWithFeedback({
{children}
)}
- {shouldShowErrorMessages && hasErrorMessages && (
-
)}
diff --git a/src/components/Onfido/BaseOnfidoWeb.tsx b/src/components/Onfido/BaseOnfidoWeb.tsx
index ebb29198bda7..d1c784c078bf 100644
--- a/src/components/Onfido/BaseOnfidoWeb.tsx
+++ b/src/components/Onfido/BaseOnfidoWeb.tsx
@@ -27,9 +27,11 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo
token: sdkToken,
containerId: CONST.ONFIDO.CONTAINER_ID,
customUI: {
- fontFamilyTitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`,
- fontFamilySubtitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`,
- fontFamilyBody: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`,
+ // Font styles are commented out until Onfido fixes it on their side, more info here - https://github.com/Expensify/App/issues/44570
+ // For now we will use Onfido default font which is better than random serif font which it started defaulting to
+ // fontFamilyTitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`,
+ // fontFamilySubtitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`,
+ // fontFamilyBody: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`,
fontSizeTitle: `${variables.fontSizeLarge}px`,
fontWeightTitle: Number(FontUtils.fontWeight.bold),
fontWeightSubtitle: 400,
@@ -140,7 +142,7 @@ function Onfido({sdkToken, onSuccess, onError, onUserExit}: OnfidoProps, ref: Fo
window.addEventListener('userAnalyticsEvent', logOnFidoEvent);
return () => window.removeEventListener('userAnalyticsEvent', logOnFidoEvent);
// Onfido should be initialized only once on mount
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
return (
diff --git a/src/components/Onfido/index.native.tsx b/src/components/Onfido/index.native.tsx
index fd681e610f86..c6eb9c8868ee 100644
--- a/src/components/Onfido/index.native.tsx
+++ b/src/components/Onfido/index.native.tsx
@@ -88,7 +88,7 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) {
}
});
// Onfido should be initialized only once on mount
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
return ;
diff --git a/src/components/OpacityView.tsx b/src/components/OpacityView.tsx
index 41ab148bd7f2..d4a5c05167a0 100644
--- a/src/components/OpacityView.tsx
+++ b/src/components/OpacityView.tsx
@@ -36,6 +36,7 @@ function OpacityView({shouldDim, children, style = [], dimmingValue = variables.
React.useEffect(() => {
if (shouldDim) {
+ // eslint-disable-next-line react-compiler/react-compiler
opacity.value = withTiming(dimmingValue, {duration: 50});
} else {
opacity.value = withTiming(1, {duration: 50});
diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx
index 21f8bb3de097..f098188de270 100644
--- a/src/components/OptionListContextProvider.tsx
+++ b/src/components/OptionListContextProvider.tsx
@@ -73,7 +73,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp
return newOptions;
});
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [reports]);
/**
@@ -124,7 +124,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp
});
// This effect is used to update the options list when personal details change so we ignore all dependencies except personalDetails
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [personalDetails]);
const loadOptions = useCallback(() => {
diff --git a/src/components/OptionsListSkeletonView.tsx b/src/components/OptionsListSkeletonView.tsx
index a11077f95bb5..6dede512f405 100644
--- a/src/components/OptionsListSkeletonView.tsx
+++ b/src/components/OptionsListSkeletonView.tsx
@@ -18,16 +18,18 @@ function getLinedWidth(index: number): string {
type OptionsListSkeletonViewProps = {
shouldAnimate?: boolean;
+ gradientOpacityEnabled?: boolean;
shouldStyleAsTable?: boolean;
};
-function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = false}: OptionsListSkeletonViewProps) {
+function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = false, gradientOpacityEnabled = false}: OptionsListSkeletonViewProps) {
const styles = useThemeStyles();
return (
{
const lineWidth = getLinedWidth(itemIndex);
diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx
index a64a676cb97a..2edf699affab 100644
--- a/src/components/PDFView/index.tsx
+++ b/src/components/PDFView/index.tsx
@@ -68,7 +68,7 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, max
useEffect(() => {
retrieveCanvasLimits();
// This rule needs to be applied so that this effect is executed only when the component is mounted
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
useEffect(() => {
diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx
index 1c337e024116..ddd6cd544f3e 100644
--- a/src/components/Picker/BasePicker.tsx
+++ b/src/components/Picker/BasePicker.tsx
@@ -61,7 +61,7 @@ function BasePicker(
// so they don't have to spend extra time selecting the only possible value.
onInputChange(items[0].value, 0);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [items]);
const context = useScrollContext();
diff --git a/src/components/PlaidLink/index.native.tsx b/src/components/PlaidLink/index.native.tsx
index 24ab75eb62b7..37b598303c3a 100644
--- a/src/components/PlaidLink/index.native.tsx
+++ b/src/components/PlaidLink/index.native.tsx
@@ -31,7 +31,7 @@ function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: Pl
};
// We generally do not need to include the token as a dependency here as it is only provided once via props and should not change
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
return null;
}
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 36e33fdda799..bcec153491c9 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -1,7 +1,7 @@
import lodashIsEqual from 'lodash/isEqual';
import type {RefObject} from 'react';
import React, {useEffect, useRef, useState} from 'react';
-import {View} from 'react-native';
+import {StyleSheet, View} from 'react-native';
import type {ModalProps} from 'react-native-modal';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
@@ -109,16 +109,18 @@ function PopoverMenu({
const selectedItemIndex = useRef(null);
const [currentMenuItems, setCurrentMenuItems] = useState(menuItems);
+ const currentMenuItemsFocusedIndex = currentMenuItems?.findIndex((option) => option.isSelected);
const [enteredSubMenuIndexes, setEnteredSubMenuIndexes] = useState([]);
- const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: currentMenuItems.length - 1, isActive: isVisible});
+ const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: currentMenuItemsFocusedIndex, maxIndex: currentMenuItems.length - 1, isActive: isVisible});
const selectItem = (index: number) => {
const selectedItem = currentMenuItems[index];
if (selectedItem?.subMenuItems) {
setCurrentMenuItems([...selectedItem.subMenuItems]);
setEnteredSubMenuIndexes([...enteredSubMenuIndexes, index]);
- setFocusedIndex(-1);
+ const selectedSubMenuItemIndex = selectedItem?.subMenuItems.findIndex((option) => option.isSelected);
+ setFocusedIndex(selectedSubMenuItemIndex);
} else {
selectedItemIndex.current = index;
onItemSelected(selectedItem, index);
@@ -162,6 +164,13 @@ function PopoverMenu({
);
};
+ const renderHeaderText = () => {
+ if (!headerText || enteredSubMenuIndexes.length !== 0) {
+ return;
+ }
+ return {headerText} ;
+ };
+
useKeyboardShortcut(
CONST.KEYBOARD_SHORTCUTS.ENTER,
() => {
@@ -213,7 +222,7 @@ function PopoverMenu({
>
- {!!headerText && enteredSubMenuIndexes.length === 0 && {headerText} }
+ {renderHeaderText()}
{enteredSubMenuIndexes.length > 0 && renderBackButtonItem()}
{currentMenuItems.map((item, menuIndex) => (
))}
diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx
index 06478b468e1e..7d58ad6d22be 100644
--- a/src/components/PopoverWithoutOverlay/index.tsx
+++ b/src/components/PopoverWithoutOverlay/index.tsx
@@ -69,7 +69,7 @@ function PopoverWithoutOverlay(
removeOnClose();
};
// We want this effect to run strictly ONLY when isVisible prop changes
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isVisible]);
const {
@@ -124,6 +124,7 @@ function PopoverWithoutOverlay(
ref={viewRef(withoutOverlayRef)}
// Prevent the parent element to capture a click. This is useful when the modal component is put inside a pressable.
onClick={(e) => e.stopPropagation()}
+ dataSet={{dragArea: false}}
>
{
+ if (!interactive) {
+ return styles.cursorDefault;
+ }
if (shouldUseDisabledCursor) {
return styles.cursorDisabled;
}
@@ -74,7 +78,7 @@ function GenericPressable(
return styles.cursorText;
}
return styles.cursorPointer;
- }, [styles, shouldUseDisabledCursor, rest.accessibilityRole, rest.role]);
+ }, [styles, shouldUseDisabledCursor, rest.accessibilityRole, rest.role, interactive]);
const onLongPressHandler = useCallback(
(event: GestureResponderEvent) => {
@@ -98,7 +102,7 @@ function GenericPressable(
const onPressHandler = useCallback(
(event?: GestureResponderEvent | KeyboardEvent) => {
- if (isDisabled) {
+ if (isDisabled || !interactive) {
return;
}
if (!onPress) {
@@ -113,7 +117,7 @@ function GenericPressable(
}
return onPress(event);
},
- [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled],
+ [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled, interactive],
);
const voidOnPressHandler = useCallback(
diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts
index 26a2fea42d94..61cb6db8ee76 100644
--- a/src/components/Pressable/GenericPressable/types.ts
+++ b/src/components/Pressable/GenericPressable/types.ts
@@ -142,6 +142,12 @@ type PressableProps = RNPressableProps &
* Specifies if the pressable responder should be disabled
*/
fullDisabled?: boolean;
+
+ /**
+ * Whether the menu item should be interactive at all
+ * e.g., show disabled cursor when disabled
+ */
+ interactive?: boolean;
};
type PressableRef = ForwardedRef;
diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx
index 86f6c9d8aff8..617811637525 100644
--- a/src/components/Pressable/PressableWithDelayToggle.tsx
+++ b/src/components/Pressable/PressableWithDelayToggle.tsx
@@ -99,7 +99,7 @@ function PressableWithDelayToggle(
return (
{
+ if (nonHeldAmount) {
+ return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount');
+ }
+ return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount});
+ }, [nonHeldAmount, transactionCount, translate, isApprove]);
+
return (
onSubmit(false)}
diff --git a/src/components/QRCode.tsx b/src/components/QRCode.tsx
index 3accb19acfaf..e949049cb942 100644
--- a/src/components/QRCode.tsx
+++ b/src/components/QRCode.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import type {ImageSourcePropType} from 'react-native';
import QRCodeLibrary from 'react-native-qrcode-svg';
-import type {Svg} from 'react-native-svg';
+import type {Svg, SvgProps} from 'react-native-svg';
import useTheme from '@hooks/useTheme';
import CONST from '@src/CONST';
@@ -19,6 +19,22 @@ type QRCodeProps = {
*/
logo?: ImageSourcePropType;
+ /**
+ * If the logo to be displayed in the middle of the QR code is an SVG, then this prop needs to be used
+ * instead of standard `logo`.
+ */
+ svgLogo?: React.FC;
+
+ /**
+ * Background color to be used for logo.
+ */
+ logoBackgroundColor?: string;
+
+ /**
+ * Fill color to be used for logos of type SVG.
+ */
+ svgLogoFillColor?: string;
+
/** The size ratio of logo to QR code */
logoRatio?: QRCodeLogoRatio;
@@ -35,21 +51,36 @@ type QRCodeProps = {
backgroundColor?: string;
/**
- * Function to retrieve the internal component ref and be able to call it's
+ * Function to retrieve the internal component ref and be able to call its
* methods
*/
getRef?: (ref: Svg) => Svg;
};
-function QRCode({url, logo, getRef, size = 120, color, backgroundColor, logoRatio = CONST.QR.DEFAULT_LOGO_SIZE_RATIO, logoMarginRatio = CONST.QR.DEFAULT_LOGO_MARGIN_RATIO}: QRCodeProps) {
+function QRCode({
+ url,
+ logo,
+ svgLogo,
+ svgLogoFillColor,
+ logoBackgroundColor,
+ getRef,
+ size = 120,
+ color,
+ backgroundColor,
+ logoRatio = CONST.QR.DEFAULT_LOGO_SIZE_RATIO,
+ logoMarginRatio = CONST.QR.DEFAULT_LOGO_MARGIN_RATIO,
+}: QRCodeProps) {
const theme = useTheme();
+
return (
) {
+function QRShare({url, title, subtitle, logo, svgLogo, svgLogoFillColor, logoBackgroundColor, logoRatio, logoMarginRatio}: QRShareProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const theme = useTheme();
+ const {isSmallScreenWidth} = useResponsiveLayout();
+ const {windowWidth} = useWindowDimensions();
+ const qrCodeContainerWidth = isSmallScreenWidth ? windowWidth : variables.sideBarWidth;
- const [qrCodeSize, setQrCodeSize] = useState();
+ const [qrCodeSize, setQrCodeSize] = useState(qrCodeContainerWidth - styles.ph5.paddingHorizontal * 2 - variables.qrShareHorizontalPadding * 2);
const svgRef = useRef();
useImperativeHandle(
@@ -48,6 +53,9 @@ function QRShare({url, title, subtitle, logo, logoRatio, logoMarginRatio}: QRSha
(svgRef.current = svg)}
url={url}
+ svgLogo={svgLogo}
+ svgLogoFillColor={svgLogoFillColor}
+ logoBackgroundColor={logoBackgroundColor}
logo={logo}
size={qrCodeSize}
logoRatio={logoRatio}
diff --git a/src/components/QRShare/types.ts b/src/components/QRShare/types.ts
index db9afdf73c2a..8f086e19a51f 100644
--- a/src/components/QRShare/types.ts
+++ b/src/components/QRShare/types.ts
@@ -1,5 +1,6 @@
+import type React from 'react';
import type {ImageSourcePropType} from 'react-native';
-import type {Svg} from 'react-native-svg';
+import type {Svg, SvgProps} from 'react-native-svg';
import type {QRCodeLogoMarginRatio, QRCodeLogoRatio} from '@components/QRCode';
type QRShareProps = {
@@ -18,11 +19,27 @@ type QRShareProps = {
* */
subtitle?: string;
+ /**
+ * If the logo to be displayed in the middle of the QR code is an SVG, then this prop needs to be used
+ * instead of standard `logo`
+ */
+ svgLogo?: React.FC;
+
/**
* The logo which will be display in the middle of the QR code
*/
logo?: ImageSourcePropType;
+ /**
+ * Background color to be used for logo.
+ */
+ logoBackgroundColor?: string;
+
+ /**
+ * Fill color to be used for logos of type SVG
+ */
+ svgLogoFillColor?: string;
+
/**
* The size ratio of logo to QR code
*/
diff --git a/src/components/Reactions/EmojiReactionBubble.tsx b/src/components/Reactions/EmojiReactionBubble.tsx
index 26a14b078b6f..6fa4ee8bb6fb 100644
--- a/src/components/Reactions/EmojiReactionBubble.tsx
+++ b/src/components/Reactions/EmojiReactionBubble.tsx
@@ -7,7 +7,6 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import type {ReactionListEvent} from '@pages/home/ReportScreenContext';
import CONST from '@src/CONST';
-import getEmojiReactionBubbleTextOffsetStyle from './getEmojiReactionBubbleTextOffsetStyle';
type EmojiReactionBubbleProps = {
/**
@@ -83,7 +82,7 @@ function EmojiReactionBubble(
accessible
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
- {emojiCodes.join('')}
+ {emojiCodes.join('')}
{count > 0 && {count} }
);
diff --git a/src/components/Reactions/getEmojiReactionBubbleTextOffsetStyle/index.ios.ts b/src/components/Reactions/getEmojiReactionBubbleTextOffsetStyle/index.ios.ts
deleted file mode 100644
index 9f7fb248a103..000000000000
--- a/src/components/Reactions/getEmojiReactionBubbleTextOffsetStyle/index.ios.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-function getEmojiReactionBubbleTextOffsetStyle() {
- // https://github.com/Expensify/App/issues/36739
- return {transform: [{translateY: 2}]};
-}
-
-export default getEmojiReactionBubbleTextOffsetStyle;
diff --git a/src/components/Reactions/getEmojiReactionBubbleTextOffsetStyle/index.ts b/src/components/Reactions/getEmojiReactionBubbleTextOffsetStyle/index.ts
deleted file mode 100644
index 1e459554789c..000000000000
--- a/src/components/Reactions/getEmojiReactionBubbleTextOffsetStyle/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-function getEmojiReactionBubbleTextOffsetStyle() {
- return {transform: [{translateY: 0}]};
-}
-
-export default getEmojiReactionBubbleTextOffsetStyle;
diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx
index ac1b36c6bf32..bb704def1836 100644
--- a/src/components/ReceiptAudit.tsx
+++ b/src/components/ReceiptAudit.tsx
@@ -7,17 +7,31 @@ import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import Text from './Text';
-function ReceiptAuditHeader({notes, shouldShowAuditMessage}: {notes: string[]; shouldShowAuditMessage: boolean}) {
+type ReceiptAuditProps = {
+ /** List of audit notes */
+ notes: string[];
+
+ /** Whether to show audit result or not (e.g.`Verified`, `Issue Found`) */
+ shouldShowAuditResult: boolean;
+};
+
+function ReceiptAudit({notes, shouldShowAuditResult}: ReceiptAuditProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
- const auditText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('common.verified');
+ let auditText = '';
+ if (notes.length > 0 && shouldShowAuditResult) {
+ auditText = translate('iou.receiptIssuesFound', notes.length);
+ } else if (!notes.length && shouldShowAuditResult) {
+ auditText = translate('common.verified');
+ }
+
return (
{translate('common.receipt')}
- {shouldShowAuditMessage && (
+ {!!auditText && (
<>
{` • ${auditText}`}
{notes.length > 0 && notes.map((message) => {message} )} ;
}
-export {ReceiptAuditHeader, ReceiptAuditMessages};
+export {ReceiptAuditMessages};
+export default ReceiptAudit;
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/ReportActionItem/ExportIntegration.tsx b/src/components/ReportActionItem/ExportIntegration.tsx
new file mode 100644
index 000000000000..78e6170d3dd7
--- /dev/null
+++ b/src/components/ReportActionItem/ExportIntegration.tsx
@@ -0,0 +1,48 @@
+/* eslint-disable react/no-array-index-key */
+import React from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ReportActionUtils from '@libs/ReportActionsUtils';
+import type {ReportAction} from '@src/types/onyx';
+
+type ExportIntegrationProps = {
+ action: OnyxEntry;
+};
+
+function ExportIntegration({action}: ExportIntegrationProps) {
+ const styles = useThemeStyles();
+ const fragments = ReportActionUtils.getExportIntegrationActionFragments(action);
+
+ return (
+
+ {fragments.map((fragment, index) => {
+ if (!fragment.url) {
+ return (
+
+ {fragment.text}{' '}
+
+ );
+ }
+
+ return (
+
+ {fragment.text}{' '}
+
+ );
+ })}
+
+ );
+}
+
+ExportIntegration.displayName = 'ExportIntegration';
+
+export default ExportIntegration;
diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
new file mode 100644
index 000000000000..a13c0a266689
--- /dev/null
+++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
@@ -0,0 +1,130 @@
+import React, {useCallback, useMemo, useState} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
+import type {DropdownOption, ReportExportType} from '@components/ButtonWithDropdownMenu/types';
+import ConfirmModal from '@components/ConfirmModal';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as PolicyActions from '@libs/actions/Policy/Policy';
+import * as ReportActions from '@libs/actions/Report';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import type {ExportType} from '@pages/home/report/ReportDetailsExportPage';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy, Report} from '@src/types/onyx';
+import type {ConnectionName} from '@src/types/onyx/Policy';
+
+type ExportWithDropdownMenuProps = {
+ policy: OnyxEntry;
+
+ report: OnyxEntry;
+
+ connectionName: ConnectionName;
+};
+
+function ExportWithDropdownMenu({policy, report, connectionName}: ExportWithDropdownMenuProps) {
+ const reportID = report?.reportID;
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useResponsiveLayout();
+ const [modalStatus, setModalStatus] = useState(null);
+ const [exportMethods] = useOnyx(ONYXKEYS.LAST_EXPORT_METHOD);
+ const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`);
+
+ const iconToDisplay = ReportUtils.getIntegrationIcon(connectionName);
+ const canBeExported = ReportUtils.canBeExported(report);
+ const isExported = ReportUtils.isExported(reportActions);
+ const hasIntegrationAutoSync = PolicyUtils.hasIntegrationAutoSync(policy, connectionName);
+
+ const dropdownOptions: Array> = useMemo(() => {
+ const optionTemplate = {
+ icon: iconToDisplay,
+ disabled: !canBeExported,
+ displayInDefaultIconColor: true,
+ };
+ const options = [
+ {
+ value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION,
+ text: translate('workspace.common.exportIntegrationSelected', connectionName),
+ ...optionTemplate,
+ },
+ {
+ value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED,
+ text: translate('workspace.common.markAsExported'),
+ ...optionTemplate,
+ },
+ ];
+ const exportMethod = exportMethods?.[report?.policyID ?? ''] ?? null;
+ if (exportMethod) {
+ options.sort((method) => (method.value === exportMethod ? -1 : 0));
+ }
+ return options;
+ // We do not include exportMethods not to re-render the component when the preffered export method changes
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, [canBeExported, iconToDisplay, connectionName, report?.policyID, translate]);
+
+ const confirmExport = useCallback(() => {
+ setModalStatus(null);
+ if (!reportID) {
+ return;
+ }
+ if (modalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
+ ReportActions.exportToIntegration(reportID, connectionName);
+ } else if (modalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
+ ReportActions.markAsManuallyExported(reportID);
+ }
+ }, [connectionName, modalStatus, reportID]);
+
+ const savePreferredExportMethod = (value: ReportExportType) => {
+ if (!report?.policyID) {
+ return;
+ }
+ PolicyActions.savePreferredExportMethod(report?.policyID, value);
+ };
+
+ return (
+ <>
+
+ success={!hasIntegrationAutoSync}
+ pressOnEnter
+ shouldAlwaysShowDropdownMenu
+ anchorAlignment={{
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
+ }}
+ onPress={(_, value) => {
+ if (isExported) {
+ setModalStatus(value);
+ return;
+ }
+ if (!reportID) {
+ return;
+ }
+ if (value === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
+ ReportActions.exportToIntegration(reportID, connectionName);
+ } else if (value === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
+ ReportActions.markAsManuallyExported(reportID);
+ }
+ }}
+ onOptionSelected={({value}) => savePreferredExportMethod(value)}
+ options={dropdownOptions}
+ style={[isSmallScreenWidth && styles.flexGrow1]}
+ buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
+ />
+ setModalStatus(null)}
+ prompt={translate('workspace.exportAgainModal.description', report?.reportName ?? '', connectionName)}
+ confirmText={translate('workspace.exportAgainModal.confirmText')}
+ cancelText={translate('workspace.exportAgainModal.cancelText')}
+ isVisible={!!modalStatus}
+ />
+ >
+ );
+}
+
+export default ExportWithDropdownMenu;
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index a724fd27f134..30cd52135397 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -7,6 +7,7 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import SpacerView from '@components/SpacerView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -27,9 +28,18 @@ type MoneyReportViewProps = {
/** Policy that the report belongs to */
policy: OnyxEntry;
+
+ /** Indicates whether the iou report is a combine report */
+ isCombinedReport?: boolean;
+
+ /** Indicates whether the total should be shown */
+ shouldShowTotal?: boolean;
+
+ /** Flag to show, hide the thread divider line */
+ shouldHideThreadDividerLine: boolean;
};
-function MoneyReportView({report, policy}: MoneyReportViewProps) {
+function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTotal = true, shouldHideThreadDividerLine}: MoneyReportViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -39,7 +49,7 @@ function MoneyReportView({report, policy}: MoneyReportViewProps) {
const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report);
- const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend;
+ const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend && shouldShowTotal;
const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, report.currency);
const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, report.currency);
const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, report.currency);
@@ -57,113 +67,135 @@ function MoneyReportView({report, policy}: MoneyReportViewProps) {
return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight);
}, [policy, report]);
+ const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !ReportUtils.isReportFieldDisabled(report, reportField, policy));
+ const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && ReportUtils.isReportFieldOfTypeTitle(enabledReportFields[0]);
+ const shouldShowReportField =
+ !ReportUtils.isClosedExpenseReportWithNoExpenses(report) && ReportUtils.isPaidGroupPolicyExpenseReport(report) && (!isCombinedReport || !isOnlyTitleFieldEnabled);
+
+ if (!shouldShowReportField && !shouldShowBreakdown && !shouldShowTotal) {
+ return null;
+ }
+
return (
-
-
- {!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && (
- <>
- {ReportUtils.reportFieldsEnabled(report) &&
- sortedPolicyReportFields.map((reportField) => {
- const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField);
- const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue;
- const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
- const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID);
-
- return (
- reportActions.clearReportFieldErrors(report.reportID, reportField)}
- >
- Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '-1', reportField.fieldID))}
- shouldShowRightIcon
- disabled={isFieldDisabled}
- wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
- shouldGreyOutWhenDisabled={false}
- numberOfLinesTitle={0}
- interactive
- shouldStackHorizontally={false}
- onSecondaryInteraction={() => {}}
- hoverAndPressStyle={false}
- titleWithTooltips={[]}
- />
-
- );
- })}
-
-
-
- {translate('common.total')}
-
-
-
- {isSettled && !isPartiallyPaid && (
-
-
-
- )}
-
- {formattedTotalAmount}
-
-
-
- {!!shouldShowBreakdown && (
- <>
-
+ <>
+
+
+ {!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && (
+ <>
+ {ReportUtils.isPaidGroupPolicyExpenseReport(report) &&
+ (!isCombinedReport || !isOnlyTitleFieldEnabled) &&
+ sortedPolicyReportFields.map((reportField) => {
+ if (ReportUtils.isReportFieldOfTypeTitle(reportField)) {
+ return null;
+ }
+
+ const fieldValue = reportField.value ?? reportField.defaultValue;
+ const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
+ const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID);
+
+ return (
+ reportActions.clearReportFieldErrors(report.reportID, reportField)}
+ >
+ Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '-1', reportField.fieldID))}
+ shouldShowRightIcon
+ disabled={isFieldDisabled}
+ wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
+ shouldGreyOutWhenDisabled={false}
+ numberOfLinesTitle={0}
+ interactive
+ shouldStackHorizontally={false}
+ onSecondaryInteraction={() => {}}
+ hoverAndPressStyle={false}
+ titleWithTooltips={[]}
+ />
+
+ );
+ })}
+ {shouldShowTotal && (
+
- {translate('cardTransactions.outOfPocket')}
+ {translate('common.total')}
+ {isSettled && !isPartiallyPaid && (
+
+
+
+ )}
- {formattedOutOfPocketAmount}
+ {formattedTotalAmount}
-
-
-
- {translate('cardTransactions.companySpend')}
-
+ )}
+
+ {!!shouldShowBreakdown && (
+ <>
+
+
+
+ {translate('cardTransactions.outOfPocket')}
+
+
+
+
+ {formattedOutOfPocketAmount}
+
+
-
-
- {formattedCompanySpendAmount}
-
+
+
+
+ {translate('cardTransactions.companySpend')}
+
+
+
+
+ {formattedCompanySpendAmount}
+
+
-
- >
- )}
- >
- )}
-
+ >
+ )}
+ >
+ )}
+
+
+ >
);
}
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index 9e31dc110579..45174c3316be 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -1,9 +1,11 @@
+import type {RouteProp} from '@react-navigation/native';
import {useRoute} from '@react-navigation/native';
import lodashSortBy from 'lodash/sortBy';
import truncate from 'lodash/truncate';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import type {GestureResponderEvent} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import Icon from '@components/Icon';
@@ -27,6 +29,8 @@ import ControlSelection from '@libs/ControlSelection';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as IOUUtils from '@libs/IOUUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -41,6 +45,7 @@ import * as Report from '@userActions/Report';
import * as Transaction from '@userActions/Transaction';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -72,7 +77,7 @@ function MoneyRequestPreviewContent({
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {windowWidth} = useWindowDimensions();
- const route = useRoute();
+ const route = useRoute>();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const sessionAccountID = session?.accountID;
@@ -126,6 +131,9 @@ function MoneyRequestPreviewContent({
const showCashOrCard = isCardTransaction ? translate('iou.card') : translate('iou.cash');
const shouldShowHoldMessage = !(isSettled && !isSettlementOrApprovalPartial) && isOnHold;
+ const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`);
+ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? '');
+ const reviewingTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
// Get transaction violations for given transaction id from onyx, find duplicated transactions violations and get duplicates
const duplicates = useMemo(
() =>
@@ -186,7 +194,9 @@ function MoneyRequestPreviewContent({
}
if (shouldShowRBR && transaction) {
- const violations = TransactionUtils.getTransactionViolations(transaction.transactionID, transactionViolations);
+ const violations = TransactionUtils.getTransactionViolations(transaction.transactionID, transactionViolations)?.sort((a) =>
+ a.type === CONST.VIOLATION_TYPES.VIOLATION ? -1 : 0,
+ );
if (violations?.[0]) {
const violationMessage = ViolationsUtils.getViolationTranslation(violations[0], translate);
const violationsCount = violations.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length;
@@ -195,16 +205,19 @@ function MoneyRequestPreviewContent({
return `${message} ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`;
}
-
- const isMerchantMissing = TransactionUtils.isMerchantMissing(transaction);
- const isAmountMissing = TransactionUtils.isAmountMissing(transaction);
- if (isAmountMissing && isMerchantMissing) {
- message += ` ${CONST.DOT_SEPARATOR} ${translate('violations.reviewRequired')}`;
- } else if (isAmountMissing) {
- message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.missingAmount')}`;
- } else if (isMerchantMissing) {
- message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.missingMerchant')}`;
- } else if (shouldShowHoldMessage) {
+ if (hasFieldErrors) {
+ const isMerchantMissing = TransactionUtils.isMerchantMissing(transaction);
+ const isAmountMissing = TransactionUtils.isAmountMissing(transaction);
+ if (isAmountMissing && isMerchantMissing) {
+ message += ` ${CONST.DOT_SEPARATOR} ${translate('violations.reviewRequired')}`;
+ } else if (isAmountMissing) {
+ message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.missingAmount')}`;
+ } else if (isMerchantMissing) {
+ message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.missingMerchant')}`;
+ }
+ return message;
+ }
+ if (shouldShowHoldMessage) {
message += ` ${CONST.DOT_SEPARATOR} ${translate('violations.hold')}`;
}
} else if (hasNoticeTypeViolations && transaction && !ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) {
@@ -264,6 +277,29 @@ function MoneyRequestPreviewContent({
[shouldShowSplitShare, isPolicyExpenseChat, action.actorAccountID, participantAccountIDs.length, transaction?.comment?.splits, requestAmount, requestCurrency, sessionAccountID],
);
+ const navigateToReviewFields = () => {
+ const comparisonResult = TransactionUtils.compareDuplicateTransactionFields(reviewingTransactionID);
+ const allTransactionIDsDuplicates = [reviewingTransactionID, ...duplicates].filter((id) => id !== transaction?.transactionID);
+ Transaction.setReviewDuplicatesKey({...comparisonResult.keep, duplicates: allTransactionIDsDuplicates, transactionID: transaction?.transactionID ?? ''});
+ if ('merchant' in comparisonResult.change) {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(route.params?.threadReportID));
+ } else if ('category' in comparisonResult.change) {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(route.params?.threadReportID));
+ } else if ('tag' in comparisonResult.change) {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(route.params?.threadReportID));
+ } else if ('description' in comparisonResult.change) {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(route.params?.threadReportID));
+ } else if ('taxCode' in comparisonResult.change) {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE.getRoute(route.params?.threadReportID));
+ } else if ('billable' in comparisonResult.change) {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(route.params?.threadReportID));
+ } else if ('reimbursable' in comparisonResult.change) {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(route.params?.threadReportID));
+ } else {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(route.params?.threadReportID));
+ }
+ };
+
const childContainer = (
{
- Transaction.setReviewDuplicatesKey(transaction?.transactionID ?? '', duplicates);
- }}
+ onPress={navigateToReviewFields}
/>
)}
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index a0d73a1b2844..93f09bcfc9f8 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -7,7 +7,7 @@ import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {useSession} from '@components/OnyxProvider';
-import {ReceiptAuditHeader, ReceiptAuditMessages} from '@components/ReceiptAudit';
+import ReceiptAudit, {ReceiptAuditMessages} from '@components/ReceiptAudit';
import ReceiptEmptyState from '@components/ReceiptEmptyState';
import Switch from '@components/Switch';
import Text from '@components/Text';
@@ -40,6 +40,7 @@ import * as Link from '@userActions/Link';
import * as Transaction from '@userActions/Transaction';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
+import * as Report from '@src/libs/actions/Report';
import * as ReportActions from '@src/libs/actions/ReportActions';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -77,10 +78,16 @@ type MoneyRequestViewOnyxPropsWithoutTransaction = {
type MoneyRequestViewPropsWithoutTransaction = MoneyRequestViewOnyxPropsWithoutTransaction & {
/** The report currently being looked at */
- report: OnyxTypes.Report;
+ report: OnyxEntry;
/** Whether we should display the animated banner above the component */
shouldShowAnimatedBackground: boolean;
+
+ /** Whether we should show Money Request with disabled all fields */
+ readonly?: boolean;
+
+ /** Updated transaction to show in duplicate transaction flow */
+ updatedTransaction?: OnyxEntry;
};
type MoneyRequestViewProps = MoneyRequestViewTransactionOnyxProps & MoneyRequestViewPropsWithoutTransaction;
@@ -108,6 +115,8 @@ function MoneyRequestView({
transactionViolations,
shouldShowAnimatedBackground,
distanceRates,
+ readonly = false,
+ updatedTransaction,
}: MoneyRequestViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -115,8 +124,11 @@ function MoneyRequestView({
const {isOffline} = useNetwork();
const {translate, toLocaleDigit} = useLocalize();
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReport?.parentReportID}`, {
+ selector: (chatReportValue) => chatReportValue && {reportID: chatReportValue.reportID, errorFields: chatReportValue.errorFields},
+ });
- const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '-1'];
+ const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1'];
const isTrackExpense = ReportUtils.isTrackExpenseReport(report);
const {canUseViolations, canUseP2PDistanceRequests} = usePermissions(isTrackExpense ? CONST.IOU.TYPE.TRACK : undefined);
const moneyRequestReport = parentReport;
@@ -144,14 +156,17 @@ function MoneyRequestView({
const isInvoice = ReportUtils.isInvoiceReport(moneyRequestReport);
const isPaidReport = ReportActionsUtils.isPayAction(parentReportAction);
const taxRates = policy?.taxRates;
- const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency);
+
+ const formattedTaxAmount = updatedTransaction?.taxAmount
+ ? CurrencyUtils.convertToDisplayString(updatedTransaction?.taxAmount, transactionCurrency)
+ : CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency);
const taxRatesDescription = taxRates?.name;
- const taxRateTitle = TransactionUtils.getTaxName(policy, transaction);
+ const taxRateTitle = updatedTransaction ? TransactionUtils.getTaxName(policy, updatedTransaction) : TransactionUtils.getTaxName(policy, transaction);
// Flags for allowing or disallowing editing an expense
const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID);
- const isCancelled = moneyRequestReport && moneyRequestReport.isCancelledIOU;
+ const isCancelled = moneyRequestReport && moneyRequestReport?.isCancelledIOU;
// Used for non-restricted fields such as: description, category, tag, billable, etc.
const canEdit = ReportActionsUtils.isMoneyRequestAction(parentReportAction) && ReportUtils.canEditMoneyRequest(parentReportAction);
@@ -163,7 +178,7 @@ function MoneyRequestView({
const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT);
const hasReceipt = TransactionUtils.hasReceipt(transaction);
const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction);
- const didRceiptScanSucceed = hasReceipt && TransactionUtils.didRceiptScanSucceed(transaction);
+ const didReceiptScanSucceed = hasReceipt && TransactionUtils.didReceiptScanSucceed(transaction);
const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE);
const isAdmin = policy?.role === 'admin';
@@ -189,7 +204,7 @@ function MoneyRequestView({
const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport);
const shouldShowViewTripDetails = TransactionUtils.hasReservationList(transaction) && !!tripID;
- const {getViolationsForField} = useViolations(transactionViolations ?? []);
+ const {getViolationsForField} = useViolations(transactionViolations ?? [], isReceiptBeingScanned || !ReportUtils.isPaidGroupPolicy(report));
const hasViolations = useCallback(
(field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string): boolean =>
!!canUseViolations && getViolationsForField(field, data, policyHasDependentTags, tagValue).length > 0,
@@ -216,15 +231,14 @@ function MoneyRequestView({
merchantTitle = translate('iou.receiptStatusTitle');
amountTitle = translate('iou.receiptStatusTitle');
}
+
const saveBillable = useCallback(
(newBillable: boolean) => {
// If the value hasn't changed, don't request to save changes on the server and just close the modal
if (newBillable === TransactionUtils.getBillable(transaction)) {
- Navigation.dismissModal();
return;
}
- IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '-1', report?.reportID, newBillable, policy, policyTagList, policyCategories);
- Navigation.dismissModal();
+ IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '-1', report?.reportID ?? '-1', newBillable, policy, policyTagList, policyCategories);
},
[transaction, report, policy, policyTagList, policyCategories],
);
@@ -254,7 +268,6 @@ function MoneyRequestView({
if (hasReceipt) {
receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction);
}
-
const pendingAction = transaction?.pendingAction;
const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => transaction?.pendingFields?.[fieldPath] ?? pendingAction;
@@ -279,6 +292,10 @@ function MoneyRequestView({
const {isError, translationPath} = fieldChecks[field] ?? {};
+ if (readonly) {
+ return '';
+ }
+
// Return form errors if there are any
if (hasErrors && isError && translationPath) {
return translate(translationPath);
@@ -292,7 +309,7 @@ function MoneyRequestView({
return '';
},
- [transactionAmount, isSettled, isCancelled, isPolicyExpenseChat, isEmptyMerchant, transactionDate, hasErrors, hasViolations, translate, getViolationsForField],
+ [transactionAmount, isSettled, isCancelled, isPolicyExpenseChat, isEmptyMerchant, transactionDate, readonly, hasErrors, hasViolations, translate, getViolationsForField],
);
const distanceRequestFields = canUseP2PDistanceRequests ? (
@@ -304,7 +321,9 @@ function MoneyRequestView({
interactive={canEditDistance}
shouldShowRightIcon={canEditDistance}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))}
+ onPress={() =>
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ }
/>
{/* TODO: correct the pending field action https://github.com/Expensify/App/issues/36987 */}
@@ -329,7 +348,7 @@ function MoneyRequestView({
interactive={canEditDistance}
shouldShowRightIcon={canEditDistance}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))}
+ onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))}
/>
);
@@ -340,14 +359,17 @@ function MoneyRequestView({
const receiptViolationNames: OnyxTypes.ViolationName[] = [
CONST.VIOLATIONS.RECEIPT_REQUIRED,
CONST.VIOLATIONS.RECEIPT_NOT_SMART_SCANNED,
- CONST.VIOLATIONS.MODIFIED_DATE,
CONST.VIOLATIONS.CASH_EXPENSE_WITH_NO_RECEIPT,
CONST.VIOLATIONS.SMARTSCAN_FAILED,
];
const receiptViolations =
transactionViolations?.filter((violation) => receiptViolationNames.includes(violation.name)).map((violation) => ViolationsUtils.getViolationTranslation(violation, translate)) ?? [];
- const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report);
- const shouldShowReceiptHeader = isReceiptAllowed && (shouldShowReceiptEmptyState || hasReceipt);
+
+ // Whether to show receipt audit result (e.g.`Verified`, `Issue Found`) and messages (e.g. `Receipt not verified. Please confirm accuracy.`)
+ // `!!(receiptViolations.length || didReceiptScanSucceed)` is for not showing `Verified` when `receiptViolations` is empty and `didReceiptScanSucceed` is false.
+ const shouldShowAuditMessage =
+ !isReceiptBeingScanned && hasReceipt && !!(receiptViolations.length || didReceiptScanSucceed) && !!canUseViolations && ReportUtils.isPaidGroupPolicy(report);
+ const shouldShowReceiptAudit = isReceiptAllowed && (shouldShowReceiptEmptyState || hasReceipt);
const errors = {
...(transaction?.errorFields?.route ?? transaction?.errors),
@@ -362,7 +384,7 @@ function MoneyRequestView({
tagListName: name,
},
PolicyUtils.hasDependentTags(policy, policyTagList),
- TransactionUtils.getTagForDisplay(transaction, index),
+ TransactionUtils.getTagForDisplay(updatedTransaction ?? transaction, index),
);
return (
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report.reportID))
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report?.reportID ?? '-1'))
}
brickRoadIndicator={tagError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={tagError}
@@ -389,10 +411,10 @@ function MoneyRequestView({
{shouldShowAnimatedBackground && }
<>
- {shouldShowReceiptHeader && (
-
)}
{(hasReceipt || errors) && (
@@ -404,14 +426,25 @@ function MoneyRequestView({
if (!transaction?.transactionID) {
return;
}
- if (
- transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD &&
- Object.values(transaction?.errors ?? {})?.find((error) => ErrorUtils.isReceiptError(error))
- ) {
- deleteTransaction(parentReport, parentReportAction);
+
+ const isCreateChatErrored = !!report?.errorFields?.createChat;
+ if ((isCreateChatErrored || !!report?.isOptimisticReport) && parentReportAction) {
+ const urlToNavigateBack = IOU.cleanUpMoneyRequest(transaction.transactionID, parentReportAction, true);
+ Navigation.goBack(urlToNavigateBack);
+ return;
+ }
+
+ if (transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) {
+ if (chatReport?.reportID && ReportUtils.getAddWorkspaceRoomOrChatReportErrors(chatReport)) {
+ Report.navigateToConciergeChatAndDeleteReport(chatReport.reportID, true, true);
+ return;
+ }
+ if (Object.values(transaction?.errors ?? {})?.find((error) => ErrorUtils.isReceiptError(error))) {
+ deleteTransaction(parentReport, parentReportAction);
+ }
}
Transaction.clearError(transaction.transactionID);
- ReportActions.clearAllRelatedReportActionErrors(report.reportID, parentReportAction);
+ ReportActions.clearAllRelatedReportActionErrors(report?.reportID ?? '-1', parentReportAction);
}}
>
{hasReceipt && (
@@ -440,7 +473,7 @@ function MoneyRequestView({
CONST.IOU.ACTION.EDIT,
iouType,
transaction?.transactionID ?? '-1',
- report.reportID,
+ report?.reportID ?? '-1',
Navigation.getActiveRouteWithoutParams(),
),
)
@@ -448,7 +481,7 @@ function MoneyRequestView({
/>
)}
{!shouldShowReceiptEmptyState && !hasReceipt && }
- {shouldShowNotesViolations && }
+ {shouldShowAuditMessage && }
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))}
+ onPress={() =>
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ }
brickRoadIndicator={getErrorForField('amount') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('amount')}
/>
@@ -467,12 +502,12 @@ function MoneyRequestView({
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
brickRoadIndicator={getErrorForField('comment') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
@@ -486,12 +521,12 @@ function MoneyRequestView({
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
}
wrapperStyle={[styles.taskDescriptionMenuItem]}
brickRoadIndicator={getErrorForField('merchant') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
@@ -504,10 +539,12 @@ function MoneyRequestView({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))}
+ onPress={() =>
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1' ?? '-1'))
+ }
brickRoadIndicator={getErrorForField('date') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('date')}
/>
@@ -516,12 +553,12 @@ function MoneyRequestView({
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
}
brickRoadIndicator={getErrorForField('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('category')}
@@ -544,28 +581,29 @@ function MoneyRequestView({
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
}
brickRoadIndicator={getErrorForField('tax') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('tax')}
/>
)}
-
{shouldShowTax && (
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report.reportID))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'),
+ )
}
/>
@@ -596,7 +634,7 @@ function MoneyRequestView({
accessibilityLabel={translate('common.billable')}
isOn={!!transactionBillable}
onToggle={saveBillable}
- disabled={!canEdit}
+ disabled={!canEdit || readonly}
/>
)}
@@ -609,30 +647,30 @@ MoneyRequestView.displayName = 'MoneyRequestView';
export default withOnyx({
policy: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`,
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`,
},
policyCategories: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report.policyID}`,
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID ?? ''}`,
},
policyTagList: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`,
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID ?? ''}`,
},
parentReport: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`,
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? ''}`,
},
parentReportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '-1'}`,
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report?.parentReportID : '-1'}`,
canEvict: false,
},
distanceRates: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`,
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`,
selector: (policy: OnyxEntry) => DistanceRequestUtils.getMileageRates(policy, true),
},
})(
withOnyx({
transaction: {
key: ({report, parentReportActions}) => {
- const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '-1'];
+ const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1'];
const originalMessage =
parentReportAction && ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction) : undefined;
const transactionID = originalMessage?.IOUTransactionID ?? -1;
@@ -641,7 +679,7 @@ export default withOnyx {
- const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '-1'];
+ const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1'];
const originalMessage =
parentReportAction && ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction) : undefined;
const transactionID = originalMessage?.IOUTransactionID ?? -1;
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index c796a267fd01..ae6ace23c64e 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -24,6 +24,7 @@ import ControlSelection from '@libs/ControlSelection';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -38,6 +39,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy, Report, ReportAction, Transaction, TransactionViolations, UserWallet} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+import ExportWithDropdownMenu from './ExportWithDropdownMenu';
import type {PendingMessageProps} from './MoneyRequestPreview/types';
import ReportActionItemImages from './ReportActionItemImages';
@@ -121,13 +123,14 @@ function ReportPreview({
hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(iouReportID),
}),
// When transactions get updated these status may have changed, so that is a case where we also want to run this.
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[transactions, iouReportID, action],
);
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [requestType, setRequestType] = useState();
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy);
+ const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID ?? '');
const {isSmallScreenWidth} = useWindowDimensions();
const [paymentType, setPaymentType] = useState();
@@ -203,6 +206,18 @@ function ReportPreview({
}
};
+ const getSettlementAmount = () => {
+ if (hasOnlyHeldExpenses) {
+ return '';
+ }
+
+ if (ReportUtils.hasHeldExpenses(iouReport?.reportID) && canAllowSettlement) {
+ return nonHeldAmount;
+ }
+
+ return CurrencyUtils.convertToDisplayString(reimbursableSpend, iouReport?.currency);
+ };
+
const getDisplayAmount = (): string => {
if (totalDisplaySpend) {
return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency);
@@ -321,6 +336,14 @@ function ReportPreview({
};
}, [formattedMerchant, formattedDescription, moneyRequestComment, translate, numberOfRequests, numberOfScanningReceipts, numberOfPendingRequests]);
+ /*
+ * Manual export
+ */
+ const connectedIntegration = PolicyUtils.getConnectedIntegration(policy);
+
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+ const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin;
+
return (
- {shouldShowSettlementButton && (
+ {shouldShowSettlementButton && !shouldShowExportIntegrationButton && (
)}
+ {shouldShowExportIntegrationButton && (
+
+ )}
{shouldShowSubmitButton && (
{isHoldMenuVisible && iouReport && requestType !== undefined && (
)}
diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx
index 43e896fe6578..941e7cd5c610 100644
--- a/src/components/ReportActionItem/TaskView.tsx
+++ b/src/components/ReportActionItem/TaskView.tsx
@@ -22,6 +22,7 @@ import getButtonState from '@libs/getButtonState';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
+import * as TaskUtils from '@libs/TaskUtils';
import * as Session from '@userActions/Session';
import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
@@ -96,6 +97,10 @@ function TaskView({report, ...props}: TaskViewProps) {
{
+ // If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page.
+ if (TaskUtils.isActiveTaskEditRoute(report.reportID)) {
+ return;
+ }
if (isCompleted) {
Task.reopenTask(report);
} else {
diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx
index a5c46b82d94a..96f705ea2d52 100644
--- a/src/components/ReportWelcomeText.tsx
+++ b/src/components/ReportWelcomeText.tsx
@@ -7,6 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
+import SidebarUtils from '@libs/SidebarUtils';
import CONST from '@src/CONST';
import type {IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -42,7 +43,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report);
const isMultipleParticipant = participantAccountIDs.length > 1;
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant);
- const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report);
+ const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy);
const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, policy, participantAccountIDs);
const additionalText = moneyRequestOptions
.filter((item): item is Exclude => item !== CONST.IOU.TYPE.INVOICE)
@@ -86,47 +87,47 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
{isPolicyExpenseChat &&
- (policy?.description ? (
+ (welcomeMessage?.messageHtml ? (
{
if (!canEditPolicyDescription) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy.id));
+ Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id ?? '-1'));
}}
style={[styles.renderHTML, canEditPolicyDescription ? styles.cursorPointer : styles.cursorText]}
accessibilityLabel={translate('reportDescriptionPage.roomDescription')}
>
-
+
) : (
- {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartOne')}
+ {welcomeMessage.phrase1}
{ReportUtils.getDisplayNameForParticipant(report?.ownerAccountID)}
- {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo')}
+ {welcomeMessage.phrase2}
{ReportUtils.getPolicyName(report)}
- {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree')}
+ {welcomeMessage.phrase3}
))}
{isChatRoom &&
- (report?.description ? (
+ (welcomeMessage?.messageHtml ? (
{
if (ReportUtils.canEditReportDescription(report, policy)) {
- Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID ?? '-1'));
return;
}
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '-1'));
}}
style={styles.renderHTML}
accessibilityLabel={translate('reportDescriptionPage.roomDescription')}
>
-
+
) : (
- {roomWelcomeMessage.phrase1}
- {roomWelcomeMessage.showReportName && (
+ {welcomeMessage.phrase1}
+ {welcomeMessage.showReportName && (
)}
- {roomWelcomeMessage.phrase2 !== undefined && {roomWelcomeMessage.phrase2} }
+ {welcomeMessage.phrase2 !== undefined && {welcomeMessage.phrase2} }
))}
{isSelfDM && (
- {translate('reportActionsView.beginningOfChatHistorySelfDM')}
+ {welcomeMessage.phrase1}
)}
{isSystemChat && (
- {translate('reportActionsView.beginningOfChatHistorySystemDM')}
+ {welcomeMessage.phrase1}
)}
{isDefault && (
- {translate('reportActionsView.beginningOfChatHistory')}
+ {welcomeMessage.phrase1}
{displayNamesWithTooltips.map(({displayName, accountID}, index) => (
// eslint-disable-next-line react/no-array-index-key
diff --git a/src/components/RequireTwoFactorAuthenticationModal.tsx b/src/components/RequireTwoFactorAuthenticationModal.tsx
new file mode 100644
index 000000000000..229231e8ff25
--- /dev/null
+++ b/src/components/RequireTwoFactorAuthenticationModal.tsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import {View} from 'react-native';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import Button from './Button';
+import Lottie from './Lottie';
+import LottieAnimations from './LottieAnimations';
+import Modal from './Modal';
+import SafeAreaConsumer from './SafeAreaConsumer';
+import Text from './Text';
+
+type RequireTwoFactorAuthenticationModalProps = {
+ /** A callback to call when the form has been submitted */
+ onSubmit: () => void;
+
+ /** A callback to call when the form has been closed */
+ onCancel?: () => void;
+
+ /** Modal visibility */
+ isVisible: boolean;
+
+ /** Describe what is showing */
+ description: string;
+
+ /**
+ * Whether the modal should enable the new focus manager.
+ * We are attempting to migrate to a new refocus manager, adding this property for gradual migration.
+ * */
+ shouldEnableNewFocusManagement?: boolean;
+};
+
+function RequireTwoFactorAuthenticationModal({onCancel = () => {}, description, isVisible, onSubmit, shouldEnableNewFocusManagement}: RequireTwoFactorAuthenticationModalProps) {
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const StyleUtils = useStyleUtils();
+
+ return (
+
+ {({safeAreaPaddingBottomStyle}) => (
+
+
+
+
+
+
+
+ {translate('twoFactorAuth.pleaseEnableTwoFactorAuth')}
+ {description}
+
+
+
+
+
+ )}
+
+ );
+}
+
+RequireTwoFactorAuthenticationModal.displayName = 'RequireTwoFactorAuthenticationModal';
+
+export default RequireTwoFactorAuthenticationModal;
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index 1d5d65d9874d..f845cfda3638 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -211,7 +211,7 @@ function ScreenWrapper(
}
};
// Rule disabled because this effect is only for component did mount & will component unmount lifecycle event
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
const isAvoidingViewportScroll = useTackInputFocus(shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit());
diff --git a/src/components/Search/SearchActionOptionsUtils.tsx b/src/components/Search/SearchActionOptionsUtils.tsx
new file mode 100644
index 000000000000..20601abd3696
--- /dev/null
+++ b/src/components/Search/SearchActionOptionsUtils.tsx
@@ -0,0 +1,16 @@
+import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
+import * as Expensicons from '@components/Icon/Expensicons';
+import CONST from '@src/CONST';
+import type {SearchHeaderOptionValue} from './SearchPageHeader';
+
+function getDownloadOption(text: string, onSelected?: () => void): DropdownOption {
+ return {
+ icon: Expensicons.Download,
+ text,
+ value: CONST.SEARCH.BULK_ACTION_TYPES.EXPORT,
+ shouldCloseModalOnSelect: true,
+ onSelected,
+ };
+}
+
+export default getDownloadOption;
diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx
new file mode 100644
index 000000000000..367a03e081cd
--- /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/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx
new file mode 100644
index 000000000000..bd4b843bbd60
--- /dev/null
+++ b/src/components/Search/SearchListWithHeader.tsx
@@ -0,0 +1,257 @@
+import type {ForwardedRef} from 'react';
+import React, {forwardRef, useCallback, useEffect, useMemo, useState} from 'react';
+import ConfirmModal from '@components/ConfirmModal';
+import DecisionModal from '@components/DecisionModal';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
+import Modal from '@components/Modal';
+import SelectionList from '@components/SelectionList';
+import type {BaseSelectionListProps, ReportListItemType, SelectionListHandle, TransactionListItemType} from '@components/SelectionList/types';
+import useLocalize from '@hooks/useLocalize';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as SearchActions from '@libs/actions/Search';
+import * as SearchUtils from '@libs/SearchUtils';
+import CONST from '@src/CONST';
+import type {SearchDataTypes, SearchQuery, SearchReport} from '@src/types/onyx/SearchResults';
+import SearchPageHeader from './SearchPageHeader';
+import type {SelectedTransactionInfo, SelectedTransactions} from './types';
+
+type SearchListWithHeaderProps = Omit, 'onSelectAll' | 'onCheckboxPress' | 'sections'> & {
+ query: SearchQuery;
+ hash: number;
+ data: TransactionListItemType[] | ReportListItemType[];
+ searchType: SearchDataTypes;
+ isMobileSelectionModeActive?: boolean;
+ setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void;
+};
+
+function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [string, SelectedTransactionInfo] {
+ return [item.keyForList, {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}];
+}
+
+function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions) {
+ return {...item, isSelected: !!selectedTransactions[item.keyForList]?.isSelected};
+}
+
+function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType, selectedTransactions: SelectedTransactions) {
+ return SearchUtils.isTransactionListItemType(item)
+ ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions)
+ : {
+ ...item,
+ transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions)),
+ isSelected: item.transactions.every((transaction) => !!selectedTransactions[transaction.keyForList]?.isSelected),
+ };
+}
+
+function SearchListWithHeader(
+ {ListItem, onSelectRow, query, hash, data, searchType, isMobileSelectionModeActive, setIsMobileSelectionModeActive, ...props}: SearchListWithHeaderProps,
+ ref: ForwardedRef,
+) {
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const {translate} = useLocalize();
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [longPressedItem, setLongPressedItem] = useState(null);
+ const [selectedTransactions, setSelectedTransactions] = useState({});
+ const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState([]);
+ const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false);
+ const [offlineModalVisible, setOfflineModalVisible] = useState(false);
+ const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
+
+ const selectedReports: Array = useMemo(() => {
+ if (searchType !== CONST.SEARCH.DATA_TYPES.REPORT) {
+ return [];
+ }
+
+ return data
+ .filter(
+ (item) => !SearchUtils.isTransactionListItemType(item) && item.reportID && item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected),
+ )
+ .map((item) => item.reportID);
+ }, [selectedTransactions, data, searchType]);
+
+ const handleOnSelectDeleteOption = (itemsToDelete: string[]) => {
+ setSelectedTransactionsToDelete(itemsToDelete);
+ setDeleteExpensesConfirmModalVisible(true);
+ };
+
+ const handleOnCancelConfirmModal = () => {
+ setSelectedTransactionsToDelete([]);
+ setDeleteExpensesConfirmModalVisible(false);
+ };
+
+ const clearSelectedItems = () => setSelectedTransactions({});
+
+ const handleDeleteExpenses = () => {
+ if (selectedTransactionsToDelete.length === 0) {
+ return;
+ }
+
+ clearSelectedItems();
+ setDeleteExpensesConfirmModalVisible(false);
+ SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsToDelete);
+ };
+
+ useEffect(() => {
+ clearSelectedItems();
+ }, [hash]);
+
+ const toggleTransaction = useCallback(
+ (item: TransactionListItemType | ReportListItemType) => {
+ if (SearchUtils.isTransactionListItemType(item)) {
+ if (!item.keyForList) {
+ return;
+ }
+
+ setSelectedTransactions((prev) => {
+ if (prev[item.keyForList]?.isSelected) {
+ const {[item.keyForList]: omittedTransaction, ...transactions} = prev;
+ return transactions;
+ }
+ return {...prev, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}};
+ });
+
+ return;
+ }
+
+ if (item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)) {
+ const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions};
+
+ item.transactions.forEach((transaction) => {
+ delete reducedSelectedTransactions[transaction.keyForList];
+ });
+
+ setSelectedTransactions(reducedSelectedTransactions);
+ return;
+ }
+
+ setSelectedTransactions({
+ ...selectedTransactions,
+ ...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)),
+ });
+ },
+ [selectedTransactions],
+ );
+
+ const openBottomModal = (item: TransactionListItemType | ReportListItemType | null) => {
+ if (!isSmallScreenWidth) {
+ return;
+ }
+
+ setLongPressedItem(item);
+ setIsModalVisible(true);
+ };
+
+ const turnOnSelectionMode = useCallback(() => {
+ setIsMobileSelectionModeActive?.(true);
+ setIsModalVisible(false);
+
+ if (longPressedItem) {
+ toggleTransaction(longPressedItem);
+ }
+ }, [longPressedItem, setIsMobileSelectionModeActive, toggleTransaction]);
+
+ const closeBottomModal = useCallback(() => {
+ setIsModalVisible(false);
+ }, []);
+
+ useEffect(() => {
+ if (isMobileSelectionModeActive) {
+ return;
+ }
+
+ setSelectedTransactions({});
+ }, [setSelectedTransactions, isMobileSelectionModeActive]);
+
+ const toggleAllTransactions = () => {
+ const areItemsOfReportType = searchType === CONST.SEARCH.DATA_TYPES.REPORT;
+ const flattenedItems = areItemsOfReportType ? (data as ReportListItemType[]).flatMap((item) => item.transactions) : data;
+ const isAllSelected = flattenedItems.length === Object.keys(selectedTransactions).length;
+
+ if (isAllSelected) {
+ clearSelectedItems();
+ return;
+ }
+
+ if (areItemsOfReportType) {
+ setSelectedTransactions(Object.fromEntries((data as ReportListItemType[]).flatMap((item) => item.transactions.map(mapTransactionItemToSelectedEntry))));
+
+ return;
+ }
+
+ setSelectedTransactions(Object.fromEntries((data as TransactionListItemType[]).map(mapTransactionItemToSelectedEntry)));
+ };
+
+ const sortedSelectedData = useMemo(() => data.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions)), [data, selectedTransactions]);
+
+ return (
+ <>
+ setOfflineModalVisible(true)}
+ setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)}
+ />
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...props}
+ sections={[{data: sortedSelectedData, isDisabled: false}]}
+ ListItem={ListItem}
+ onSelectRow={onSelectRow}
+ onLongPressRow={openBottomModal}
+ ref={ref}
+ onCheckboxPress={toggleTransaction}
+ onSelectAll={toggleAllTransactions}
+ isMobileSelectionModeActive={isMobileSelectionModeActive}
+ />
+
+ setOfflineModalVisible(false)}
+ secondOptionText={translate('common.buttonConfirm')}
+ isVisible={offlineModalVisible}
+ onClose={() => setOfflineModalVisible(false)}
+ />
+ setDownloadErrorModalVisible(false)}
+ secondOptionText={translate('common.buttonConfirm')}
+ isVisible={downloadErrorModalVisible}
+ onClose={() => setDownloadErrorModalVisible(false)}
+ />
+
+
+
+ >
+ );
+}
+
+SearchListWithHeader.displayName = 'SearchListWithHeader';
+
+export default forwardRef(SearchListWithHeader);
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
new file mode 100644
index 000000000000..bd1e053b1c7e
--- /dev/null
+++ b/src/components/Search/SearchPageHeader.tsx
@@ -0,0 +1,236 @@
+import React, {useMemo} from 'react';
+import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
+import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import * as Illustrations from '@components/Icon/Illustrations';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as SearchActions from '@libs/actions/Search';
+import Navigation from '@libs/Navigation/Navigation';
+import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type {SearchQuery, SearchReport} from '@src/types/onyx/SearchResults';
+import type DeepValueOf from '@src/types/utils/DeepValueOf';
+import type IconAsset from '@src/types/utils/IconAsset';
+import getDownloadOption from './SearchActionOptionsUtils';
+import {useSearchContext} from './SearchContext';
+import type {SelectedTransactions} from './types';
+
+type SearchPageHeaderProps = {
+ query: SearchQuery;
+ selectedTransactions?: SelectedTransactions;
+ selectedReports?: Array;
+ clearSelectedItems?: () => void;
+ hash: number;
+ onSelectDeleteOption?: (itemsToDelete: string[]) => void;
+ isMobileSelectionModeActive?: boolean;
+ setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void;
+ setOfflineModalOpen?: () => void;
+ setDownloadErrorModalOpen?: () => void;
+};
+
+type SearchHeaderOptionValue = DeepValueOf | undefined;
+
+function SearchPageHeader({
+ query,
+ selectedTransactions = {},
+ hash,
+ clearSelectedItems,
+ onSelectDeleteOption,
+ isMobileSelectionModeActive,
+ setIsMobileSelectionModeActive,
+ setOfflineModalOpen,
+ setDownloadErrorModalOpen,
+ selectedReports,
+}: SearchPageHeaderProps) {
+ const {translate} = useLocalize();
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
+ const {activeWorkspaceID} = useActiveWorkspace();
+ const {isSmallScreenWidth} = useResponsiveLayout();
+ const {setSelectedTransactionIDs} = useSearchContext();
+
+ const headerContent: {[key in SearchQuery]: {icon: IconAsset; title: string}} = {
+ all: {icon: Illustrations.MoneyReceipts, title: translate('common.expenses')},
+ shared: {icon: Illustrations.SendMoney, title: translate('common.shared')},
+ drafts: {icon: Illustrations.Pencil, title: translate('common.drafts')},
+ finished: {icon: Illustrations.CheckmarkCircle, title: translate('common.finished')},
+ };
+
+ const selectedTransactionsKeys = Object.keys(selectedTransactions ?? []);
+
+ const headerButtonsOptions = useMemo(() => {
+ if (selectedTransactionsKeys.length === 0) {
+ return [];
+ }
+
+ const options: Array> = [];
+
+ // Because of some problems with the lib we use for download on native we are only enabling download for web, we should remove the SearchActionOptionsUtils files when https://github.com/Expensify/App/issues/45511 is done
+ const downloadOption = getDownloadOption(translate('common.download'), () => {
+ if (isOffline) {
+ setOfflineModalOpen?.();
+ return;
+ }
+
+ SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedTransactionsKeys, [activeWorkspaceID ?? ''], () => {
+ setDownloadErrorModalOpen?.();
+ });
+ });
+
+ if (downloadOption) {
+ options.push(downloadOption);
+ }
+
+ const shouldShowHoldOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canHold);
+
+ if (shouldShowHoldOption) {
+ options.push({
+ icon: Expensicons.Stopwatch,
+ text: translate('search.bulkActions.hold'),
+ value: CONST.SEARCH.BULK_ACTION_TYPES.HOLD,
+ shouldCloseModalOnSelect: true,
+ onSelected: () => {
+ if (isOffline) {
+ setOfflineModalOpen?.();
+ return;
+ }
+
+ clearSelectedItems?.();
+ if (isMobileSelectionModeActive) {
+ setIsMobileSelectionModeActive?.(false);
+ }
+ setSelectedTransactionIDs(selectedTransactionsKeys);
+ Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(query));
+ },
+ });
+ }
+
+ const shouldShowUnholdOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canUnhold);
+
+ if (shouldShowUnholdOption) {
+ options.push({
+ icon: Expensicons.Stopwatch,
+ text: translate('search.bulkActions.unhold'),
+ value: CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD,
+ shouldCloseModalOnSelect: true,
+ onSelected: () => {
+ if (isOffline) {
+ setOfflineModalOpen?.();
+ return;
+ }
+
+ clearSelectedItems?.();
+ if (isMobileSelectionModeActive) {
+ setIsMobileSelectionModeActive?.(false);
+ }
+ SearchActions.unholdMoneyRequestOnSearch(hash, selectedTransactionsKeys);
+ },
+ });
+ }
+
+ const shouldShowDeleteOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canDelete);
+
+ if (shouldShowDeleteOption) {
+ options.push({
+ icon: Expensicons.Trashcan,
+ text: translate('search.bulkActions.delete'),
+ value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE,
+ shouldCloseModalOnSelect: true,
+ onSelected: () => {
+ if (isOffline) {
+ setOfflineModalOpen?.();
+ return;
+ }
+
+ onSelectDeleteOption?.(selectedTransactionsKeys);
+ },
+ });
+ }
+
+ if (options.length === 0) {
+ const emptyOptionStyle = {
+ interactive: false,
+ iconFill: theme.icon,
+ iconHeight: variables.iconSizeLarge,
+ iconWidth: variables.iconSizeLarge,
+ numberOfLinesTitle: 2,
+ titleStyle: {...styles.colorMuted, ...styles.fontWeightNormal, ...styles.textWrap},
+ };
+
+ options.push({
+ icon: Expensicons.Exclamation,
+ text: translate('search.bulkActions.noOptionsAvailable'),
+ value: undefined,
+ ...emptyOptionStyle,
+ });
+ }
+
+ return options;
+ }, [
+ selectedTransactionsKeys,
+ selectedTransactions,
+ translate,
+ onSelectDeleteOption,
+ clearSelectedItems,
+ isMobileSelectionModeActive,
+ hash,
+ setIsMobileSelectionModeActive,
+ theme.icon,
+ styles.colorMuted,
+ styles.fontWeightNormal,
+ query,
+ isOffline,
+ setOfflineModalOpen,
+ setDownloadErrorModalOpen,
+ activeWorkspaceID,
+ selectedReports,
+ styles.textWrap,
+ setSelectedTransactionIDs,
+ ]);
+
+ if (isSmallScreenWidth) {
+ if (isMobileSelectionModeActive) {
+ return (
+
+ );
+ }
+ return null;
+ }
+
+ return (
+
+ {headerButtonsOptions.length > 0 && (
+ null}
+ shouldAlwaysShowDropdownMenu
+ pressOnEnter
+ buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
+ customText={translate('workspace.common.selected', {selectedNumber: selectedTransactionsKeys.length})}
+ options={headerButtonsOptions}
+ isSplitButton={false}
+ />
+ )}
+
+ );
+}
+
+SearchPageHeader.displayName = 'SearchPageHeader';
+
+export type {SearchHeaderOptionValue};
+export default SearchPageHeader;
diff --git a/src/components/Search.tsx b/src/components/Search/index.tsx
similarity index 61%
rename from src/components/Search.tsx
rename to src/components/Search/index.tsx
index 714993204afb..6b47364345c7 100644
--- a/src/components/Search.tsx
+++ b/src/components/Search/index.tsx
@@ -1,8 +1,12 @@
import {useNavigation} from '@react-navigation/native';
import type {StackNavigationProp} from '@react-navigation/stack';
+import lodashMemoize from 'lodash/memoize';
import React, {useCallback, useEffect, useRef} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
+import SearchTableHeader from '@components/SelectionList/SearchTableHeader';
+import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
+import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -11,7 +15,6 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import * as ReportUtils from '@libs/ReportUtils';
import * as SearchUtils from '@libs/SearchUtils';
-import type {SearchColumnType, SortOrder} from '@libs/SearchUtils';
import Navigation from '@navigation/Navigation';
import type {AuthScreensParamList} from '@navigation/types';
import EmptySearchView from '@pages/Search/EmptySearchView';
@@ -19,20 +22,20 @@ import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {SearchQuery} from '@src/types/onyx/SearchResults';
import type SearchResults from '@src/types/onyx/SearchResults';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
-import SelectionList from './SelectionList';
-import SearchTableHeader from './SelectionList/SearchTableHeader';
-import type {ReportListItemType, TransactionListItemType} from './SelectionList/types';
-import TableListItemSkeleton from './Skeletons/TableListItemSkeleton';
+import type {SearchDataTypes, SearchQuery} from '@src/types/onyx/SearchResults';
+import {useSearchContext} from './SearchContext';
+import SearchListWithHeader from './SearchListWithHeader';
+import SearchPageHeader from './SearchPageHeader';
+import type {SearchColumnType, SortOrder} from './types';
type SearchProps = {
query: SearchQuery;
policyIDs?: string;
sortBy?: SearchColumnType;
sortOrder?: SortOrder;
+ isMobileSelectionModeActive?: boolean;
+ setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void;
};
const sortableSearchTabs: SearchQuery[] = [CONST.SEARCH.TAB.ALL];
@@ -40,22 +43,20 @@ const transactionItemMobileHeight = 100;
const reportItemTransactionHeight = 52;
const listItemPadding = 12; // this is equivalent to 'mb3' on every transaction/report list item
const searchHeaderHeight = 54;
-
-function isTransactionListItemType(item: TransactionListItemType | ReportListItemType): item is TransactionListItemType {
- const transactionListItem = item as TransactionListItemType;
- return transactionListItem.transactionID !== undefined;
-}
-
-function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
+function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchProps) {
const {isOffline} = useNetwork();
const styles = useThemeStyles();
- const {isLargeScreenWidth} = useWindowDimensions();
+ const {isLargeScreenWidth, isSmallScreenWidth} = useWindowDimensions();
const navigation = useNavigation>();
const lastSearchResultsRef = useRef>();
+ const {setCurrentSearchHash} = useSearchContext();
+
+ const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder);
+ const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
const getItemHeight = useCallback(
(item: TransactionListItemType | ReportListItemType) => {
- if (isTransactionListItemType(item)) {
+ if (SearchUtils.isTransactionListItemType(item)) {
return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding;
}
@@ -73,8 +74,15 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
[isLargeScreenWidth],
);
- const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder);
- const [currentSearchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
+ const getItemHeightMemoized = lodashMemoize(
+ (item: TransactionListItemType | ReportListItemType) => getItemHeight(item),
+ (item) => {
+ // List items are displayed differently on "L"arge and "N"arrow screens so the height will differ
+ // in addition the same items might be displayed as part of different Search screens ("Expenses", "All", "Finished")
+ const screenSizeHash = isLargeScreenWidth ? 'L' : 'N';
+ return `${hash}-${item.keyForList}-${screenSizeHash}`;
+ },
+ );
// save last non-empty search results to avoid ugly flash of loading screen when hash changes and onyx returns empty data
if (currentSearchResults?.data && currentSearchResults !== lastSearchResultsRef.current) {
@@ -88,31 +96,50 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
return;
}
+ setCurrentSearchHash(hash);
SearchActions.search({hash, query, policyIDs, offset: 0, sortBy, sortOrder});
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [hash, isOffline]);
- const isLoadingItems = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || searchResults?.data === undefined;
- const isLoadingMoreItems = !isLoadingItems && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
- const shouldShowEmptyState = !isLoadingItems && isEmptyObject(searchResults?.data);
+ const isDataLoaded = searchResults?.data !== undefined;
+ const shouldShowLoadingState = !isOffline && !isDataLoaded;
+ const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
- if (isLoadingItems) {
- return ;
+ if (shouldShowLoadingState) {
+ return (
+ <>
+
+
+ >
+ );
}
+ const shouldShowEmptyState = !isDataLoaded || SearchUtils.isSearchResultsEmpty(searchResults);
+
if (shouldShowEmptyState) {
- return ;
+ return (
+ <>
+
+
+ >
+ );
}
const openReport = (item: TransactionListItemType | ReportListItemType) => {
- let reportID = isTransactionListItemType(item) ? item.transactionThreadReportID : item.reportID;
+ let reportID = SearchUtils.isTransactionListItemType(item) ? item.transactionThreadReportID : item.reportID;
if (!reportID) {
return;
}
// If we're trying to open a legacy transaction without a transaction thread, let's create the thread and navigate the user
- if (isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) {
+ if (SearchUtils.isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) {
reportID = ReportUtils.generateReportID();
SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID);
}
@@ -121,7 +148,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
};
const fetchMoreResults = () => {
- if (!searchResults?.search?.hasMoreResults || isLoadingItems || isLoadingMoreItems) {
+ if (!searchResults?.search?.hasMoreResults || shouldShowLoadingState || shouldShowLoadingMoreItems) {
return;
}
const currentOffset = searchResults?.search?.offset ?? 0;
@@ -151,19 +178,28 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
const shouldShowYear = SearchUtils.shouldShowYear(searchResults?.data);
+ const canSelectMultiple = isSmallScreenWidth ? isMobileSelectionModeActive : true;
+
return (
-
+
+ !isLargeScreenWidth ? null : (
+
+ )
}
+ canSelectMultiple={canSelectMultiple}
customListHeaderHeight={searchHeaderHeight}
// To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling,
// we have configured a larger windowSize and a longer delay between batch renders.
@@ -177,19 +213,20 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
windowSize={111}
updateCellsBatchingPeriod={200}
ListItem={ListItem}
- sections={[{data: sortedData, isDisabled: false}]}
- onSelectRow={(item) => openReport(item)}
- getItemHeight={getItemHeight}
+ onSelectRow={openReport}
+ getItemHeight={getItemHeightMemoized}
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}
onEndReached={fetchMoreResults}
+ setIsMobileSelectionModeActive={setIsMobileSelectionModeActive}
+ isMobileSelectionModeActive={isMobileSelectionModeActive}
listFooterContent={
- isLoadingMoreItems ? (
-
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
new file mode 100644
index 000000000000..232ae841d4ff
--- /dev/null
+++ b/src/components/Search/types.ts
@@ -0,0 +1,52 @@
+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 */
+ isSelected: boolean;
+
+ /** If the transaction can be deleted */
+ canDelete: boolean;
+
+ /** If the transaction can be put on hold */
+ canHold: boolean;
+
+ /** If the transaction can be removed from hold */
+ canUnhold: boolean;
+
+ /** The action that can be performed for the transaction */
+ action: string;
+};
+
+/** Model of selected results */
+type SelectedTransactions = Record;
+
+type SortOrder = ValueOf;
+type SearchColumnType = ValueOf;
+
+type SearchContext = {
+ currentSearchHash: number;
+ selectedTransactionIDs: string[];
+ setCurrentSearchHash: (hash: number) => void;
+ setSelectedTransactionIDs: (selectedTransactionIds: string[]) => void;
+};
+
+type ASTNode = {
+ operator: ValueOf;
+ left: ValueOf | ASTNode;
+ right: string | ASTNode;
+};
+
+type QueryFilter = {
+ operator: ValueOf;
+ value: string | number;
+};
+
+type AllFieldKeys = ValueOf | ValueOf;
+
+type QueryFilters = {
+ [K in AllFieldKeys]: QueryFilter | QueryFilter[];
+};
+
+export type {SelectedTransactionInfo, SelectedTransactions, SearchColumnType, SortOrder, SearchContext, ASTNode, QueryFilter, QueryFilters, AllFieldKeys};
diff --git a/src/components/Section/index.tsx b/src/components/Section/index.tsx
index 49ef1e89cff9..f8b0a9edb696 100644
--- a/src/components/Section/index.tsx
+++ b/src/components/Section/index.tsx
@@ -13,6 +13,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import isIllustrationLottieAnimation from '@libs/isIllustrationLottieAnimation';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type IconAsset from '@src/types/utils/IconAsset';
import IconSection from './IconSection';
@@ -91,14 +92,6 @@ type SectionProps = Partial & {
banner?: ReactNode;
};
-function isIllustrationLottieAnimation(illustration: DotLottieAnimation | IconAsset | undefined): illustration is DotLottieAnimation {
- if (typeof illustration === 'number' || !illustration) {
- return false;
- }
-
- return 'file' in illustration && 'w' in illustration && 'h' in illustration;
-}
-
function Section({
children,
childrenStyles,
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index c9dc773c8818..5be228f0156e 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -32,6 +32,7 @@ function BaseListItem({
shouldSyncFocus = true,
onFocus = () => {},
hoverStyle,
+ onLongPressRow,
}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -42,7 +43,7 @@ function BaseListItem({
// Sync focus on an item
useSyncFocus(pressableRef, !!isFocused, shouldSyncFocus);
- const handleMouseUp = (e: React.MouseEvent) => {
+ const handleMouseLeave = (e: React.MouseEvent) => {
e.stopPropagation();
setMouseUp();
};
@@ -71,6 +72,9 @@ function BaseListItem({
// eslint-disable-next-line react/jsx-props-no-spreading
{...bind}
ref={pressableRef}
+ onLongPress={() => {
+ onLongPressRow?.(item);
+ }}
onPress={(e) => {
if (isMouseDownOnInput) {
e?.stopPropagation(); // Preventing the click action
@@ -82,6 +86,7 @@ function BaseListItem({
onSelectRow(item);
}}
disabled={isDisabled && !item.isSelected}
+ interactive={item.isInteractive}
accessibilityLabel={item.text ?? ''}
role={CONST.ROLE.BUTTON}
hoverDimmingValue={1}
@@ -91,8 +96,7 @@ function BaseListItem({
id={keyForList ?? ''}
style={pressableStyle}
onFocus={onFocus}
- onMouseUp={handleMouseUp}
- onMouseLeave={handleMouseUp}
+ onMouseLeave={handleMouseLeave}
tabIndex={item.tabIndex}
>
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 617c70a1d224..6cc6b403dc7a 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -24,6 +24,7 @@ import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset';
import Log from '@libs/Log';
+import * as SearchUtils from '@libs/SearchUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -66,6 +67,7 @@ function BaseSelectionList(
showConfirmButton = false,
shouldPreventDefaultFocusOnSelectRow = false,
containerStyle,
+ sectionListStyle,
disableKeyboardShortcuts = false,
children,
shouldStopPropagation = false,
@@ -92,6 +94,8 @@ function BaseSelectionList(
updateCellsBatchingPeriod = 50,
removeClippedSubviews = true,
shouldDelayFocus = true,
+ onLongPressRow,
+ isMobileSelectionModeActive,
}: BaseSelectionListProps,
ref: ForwardedRef,
) {
@@ -165,7 +169,7 @@ function BaseSelectionList(
itemLayouts.push({length: fullItemHeight, offset});
offset += fullItemHeight;
- if (item.isSelected) {
+ if (item.isSelected && !selectedOptions.find((option) => option.keyForList === item.keyForList)) {
selectedOptions.push(item);
}
});
@@ -221,7 +225,7 @@ function BaseSelectionList(
return [processedSections, showMoreButton];
// we don't need to add styles here as they change
// we don't need to add flattendedSections here as they will change along with sections
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [sections, currentPage]);
// Disable `Enter` shortcut if the active element is a button or checkbox
@@ -247,7 +251,7 @@ function BaseSelectionList(
listRef.current.scrollToLocation({sectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight});
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[flattenedSections.allOptions],
);
@@ -258,7 +262,7 @@ function BaseSelectionList(
}
setDisabledArrowKeyIndexes(flattenedSections.disabledArrowKeyOptionsIndexes);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [flattenedSections.disabledArrowKeyOptionsIndexes]);
// If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member
@@ -277,7 +281,7 @@ function BaseSelectionList(
onChangeText?.('');
}, [onChangeText]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
const debouncedOnSelectRow = useCallback(lodashDebounce(onSelectRow, 200), [onSelectRow]);
/**
@@ -336,7 +340,7 @@ function BaseSelectionList(
// This debounce happens on the trailing edge because on repeated enter presses, rapid component state update cancels the existing debounce and the redundant
// enter presses runs the debounced function again.
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
const debouncedSelectFocusedOption = useCallback(lodashDebounce(selectFocusedOption, 100), [selectFocusedOption]);
/**
@@ -431,6 +435,13 @@ function BaseSelectionList(
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
const showTooltip = shouldShowTooltips && normalizedIndex < 10;
+ const handleOnCheckboxPress = () => {
+ if (SearchUtils.isReportListItemType(item)) {
+ return onCheckboxPress;
+ }
+ return onCheckboxPress ? () => onCheckboxPress(item) : undefined;
+ };
+
return (
<>
(
isDisabled={isDisabled}
showTooltip={showTooltip}
canSelectMultiple={canSelectMultiple}
+ onLongPressRow={onLongPressRow}
+ isMobileSelectionModeActive={isMobileSelectionModeActive}
onSelectRow={() => selectRow(item)}
- onCheckboxPress={onCheckboxPress ? () => onCheckboxPress?.(item) : undefined}
+ onCheckboxPress={handleOnCheckboxPress()}
onDismissError={() => onDismissError?.(item)}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
// We're already handling the Enter key press in the useKeyboardShortcut hook, so we don't want the list item to submit the form
@@ -717,7 +730,7 @@ function BaseSelectionList(
viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
testID="selection-list"
onLayout={onSectionListLayout}
- style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0}
+ style={[(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0, sectionListStyle]}
ListHeaderComponent={listHeaderContent}
ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance}
ListEmptyComponent={listEmptyContent}
diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx
index 6aabfebf0da9..f535c0342e55 100644
--- a/src/components/SelectionList/Search/ActionCell.tsx
+++ b/src/components/SelectionList/Search/ActionCell.tsx
@@ -9,34 +9,49 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import type {SearchTransactionAction} from '@src/types/onyx/SearchResults';
+
+const actionTranslationsMap: Record = {
+ view: 'common.view',
+ review: 'common.review',
+ done: 'common.done',
+ paid: 'iou.settledExpensify',
+};
type ActionCellProps = {
- onButtonPress: () => void;
- action?: string;
+ action?: SearchTransactionAction;
isLargeScreenWidth?: boolean;
+ isSelected?: boolean;
+ goToItem: () => void;
+ isChildListItem?: boolean;
+ parentAction?: string;
};
-function ActionCell({onButtonPress, action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true}: ActionCellProps) {
+function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true, isSelected = false, goToItem, isChildListItem = false, parentAction = ''}: ActionCellProps) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- 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';
+ const text = translate(actionTranslationsMap[action]);
+
+ const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID);
+
+ if ((parentAction !== CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID) || action === CONST.SEARCH.ACTION_TYPES.DONE) {
return (
- );
+ const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {};
+
+ if (action === CONST.SEARCH.ACTION_TYPES.VIEW || shouldUseViewAction) {
+ return isLargeScreenWidth ? (
+
+ ) : null;
+ }
+
+ if (action === CONST.SEARCH.ACTION_TYPES.REVIEW) {
+ return (
+
+ );
+ }
}
ActionCell.displayName = 'ActionCell';
diff --git a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
index 8f46a5388da8..e4702734fcd0 100644
--- a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
+++ b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
@@ -2,24 +2,45 @@ import React, {memo} from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import {PressableWithFeedback} from '@components/Pressable';
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 CONST from '@src/CONST';
+import type {SearchAccountDetails, SearchTransactionAction} from '@src/types/onyx/SearchResults';
import ActionCell from './ActionCell';
import UserInfoCell from './UserInfoCell';
type ExpenseItemHeaderNarrowProps = {
+ text?: string;
participantFrom: SearchAccountDetails;
participantTo: SearchAccountDetails;
participantFromDisplayName: string;
participantToDisplayName: string;
+ action?: SearchTransactionAction;
onButtonPress: () => void;
- action?: string;
+ canSelectMultiple?: boolean;
+ isSelected?: boolean;
+ isDisabled?: boolean | null;
+ isDisabledCheckbox?: boolean;
+ handleCheckboxPress?: () => void;
};
-function ExpenseItemHeaderNarrow({participantFrom, participantFromDisplayName, participantTo, participantToDisplayName, onButtonPress, action}: ExpenseItemHeaderNarrowProps) {
+function ExpenseItemHeaderNarrow({
+ participantFrom,
+ participantFromDisplayName,
+ participantTo,
+ participantToDisplayName,
+ onButtonPress,
+ action,
+ canSelectMultiple,
+ isDisabledCheckbox,
+ isSelected,
+ isDisabled,
+ handleCheckboxPress,
+ text,
+}: ExpenseItemHeaderNarrowProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const theme = useTheme();
@@ -27,6 +48,26 @@ function ExpenseItemHeaderNarrow({participantFrom, participantFromDisplayName, p
return (
+ {canSelectMultiple && (
+ handleCheckboxPress?.()}
+ style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), isDisabledCheckbox && styles.cursorDisabled, styles.mr1]}
+ >
+
+ {isSelected && (
+
+ )}
+
+
+ )}
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index f9e8e1951d9a..fb503960cd5e 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -1,10 +1,10 @@
import React from 'react';
import {View} from 'react-native';
+import Checkbox from '@components/Checkbox';
import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem, ReportListItemProps, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';
-import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -54,15 +54,16 @@ function ReportListItem({
showTooltip,
isDisabled,
canSelectMultiple,
+ onCheckboxPress,
onSelectRow,
onDismissError,
onFocus,
+ onLongPressRow,
shouldSyncFocus,
}: ReportListItemProps) {
const reportItem = item as unknown as ReportListItemType;
const styles = useThemeStyles();
- const {translate} = useLocalize();
const {isLargeScreenWidth} = useWindowDimensions();
const StyleUtils = useStyleUtils();
@@ -70,7 +71,7 @@ function ReportListItem({
return;
}
- const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive];
+ const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive, styles.ph3];
const handleOnButtonPress = () => {
onSelectRow(item);
@@ -86,8 +87,8 @@ function ReportListItem({
return null;
}
- const participantFrom = reportItem.transactions[0].from;
- const participantTo = reportItem.transactions[0].to;
+ const participantFrom = reportItem.from;
+ const participantTo = reportItem.to;
// These values should come as part of the item via SearchUtils.getSections() but ReportListItem is not yet 100% handled
// This will be simplified in future once sorting of ReportListItem is done
@@ -104,9 +105,11 @@ function ReportListItem({
showTooltip={showTooltip}
isDisabled={isDisabled}
canSelectMultiple={canSelectMultiple}
+ onCheckboxPress={() => onCheckboxPress?.(transactionItem as unknown as TItem)}
onSelectRow={() => openReportInRHP(transactionItem)}
onDismissError={onDismissError}
onFocus={onFocus}
+ onLongPressRow={onLongPressRow}
shouldSyncFocus={shouldSyncFocus}
/>
);
@@ -123,6 +126,7 @@ function ReportListItem({
showTooltip={showTooltip}
canSelectMultiple={canSelectMultiple}
onSelectRow={onSelectRow}
+ onLongPressRow={onLongPressRow}
onDismissError={onDismissError}
errors={item.errors}
pendingAction={item.pendingAction}
@@ -142,12 +146,22 @@ function ReportListItem({
onButtonPress={handleOnButtonPress}
/>
)}
-
+
-
+ {canSelectMultiple && (
+ onCheckboxPress?.(item)}
+ isChecked={item.isSelected}
+ containerStyle={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!item.isSelected, !!item.isDisabled)]}
+ disabled={!!isDisabled || item.isDisabledCheckbox}
+ accessibilityLabel={item.text ?? ''}
+ shouldStopMouseDownPropagation
+ style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, !isLargeScreenWidth && styles.mr3]}
+ />
+ )}
+
{reportItem?.reportName}
- {`${reportItem.transactions.length} ${translate('search.groupedExpenses')}`}
@@ -159,30 +173,31 @@ function ReportListItem({
{isLargeScreenWidth && (
- <>
- {/** We add an empty view with type style to align the total with the table header */}
-
-
-
-
- >
+
+
+
)}
-
{reportItem.transactions.map((transaction) => (
{
openReportInRHP(transaction);
}}
+ onCheckboxPress={() => onCheckboxPress?.(transaction as unknown as TItem)}
showItemHeaderOnNarrowLayout={false}
containerStyle={styles.mt3}
isChildListItem
+ isDisabled={!!isDisabled}
+ canSelectMultiple={!!canSelectMultiple}
+ isButtonSelected={item.isSelected}
+ shouldShowTransactionCheckbox
/>
))}
diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx
index 23ab549dd495..a10552ca9ad8 100644
--- a/src/components/SelectionList/Search/TransactionListItem.tsx
+++ b/src/components/SelectionList/Search/TransactionListItem.tsx
@@ -12,8 +12,10 @@ function TransactionListItem({
isDisabled,
canSelectMultiple,
onSelectRow,
+ onCheckboxPress,
onDismissError,
onFocus,
+ onLongPressRow,
shouldSyncFocus,
}: TransactionListItemProps) {
const transactionItem = item as unknown as TransactionListItemType;
@@ -21,7 +23,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,
@@ -45,6 +47,7 @@ function TransactionListItem({
pendingAction={item.pendingAction}
keyForList={item.keyForList}
onFocus={onFocus}
+ onLongPressRow={onLongPressRow}
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
>
@@ -54,6 +57,11 @@ function TransactionListItem({
onButtonPress={() => {
onSelectRow(item);
}}
+ onCheckboxPress={() => onCheckboxPress?.(item)}
+ isDisabled={!!isDisabled}
+ canSelectMultiple={!!canSelectMultiple}
+ isButtonSelected={item.isSelected}
+ shouldShowTransactionCheckbox={false}
/>
);
diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx
index 0adc7ee21fd1..4f83814374ba 100644
--- a/src/components/SelectionList/Search/TransactionListItemRow.tsx
+++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx
@@ -1,8 +1,10 @@
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
+import Checkbox from '@components/Checkbox';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import {PressableWithFeedback} from '@components/Pressable';
import ReceiptImage from '@components/ReceiptImage';
import type {TransactionListItemType} from '@components/SelectionList/types';
import TextWithTooltip from '@components/TextWithTooltip';
@@ -13,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
+import StringUtils from '@libs/StringUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import variables from '@styles/variables';
@@ -43,9 +46,15 @@ type TransactionListItemRowProps = {
item: TransactionListItemType;
showTooltip: boolean;
onButtonPress: () => void;
+ onCheckboxPress: () => void;
showItemHeaderOnNarrowLayout?: boolean;
containerStyle?: StyleProp;
isChildListItem?: boolean;
+ isDisabled: boolean;
+ canSelectMultiple: boolean;
+ isButtonSelected?: boolean;
+ parentAction?: string;
+ shouldShowTransactionCheckbox?: boolean;
};
const getTypeIcon = (type?: SearchTransactionType) => {
@@ -66,13 +75,15 @@ function ReceiptCell({transactionItem}: TransactionCellProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const backgroundStyles = transactionItem.isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border);
+
return (
@@ -109,16 +121,20 @@ function MerchantCell({transactionItem, showTooltip, isLargeScreenWidth}: Transa
const styles = useThemeStyles();
const {translate} = useLocalize();
const description = TransactionUtils.getDescription(transactionItem);
- let merchant = transactionItem.shouldShowMerchant ? transactionItem.formattedMerchant : description;
+ let merchantOrDescriptionToDisplay = transactionItem.formattedMerchant;
+ if (!merchantOrDescriptionToDisplay && !isLargeScreenWidth) {
+ merchantOrDescriptionToDisplay = description;
+ }
+ let merchant = transactionItem.shouldShowMerchant ? merchantOrDescriptionToDisplay : description;
if (TransactionUtils.hasReceipt(transactionItem) && TransactionUtils.isReceiptBeingScanned(transactionItem) && transactionItem.shouldShowMerchant) {
merchant = translate('iou.receiptStatusTitle');
}
-
+ const merchantToDisplay = StringUtils.getFirstLine(merchant);
return (
);
@@ -145,7 +161,7 @@ function TotalCell({showTooltip, isLargeScreenWidth, transactionItem}: TotalCell
function TypeCell({transactionItem, isLargeScreenWidth}: TransactionCellProps) {
const theme = useTheme();
- const typeIcon = getTypeIcon(transactionItem.type);
+ const typeIcon = getTypeIcon(transactionItem.transactionType);
return (
- ) : (
-
);
}
@@ -209,67 +232,104 @@ function TaxCell({transactionItem, showTooltip}: TransactionCellProps) {
);
}
-function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeaderOnNarrowLayout = true, containerStyle, isChildListItem = false}: TransactionListItemRowProps) {
+function TransactionListItemRow({
+ item,
+ showTooltip,
+ isDisabled,
+ canSelectMultiple,
+ onButtonPress,
+ onCheckboxPress,
+ showItemHeaderOnNarrowLayout = true,
+ containerStyle,
+ isChildListItem = false,
+ isButtonSelected = false,
+ parentAction = '',
+ shouldShowTransactionCheckbox,
+}: TransactionListItemRowProps) {
const styles = useThemeStyles();
const {isLargeScreenWidth} = useWindowDimensions();
const StyleUtils = useStyleUtils();
+ const theme = useTheme();
if (!isLargeScreenWidth) {
return (
{showItemHeaderOnNarrowLayout && (
)}
-
+
+ {canSelectMultiple && shouldShowTransactionCheckbox && (
+
+
+ {item.isSelected && (
+
+ )}
+
+
+ )}
-
+
-
-
-
-
+ {!!item.category && (
+
+
+
+ )}
@@ -280,11 +340,27 @@ function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeade
return (
-
+ {canSelectMultiple && (
+
+ )}
+
+
+
+
@@ -292,14 +368,14 @@ function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeade
@@ -317,7 +393,7 @@ function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeade
{item.shouldShowCategory && (
@@ -326,7 +402,7 @@ function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeade
{item.shouldShowTag && (
@@ -336,7 +412,7 @@ function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeade
@@ -346,21 +422,17 @@ function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeade
-
-
-
diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx
index 6ba753273e8c..4bf1715e0434 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';
@@ -25,6 +25,12 @@ const SearchColumns: SearchColumnConfig[] = [
shouldShow: () => true,
isColumnSortable: false,
},
+ {
+ columnName: CONST.SEARCH.TABLE_COLUMNS.TYPE,
+ translationKey: 'common.type',
+ shouldShow: () => true,
+ isColumnSortable: false,
+ },
{
columnName: CONST.SEARCH.TABLE_COLUMNS.DATE,
translationKey: 'common.date',
@@ -53,17 +59,17 @@ const SearchColumns: SearchColumnConfig[] = [
{
columnName: CONST.SEARCH.TABLE_COLUMNS.CATEGORY,
translationKey: 'common.category',
- shouldShow: (data, metadata) => metadata?.columnsToShow.shouldShowCategoryColumn ?? false,
+ shouldShow: (data, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false,
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.TAG,
translationKey: 'common.tag',
- shouldShow: (data, metadata) => metadata?.columnsToShow.shouldShowTagColumn ?? false,
+ shouldShow: (data, metadata) => metadata?.columnsToShow?.shouldShowTagColumn ?? false,
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT,
translationKey: 'common.tax',
- shouldShow: (data, metadata) => metadata?.columnsToShow.shouldShowTaxColumn ?? false,
+ shouldShow: (data, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false,
isColumnSortable: false,
},
{
@@ -71,12 +77,6 @@ const SearchColumns: SearchColumnConfig[] = [
translationKey: 'common.total',
shouldShow: () => true,
},
- {
- columnName: CONST.SEARCH.TABLE_COLUMNS.TYPE,
- translationKey: 'common.type',
- shouldShow: () => true,
- isColumnSortable: false,
- },
{
columnName: CONST.SEARCH.TABLE_COLUMNS.ACTION,
translationKey: 'common.action',
@@ -107,8 +107,8 @@ 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/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx
index 9fc138254f8b..83bc8df36571 100644
--- a/src/components/SelectionList/TableListItem.tsx
+++ b/src/components/SelectionList/TableListItem.tsx
@@ -43,7 +43,7 @@ function TableListItem({
return (
({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={isDisabled || item.isDisabledCheckbox}
onPress={handleCheckboxPress}
- style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.mr3]}
+ style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.mr3, item.cursorStyle]}
>
-
+
{item.isSelected && (
= {
/** Handles what to do when the item is focused */
onFocus?: () => void;
+
+ /** Callback to fire when the item is long pressed */
+ onLongPressRow?: (item: TItem) => void;
+
+ /** Whether Selection Mode is active - used only on small screens */
+ isMobileSelectionModeActive?: boolean;
} & TRightHandSideComponent;
type ListItem = {
@@ -79,6 +87,9 @@ type ListItem = {
/** Whether this option is disabled for selection */
isDisabled?: boolean | null;
+ /** Whether this item should be interactive at all */
+ isInteractive?: boolean;
+
/** List title is bold by default. Use this props to customize it */
isBold?: boolean;
@@ -131,6 +142,9 @@ type ListItem = {
/** Whether item pressable wrapper should be focusable */
tabIndex?: 0 | -1;
+
+ /** The style to override the cursor appearance */
+ cursorStyle?: CursorStyles[keyof CursorStyles];
};
type TransactionListItemType = ListItem &
@@ -172,10 +186,19 @@ type TransactionListItemType = ListItem &
* This is true if at least one transaction in the dataset was created in past years
*/
shouldShowYear: boolean;
+
+ /** Key used internally by React */
+ keyForList: string;
};
type ReportListItemType = ListItem &
SearchReport & {
+ /** The personal details of the user requesting money */
+ from: SearchAccountDetails;
+
+ /** The personal details of the user paying the request */
+ to: SearchAccountDetails;
+
transactions: TransactionListItemType[];
};
@@ -390,6 +413,9 @@ type BaseSelectionListProps = Partial & {
/** Styles to apply to SelectionList container */
containerStyle?: StyleProp;
+ /** Styles to apply to SectionList component */
+ sectionListStyle?: StyleProp;
+
/** Whether focus event should be delayed */
shouldDelayFocus?: boolean;
@@ -448,6 +474,12 @@ type BaseSelectionListProps = Partial & {
* https://reactnative.dev/docs/optimizing-flatlist-configuration#windowsize
*/
windowSize?: number;
+
+ /** Callback to fire when the item is long pressed */
+ onLongPressRow?: (item: TItem) => void;
+
+ /** Whether Selection Mode is active - used only on small screens */
+ isMobileSelectionModeActive?: boolean;
} & TRightHandSideComponent;
type SelectionListHandle = {
diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx
index 3b5265685050..01ac6e158d7d 100644
--- a/src/components/SelectionScreen.tsx
+++ b/src/components/SelectionScreen.tsx
@@ -1,12 +1,19 @@
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 ErrorMessageRow from './ErrorMessageRow';
import HeaderWithBackButton from './HeaderWithBackButton';
+import OfflineWithFeedback from './OfflineWithFeedback';
import ScreenWrapper from './ScreenWrapper';
import SelectionList from './SelectionList';
import type RadioListItem from './SelectionList/RadioListItem';
@@ -14,11 +21,13 @@ import type TableListItem from './SelectionList/TableListItem';
import type {ListItem, SectionListDataType} from './SelectionList/types';
import type UserListItem from './SelectionList/UserListItem';
-type SelectorType = ListItem & {
- value: string;
+type SelectorType = ListItem & {
+ value: T;
+
+ onPress?: () => void;
};
-type SelectionScreenProps = {
+type SelectionScreenProps = {
/** Used to set the testID for tests */
displayName: string;
@@ -31,8 +40,11 @@ type SelectionScreenProps = {
/** Content to display if the list is empty */
listEmptyContent?: React.JSX.Element | null;
+ /** Custom content to display in the footer of list component. */
+ listFooterContent?: React.JSX.Element | null;
+
/** Sections for the section list */
- sections: Array>;
+ sections: Array>>;
/** Default renderer for every item in the list */
listItem: typeof RadioListItem | typeof UserListItem | typeof TableListItem;
@@ -41,7 +53,7 @@ type SelectionScreenProps = {
initiallyFocusedOptionKey?: string | null | undefined;
/** Callback to fire when a row is pressed */
- onSelectRow: (selection: SelectorType) => void;
+ onSelectRow: (selection: SelectorType) => void;
/** Callback to fire when back button is pressed */
onBackButtonPress: () => void;
@@ -60,13 +72,26 @@ 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({
+function SelectionScreen({
displayName,
title,
headerContent,
listEmptyContent,
+ listFooterContent,
sections,
listItem,
initiallyFocusedOptionKey,
@@ -77,8 +102,13 @@ function SelectionScreen({
featureName,
shouldBeBlocked,
connectionName,
-}: SelectionScreenProps) {
+ pendingAction,
+ errors,
+ errorRowStyles,
+ onClose,
+}: SelectionScreenProps) {
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const policy = PolicyUtils.getPolicy(policyID);
const isConnectionEmpty = isEmpty(policy?.connections?.[connectionName]);
@@ -91,23 +121,37 @@ function SelectionScreen({
shouldBeBlocked={isConnectionEmpty || shouldBeBlocked}
>
-
+ {headerContent}
+
+
+
+
+
);
diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx
index d3916220ca88..294752a3a0d7 100644
--- a/src/components/SettlementButton.tsx
+++ b/src/components/SettlementButton.tsx
@@ -4,8 +4,10 @@ import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx, withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import playSound, {SOUNDS} from '@libs/Sound';
+import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import * as BankAccounts from '@userActions/BankAccounts';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
@@ -103,6 +105,9 @@ type SettlementButtonProps = SettlementButtonOnyxProps & {
/** Callback to open confirmation modal if any of the transactions is on HOLD */
confirmApproval?: () => void;
+
+ /** Whether to use keyboard shortcuts for confirmation or not */
+ useKeyboardShortcuts?: boolean;
};
function SettlementButton({
@@ -138,6 +143,7 @@ function SettlementButton({
enterKeyEventListenerPriority = 0,
confirmApproval,
policy,
+ useKeyboardShortcuts = false,
}: SettlementButtonProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -223,10 +229,15 @@ function SettlementButton({
}
return buttonOptions;
// We don't want to reorder the options when the preferred payment method changes while the button is still visible
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [currency, formattedAmount, iouReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]);
const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => {
+ if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) {
+ Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id));
+ return;
+ }
+
if (iouPaymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || iouPaymentType === CONST.IOU.PAYMENT_TYPE.VBBA) {
triggerKYCFlow(event, iouPaymentType);
BankAccounts.setPersonalBankAccountContinueKYCOnSuccess(ROUTES.ENABLE_PAYMENTS);
@@ -276,11 +287,14 @@ function SettlementButton({
onPress={(event, iouPaymentType) => selectPaymentType(event, iouPaymentType, triggerKYCFlow)}
pressOnEnter={pressOnEnter}
options={paymentButtonOptions}
- onOptionSelected={(option) => savePreferredPaymentMethod(policyID, option.value)}
+ onOptionSelected={(option) => {
+ savePreferredPaymentMethod(policyID, option.value);
+ }}
style={style}
buttonSize={buttonSize}
anchorAlignment={paymentMethodDropdownAnchorAlignment}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
+ useKeyboardShortcuts={useKeyboardShortcuts}
/>
)}
diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts
index 5e4cb8dfcc90..b3050f986be1 100644
--- a/src/components/ShowContextMenuContext.ts
+++ b/src/components/ShowContextMenuContext.ts
@@ -7,11 +7,12 @@ import * as ReportUtils from '@libs/ReportUtils';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
-import type {Report, ReportAction} from '@src/types/onyx';
+import type {Report, ReportAction, ReportNameValuePairs} from '@src/types/onyx';
type ShowContextMenuContextProps = {
anchor: ContextMenuAnchor;
report: OnyxEntry;
+ reportNameValuePairs: OnyxEntry;
action: OnyxEntry;
transactionThreadReport?: OnyxEntry;
checkIfContextMenuActive: () => void;
@@ -20,6 +21,7 @@ type ShowContextMenuContextProps = {
const ShowContextMenuContext = createContext({
anchor: null,
report: undefined,
+ reportNameValuePairs: undefined,
action: undefined,
transactionThreadReport: undefined,
checkIfContextMenuActive: () => {},
diff --git a/src/components/SingleChoiceQuestion.tsx b/src/components/SingleChoiceQuestion.tsx
index c2dc72438e43..e52007850475 100644
--- a/src/components/SingleChoiceQuestion.tsx
+++ b/src/components/SingleChoiceQuestion.tsx
@@ -22,7 +22,7 @@ function SingleChoiceQuestion({prompt, errorText, possibleAnswers, currentQuesti
<>
{prompt}
diff --git a/src/components/Skeletons/ItemListSkeletonView.tsx b/src/components/Skeletons/ItemListSkeletonView.tsx
index 1ee2da8a8019..046cdfffbee5 100644
--- a/src/components/Skeletons/ItemListSkeletonView.tsx
+++ b/src/components/Skeletons/ItemListSkeletonView.tsx
@@ -1,6 +1,6 @@
-import React, {useMemo, useState} from 'react';
-import {View} from 'react-native';
-import type {StyleProp, ViewStyle} from 'react-native';
+import React, {useCallback, useMemo, useState} from 'react';
+import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native';
+import {StyleSheet, View} from 'react-native';
import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -10,22 +10,62 @@ type ListItemSkeletonProps = {
shouldAnimate?: boolean;
renderSkeletonItem: (args: {itemIndex: number}) => React.ReactNode;
fixedNumItems?: number;
+ gradientOpacityEnabled?: boolean;
itemViewStyle?: StyleProp;
itemViewHeight?: number;
};
-function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem, fixedNumItems, itemViewStyle = {}, itemViewHeight = CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT}: ListItemSkeletonProps) {
+const getVerticalMargin = (style: StyleProp): number => {
+ if (!style) {
+ return 0;
+ }
+
+ const flattenStyle = StyleSheet.flatten(style);
+ const marginVertical = Number(flattenStyle?.marginVertical ?? 0);
+ const marginTop = Number(flattenStyle?.marginTop ?? 0);
+ const marginBottom = Number(flattenStyle?.marginBottom ?? 0);
+
+ return marginVertical + marginTop + marginBottom;
+};
+
+function ItemListSkeletonView({
+ shouldAnimate = true,
+ renderSkeletonItem,
+ fixedNumItems,
+ gradientOpacityEnabled = false,
+ itemViewStyle = {},
+ itemViewHeight = CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT,
+}: ListItemSkeletonProps) {
const theme = useTheme();
const themeStyles = useThemeStyles();
const [numItems, setNumItems] = useState(fixedNumItems ?? 0);
+
+ const totalItemHeight = itemViewHeight + getVerticalMargin(itemViewStyle);
+
+ const handleLayout = useCallback(
+ (event: LayoutChangeEvent) => {
+ if (fixedNumItems) {
+ return;
+ }
+
+ const totalHeight = event.nativeEvent.layout.height;
+ const newNumItems = Math.ceil(totalHeight / totalItemHeight);
+ if (newNumItems !== numItems) {
+ setNumItems(newNumItems);
+ }
+ },
+ [fixedNumItems, numItems, totalItemHeight],
+ );
+
const skeletonViewItems = useMemo(() => {
const items = [];
for (let i = 0; i < numItems; i++) {
+ const opacity = gradientOpacityEnabled ? 1 - i / (numItems - 1) : 1;
items.push(
{
- if (fixedNumItems) {
- return;
- }
-
- const newNumItems = Math.ceil(event.nativeEvent.layout.height / itemViewHeight);
- if (newNumItems === numItems) {
- return;
- }
- setNumItems(newNumItems);
- }}
+ onLayout={handleLayout}
>
- {skeletonViewItems}
+ {skeletonViewItems}
);
}
diff --git a/src/components/Skeletons/TableListItemSkeleton.tsx b/src/components/Skeletons/SearchRowSkeleton.tsx
similarity index 54%
rename from src/components/Skeletons/TableListItemSkeleton.tsx
rename to src/components/Skeletons/SearchRowSkeleton.tsx
index 6ff3a3aedbb9..2359e47b7520 100644
--- a/src/components/Skeletons/TableListItemSkeleton.tsx
+++ b/src/components/Skeletons/SearchRowSkeleton.tsx
@@ -2,26 +2,41 @@ import React from 'react';
import {Circle, Rect} from 'react-native-svg';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
import ItemListSkeletonView from './ItemListSkeletonView';
-type TableListItemSkeletonProps = {
+type SearchRowSkeletonProps = {
shouldAnimate?: boolean;
fixedNumItems?: number;
+ gradientOpacityEnabled?: boolean;
};
-const barHeight = '10';
-const shortBarWidth = '40';
-const longBarWidth = '120';
+const barHeight = 8;
+const longBarWidth = 120;
+const leftPaneWidth = variables.sideBarWidth;
-function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListItemSkeletonProps) {
+// 12 is the gap between the element and the right button
+const gapWidth = 12;
+
+// 80 is the width of the element itself
+const rightSideElementWidth = 80;
+
+// 24 is the padding of the central pane summing two sides
+const centralPanePadding = 40;
+
+// 80 is the width of the button on the right side
+const rightButtonWidth = 80;
+
+function SearchRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: SearchRowSkeletonProps) {
const styles = useThemeStyles();
- const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
+ const {windowWidth, isSmallScreenWidth, isLargeScreenWidth} = useWindowDimensions();
if (isSmallScreenWidth) {
return (
(
@@ -51,7 +66,7 @@ function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListI
height={4}
/>
);
}
+
return (
(
<>
-
+ {isLargeScreenWidth && (
+ <>
+
+
+
+ >
+ )}
+
+
>
)}
@@ -146,6 +181,6 @@ function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListI
);
}
-TableListItemSkeleton.displayName = 'TableListItemSkeleton';
+SearchRowSkeleton.displayName = 'SearchRowSkeleton';
-export default TableListItemSkeleton;
+export default SearchRowSkeleton;
diff --git a/src/components/Skeletons/TableRowSkeleton.tsx b/src/components/Skeletons/TableRowSkeleton.tsx
new file mode 100644
index 000000000000..865bffc5842f
--- /dev/null
+++ b/src/components/Skeletons/TableRowSkeleton.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import {Circle, Rect} from 'react-native-svg';
+import useThemeStyles from '@hooks/useThemeStyles';
+import ItemListSkeletonView from './ItemListSkeletonView';
+
+type TableListItemSkeletonProps = {
+ shouldAnimate?: boolean;
+ fixedNumItems?: number;
+ gradientOpacityEnabled?: boolean;
+};
+
+const barHeight = '8';
+const shortBarWidth = '60';
+const longBarWidth = '124';
+
+function TableListItemSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: TableListItemSkeletonProps) {
+ const styles = useThemeStyles();
+
+ return (
+ (
+ <>
+
+
+
+ >
+ )}
+ />
+ );
+}
+
+TableListItemSkeleton.displayName = 'TableListItemSkeleton';
+
+export default TableListItemSkeleton;
diff --git a/src/components/SpacerView.tsx b/src/components/SpacerView.tsx
index 23c109f7d589..bb762da1226b 100644
--- a/src/components/SpacerView.tsx
+++ b/src/components/SpacerView.tsx
@@ -35,10 +35,11 @@ function SpacerView({shouldShow, style}: SpacerViewProps) {
marginVertical: shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_MARGIN_VERTICAL : CONST.HORIZONTAL_SPACER.HIDDEN_MARGIN_VERTICAL,
borderBottomWidth: shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_BORDER_BOTTOM_WIDTH : CONST.HORIZONTAL_SPACER.HIDDEN_BORDER_BOTTOM_WIDTH,
};
+ // eslint-disable-next-line react-compiler/react-compiler
marginVertical.value = values.marginVertical;
borderBottomWidth.value = values.borderBottomWidth;
- // eslint-disable-next-line react-hooks/exhaustive-deps -- we only need to trigger when shouldShow prop is changed
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we only need to trigger when shouldShow prop is changed
}, [shouldShow, prevShouldShow]);
return ;
diff --git a/src/components/SplashScreenHider/index.native.tsx b/src/components/SplashScreenHider/index.native.tsx
index e31f391f7376..7c579519c926 100644
--- a/src/components/SplashScreenHider/index.native.tsx
+++ b/src/components/SplashScreenHider/index.native.tsx
@@ -34,6 +34,7 @@ function SplashScreenHider({onHide = () => {}}: SplashScreenHiderProps): SplashS
hideHasBeenCalled.current = true;
BootSplash.hide().then(() => {
+ // eslint-disable-next-line react-compiler/react-compiler
scale.value = withTiming(0, {
duration: 200,
easing: Easing.back(2),
diff --git a/src/components/StateSelector.tsx b/src/components/StateSelector.tsx
index 2481c29d8123..e2be9281d0bb 100644
--- a/src/components/StateSelector.tsx
+++ b/src/components/StateSelector.tsx
@@ -3,7 +3,7 @@ import {CONST as COMMON_CONST} from 'expensify-common';
import React, {useEffect, useRef} from 'react';
import type {ForwardedRef} from 'react';
import type {View} from 'react-native';
-import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute';
+import useGeographicalStateAndCountryFromRoute from '@hooks/useGeographicalStateAndCountryFromRoute';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
@@ -43,7 +43,7 @@ function StateSelector(
) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const stateFromUrl = useGeographicalStateFromRoute();
+ const {state: stateFromUrl} = useGeographicalStateAndCountryFromRoute();
const didOpenStateSelector = useRef(false);
const isFocused = useIsFocused();
@@ -71,7 +71,7 @@ function StateSelector(
// This helps prevent issues where the component might not update correctly if the state is controlled by both the parent and the URL.
Navigation.setParams({state: undefined});
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [stateFromUrl, onBlur, isFocused]);
const title = stateCode && Object.keys(COMMON_CONST.STATES).includes(stateCode) ? translate(`allStates.${stateCode}.stateName`) : '';
diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx
index 2e29008cd9ec..1ddc65bbd0fc 100644
--- a/src/components/Switch.tsx
+++ b/src/components/Switch.tsx
@@ -23,6 +23,9 @@ type SwitchProps = {
/** Whether to show the lock icon even if the switch is enabled */
showLockIcon?: boolean;
+
+ /** Callback to fire when the switch is toggled in disabled state */
+ disabledAction?: () => void;
};
const OFFSET_X = {
@@ -30,13 +33,17 @@ const OFFSET_X = {
ON: 20,
};
-function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon}: SwitchProps) {
+function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, disabledAction}: SwitchProps) {
const styles = useThemeStyles();
const offsetX = useRef(new Animated.Value(isOn ? OFFSET_X.ON : OFFSET_X.OFF));
const theme = useTheme();
const handleSwitchPress = () => {
InteractionManager.runAfterInteractions(() => {
+ if (disabled) {
+ disabledAction?.();
+ return;
+ }
onToggle(!isOn);
});
};
@@ -51,7 +58,7 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon}: Sw
return (
(ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))}
+ onPress={Session.checkIfActionIsAllowed(() => {
+ // If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page.
+ if (TaskUtils.isActiveTaskEditRoute(report.reportID)) {
+ return;
+ }
+ if (ReportUtils.isCompletedTaskReport(report)) {
+ Task.reopenTask(report);
+ } else {
+ Task.completeTask(report);
+ }
+ })}
style={styles.flex1}
/>
diff --git a/src/components/TestToolsModal.tsx b/src/components/TestToolsModal.tsx
index ad1c65e76a4b..5c330bd700e0 100644
--- a/src/components/TestToolsModal.tsx
+++ b/src/components/TestToolsModal.tsx
@@ -31,7 +31,7 @@ type TestToolsModalOnyxProps = {
type TestToolsModalProps = TestToolsModalOnyxProps;
function TestToolsModal({isTestToolsModalOpen = false, shouldStoreLogs = false}: TestToolsModalProps) {
- const {isDevelopment} = useEnvironment();
+ const {isProduction} = useEnvironment();
const {windowWidth} = useWindowDimensions();
const StyleUtils = useStyleUtils();
const styles = useThemeStyles();
@@ -44,7 +44,6 @@ function TestToolsModal({isTestToolsModalOpen = false, shouldStoreLogs = false}:
onClose={toggleTestToolsModal}
>
- {isDevelopment && }
)}
+ {!isProduction && }
);
diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index 8f685fb668e7..59f205da023f 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -64,6 +64,7 @@ function BaseTextInput(
shouldShowClearButton = false,
prefixContainerStyle = [],
prefixStyle = [],
+ contentWidth,
...props
}: BaseTextInputProps,
ref: ForwardedRef,
@@ -108,7 +109,7 @@ function BaseTextInput(
}
input.current.focus();
// We only want this to run on mount
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
const animateLabel = useCallback(
@@ -251,12 +252,14 @@ function BaseTextInput(
const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([
styles.textInputContainer,
textInputContainerStyles,
- autoGrow && StyleUtils.getWidthStyle(textInputWidth),
+ (autoGrow || !!contentWidth) && StyleUtils.getWidthStyle(textInputWidth),
!hideFocusedState && isFocused && styles.borderColorFocus,
(!!hasError || !!errorText) && styles.borderColorDanger,
autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined},
]);
+ const inputPaddingLeft = !!prefixCharacter && StyleUtils.getPaddingLeft(StyleUtils.getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft);
+
return (
<>
@@ -341,7 +344,7 @@ function BaseTextInput(
styles.w100,
inputStyle,
(!hasLabel || isMultiline) && styles.pv0,
- !!prefixCharacter && StyleUtils.getPaddingLeft(StyleUtils.getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft),
+ inputPaddingLeft,
inputProps.secureTextEntry && styles.secureInput,
!isMultiline && {height, lineHeight: undefined},
@@ -411,6 +414,29 @@ function BaseTextInput(
/>
)}
+ {contentWidth && (
+ {
+ if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
+ return;
+ }
+ setTextInputWidth(e.nativeEvent.layout.width);
+ setTextInputHeight(e.nativeEvent.layout.height);
+ }}
+ >
+
+ {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
+ {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
+
+
+ )}
{/*
Text input component doesn't support auto grow by default.
We're using a hidden text input to achieve that.
diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx
index 3a1032ff7a43..685d54d86765 100644
--- a/src/components/TextInput/BaseTextInput/index.tsx
+++ b/src/components/TextInput/BaseTextInput/index.tsx
@@ -66,6 +66,7 @@ function BaseTextInput(
shouldShowClearButton = false,
prefixContainerStyle = [],
prefixStyle = [],
+ contentWidth,
...inputProps
}: BaseTextInputProps,
ref: ForwardedRef,
@@ -108,7 +109,7 @@ function BaseTextInput(
}
input.current.focus();
// We only want this to run on mount
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
const animateLabel = useCallback(
@@ -248,7 +249,7 @@ function BaseTextInput(
const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([
styles.textInputContainer,
textInputContainerStyles,
- autoGrow && StyleUtils.getWidthStyle(textInputWidth),
+ (autoGrow || !!contentWidth) && StyleUtils.getWidthStyle(textInputWidth),
!hideFocusedState && isFocused && styles.borderColorFocus,
(!!hasError || !!errorText) && styles.borderColorDanger,
autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined},
@@ -274,6 +275,8 @@ function BaseTextInput(
return undefined;
}, [inputStyle]);
+ const inputPaddingLeft = !!prefixCharacter && StyleUtils.getPaddingLeft(StyleUtils.getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft);
+
return (
<>
)}
+ {contentWidth && (
+ {
+ if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
+ return;
+ }
+ setTextInputWidth(e.nativeEvent.layout.width);
+ setTextInputHeight(e.nativeEvent.layout.height);
+ }}
+ >
+
+ {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
+ {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
+
+
+ )}
{/*
Text input component doesn't support auto grow by default.
We're using a hidden text input to achieve that.
diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts
index 7a46cca693e3..80325e0a21f3 100644
--- a/src/components/TextInput/BaseTextInput/types.ts
+++ b/src/components/TextInput/BaseTextInput/types.ts
@@ -120,6 +120,9 @@ type CustomBaseTextInputProps = {
/** Style for the prefix container */
prefixContainerStyle?: StyleProp;
+
+ /** The width of inner content */
+ contentWidth?: number;
};
type BaseTextInputRef = HTMLFormElement | AnimatedTextInputRef;
diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx
index e1083b88414b..3136a43e8669 100644
--- a/src/components/TextInput/TextInputLabel/index.tsx
+++ b/src/components/TextInput/TextInputLabel/index.tsx
@@ -16,7 +16,7 @@ function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}:
return;
}
labelRef.current.setAttribute('for', inputId);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
return (
diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx
index a9dda9e956cf..4070714c798e 100644
--- a/src/components/TextInput/index.tsx
+++ b/src/components/TextInput/index.tsx
@@ -40,7 +40,7 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef;
+} & Pick;
export default TextInputWithCurrencySymbolProps;
diff --git a/src/components/TextPicker/TextSelectorModal.tsx b/src/components/TextPicker/TextSelectorModal.tsx
index a867a7030b54..bea95df3dbfe 100644
--- a/src/components/TextPicker/TextSelectorModal.tsx
+++ b/src/components/TextPicker/TextSelectorModal.tsx
@@ -1,11 +1,13 @@
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';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
import TextInput from '@components/TextInput';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useLocalize from '@hooks/useLocalize';
@@ -14,7 +16,7 @@ import CONST from '@src/CONST';
import type {TextSelectorModalProps} from './types';
import usePaddingStyle from './usePaddingStyle';
-function TextSelectorModal({value, description = '', onValueSelected, isVisible, onClose, ...rest}: TextSelectorModalProps) {
+function TextSelectorModal({value, description = '', subtitle, onValueSelected, isVisible, onClose, shouldClearOnClose, ...rest}: TextSelectorModalProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -22,13 +24,26 @@ function TextSelectorModal({value, description = '', onValueSelected, isVisible,
const paddingStyle = usePaddingStyle();
const inputRef = useRef(null);
+ const inputValueRef = useRef(value);
const focusTimeoutRef = useRef(null);
+ const hide = useCallback(() => {
+ onClose();
+ if (shouldClearOnClose) {
+ setValue('');
+ }
+ }, [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) {
@@ -44,8 +59,8 @@ function TextSelectorModal({value, description = '', onValueSelected, isVisible,
+ {!!subtitle && {subtitle} }
&
+
+ /** Whether to clear the input value when the modal closes */
+ shouldClearOnClose?: boolean;
+} & Pick &
TextProps;
type TextPickerProps = {
@@ -39,7 +42,7 @@ type TextPickerProps = {
/** Whether to show the tooltip text */
shouldShowTooltips?: boolean;
-} & Pick &
+} & Pick &
TextProps;
export type {TextSelectorModalProps, TextPickerProps};
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 (
-
+
{}}: Tim
handleMinutesChange(insertAtPosition(minutes, trimmedKey, selectionMinute.start, selectionMinute.end));
}
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[minutes, hours, selectionMinute, selectionHour],
);
@@ -381,7 +381,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}}: Tim
focusHourInputOnLastCharacter();
}
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[selectionHour, selectionMinute],
);
const arrowRightCallback = useCallback(
@@ -394,7 +394,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}}: Tim
focusMinuteInputOnFirstCharacter();
}
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[selectionHour, selectionMinute],
);
@@ -409,7 +409,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}}: Tim
e.preventDefault();
focusHourInputOnLastCharacter();
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[selectionMinute.start, selectionMinute.end, focusHourInputOnLastCharacter],
);
@@ -430,7 +430,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}}: Tim
useEffect(() => {
onInputChange(`${hours}:${minutes} ${amPmValue}`);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [hours, minutes, amPmValue]);
const handleSubmit = () => {
@@ -463,6 +463,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}}: Tim
// eslint-disable-next-line no-param-reassign
ref.current = {hourRef: textInputRef as TextInput | null, minuteRef: minuteInputRef.current};
}
+ // eslint-disable-next-line react-compiler/react-compiler
hourInputRef.current = textInputRef as TextInput | null;
}}
onSelectionChange={(e) => {
diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
index 1398d74bbd67..cb158150fc88 100644
--- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
+++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
@@ -1,5 +1,5 @@
import React, {memo, useEffect, useRef} from 'react';
-import type {LayoutEvent} from 'react-native';
+import type {LayoutChangeEvent} from 'react-native';
import GenericTooltip from '@components/Tooltip/GenericTooltip';
import type TooltipProps from '@components/Tooltip/types';
import getBounds from './getBounds';
@@ -41,9 +41,10 @@ function BaseEducationalTooltip({children, ...props}: TooltipProps) {
{...props}
>
{({showTooltip, hideTooltip, updateTargetBounds}) => {
+ // eslint-disable-next-line react-compiler/react-compiler
hideTooltipRef.current = hideTooltip;
return React.cloneElement(children as React.ReactElement, {
- onLayout: (e: LayoutEvent) => {
+ onLayout: (e: LayoutChangeEvent) => {
updateTargetBounds(getBounds(e));
showTooltip();
},
diff --git a/src/components/Tooltip/EducationalTooltip/getBounds/index.native.ts b/src/components/Tooltip/EducationalTooltip/getBounds/index.native.ts
index e0f06785d338..44e34ba5ff21 100644
--- a/src/components/Tooltip/EducationalTooltip/getBounds/index.native.ts
+++ b/src/components/Tooltip/EducationalTooltip/getBounds/index.native.ts
@@ -1,6 +1,6 @@
-import type {LayoutEvent} from 'react-native';
+import type {LayoutChangeEvent} from 'react-native';
import type GetBounds from './types';
-const getBounds: GetBounds = (layoutEvent: LayoutEvent) => layoutEvent.nativeEvent.layout;
+const getBounds: GetBounds = (event: LayoutChangeEvent) => event.nativeEvent.layout;
export default getBounds;
diff --git a/src/components/Tooltip/EducationalTooltip/getBounds/index.ts b/src/components/Tooltip/EducationalTooltip/getBounds/index.ts
index be728b519c51..d94949277740 100644
--- a/src/components/Tooltip/EducationalTooltip/getBounds/index.ts
+++ b/src/components/Tooltip/EducationalTooltip/getBounds/index.ts
@@ -1,6 +1,6 @@
-import type {LayoutEvent} from 'react-native';
+import type {LayoutChangeEvent} from 'react-native';
import type GetBounds from './types';
-const getBounds: GetBounds = (layoutEvent: LayoutEvent) => (layoutEvent.nativeEvent.target as HTMLElement).getBoundingClientRect();
+const getBounds: GetBounds = (event: LayoutChangeEvent) => (event.nativeEvent.target as HTMLElement).getBoundingClientRect();
export default getBounds;
diff --git a/src/components/Tooltip/EducationalTooltip/getBounds/types.ts b/src/components/Tooltip/EducationalTooltip/getBounds/types.ts
index 5edf6f60e0c6..081962166ff1 100644
--- a/src/components/Tooltip/EducationalTooltip/getBounds/types.ts
+++ b/src/components/Tooltip/EducationalTooltip/getBounds/types.ts
@@ -1,5 +1,5 @@
-import type {LayoutEvent, LayoutRectangle} from 'react-native';
+import type {LayoutChangeEvent, LayoutRectangle} from 'react-native';
-type GetBounds = (layoutEvent: LayoutEvent) => LayoutRectangle;
+type GetBounds = (event: LayoutChangeEvent) => LayoutRectangle;
export default GetBounds;
diff --git a/src/components/Tooltip/PopoverAnchorTooltip.tsx b/src/components/Tooltip/PopoverAnchorTooltip.tsx
index 693de83fa5d7..5eb1f45dafcc 100644
--- a/src/components/Tooltip/PopoverAnchorTooltip.tsx
+++ b/src/components/Tooltip/PopoverAnchorTooltip.tsx
@@ -10,7 +10,7 @@ function PopoverAnchorTooltip({shouldRender = true, children, ...props}: Tooltip
const isPopoverRelatedToTooltipOpen = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/dot-notation
- const tooltipNode: Node | null = tooltipRef.current?.['_childNode'] ?? null;
+ const tooltipNode = (tooltipRef.current?.['_childNode'] as Node | undefined) ?? null;
if (
isOpen &&
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx
index 8230f9132d00..c6a244be3fcb 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.tsx
+++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx
@@ -124,6 +124,7 @@ function BaseVideoPlayer({
if (videoResumeTryNumber.current === 1) {
playVideo();
}
+ // eslint-disable-next-line react-compiler/react-compiler
videoResumeTryNumber.current -= 1;
},
[playVideo, videoResumeTryNumber],
@@ -160,7 +161,7 @@ function BaseVideoPlayer({
videoStateRef.current = status;
onPlaybackStatusUpdate?.(status);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to trigger this when isPlaying changes because isPlaying is only used inside shouldReplayVideo
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this when isPlaying changes because isPlaying is only used inside shouldReplayVideo
[onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration],
);
diff --git a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx
index c9cf2c25d7ad..5d1ea0d85d0b 100644
--- a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx
+++ b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx
@@ -30,6 +30,7 @@ function ProgressBar({duration, position, seekPosition}: ProgressBarProps) {
const wasVideoPlayingOnCheck = useSharedValue(false);
const onCheckVideoPlaying = (isPlaying: boolean) => {
+ // eslint-disable-next-line react-compiler/react-compiler
wasVideoPlayingOnCheck.value = isPlaying;
};
diff --git a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx
index 011391ed4c71..abe1748c18b9 100644
--- a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx
+++ b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx
@@ -45,6 +45,7 @@ function VolumeButton({style, small = false}: VolumeButtonProps) {
const changeVolumeOnPan = useCallback(
(event: GestureStateChangeEvent | GestureUpdateEvent) => {
const val = NumberUtils.roundToTwoDecimalPlaces(1 - event.y / sliderHeight);
+ // eslint-disable-next-line react-compiler/react-compiler
volume.value = NumberUtils.clamp(val, 0, 1);
},
[sliderHeight, volume],
diff --git a/src/components/VideoPlayer/VideoPlayerControls/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/index.tsx
index f5461159aa8e..04cb69a96f8b 100644
--- a/src/components/VideoPlayer/VideoPlayerControls/index.tsx
+++ b/src/components/VideoPlayer/VideoPlayerControls/index.tsx
@@ -72,6 +72,7 @@ function VideoPlayerControls({
};
const enterFullScreenMode = useCallback(() => {
+ // eslint-disable-next-line react-compiler/react-compiler
isFullScreenRef.current = true;
updateCurrentlyPlayingURL(url);
videoPlayerRef.current?.presentFullscreenPlayer();
diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
index 0958ec148c3d..bfb7e3739d17 100644
--- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
+++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
@@ -62,6 +62,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) {
updatePlaybackSpeed(speed);
},
shouldPutLeftPaddingWhenNoIcon: true,
+ isSelected: currentPlaybackSpeed === speed,
})),
});
return items;
diff --git a/src/components/VideoPlayerContexts/VolumeContext.tsx b/src/components/VideoPlayerContexts/VolumeContext.tsx
index d67b989e5887..f22b524848de 100644
--- a/src/components/VideoPlayerContexts/VolumeContext.tsx
+++ b/src/components/VideoPlayerContexts/VolumeContext.tsx
@@ -16,6 +16,7 @@ function VolumeContextProvider({children}: ChildrenProps) {
return;
}
currentVideoPlayerRef.current.setStatusAsync({volume: newVolume, isMuted: newVolume === 0});
+ // eslint-disable-next-line react-compiler/react-compiler
volume.value = newVolume;
},
[currentVideoPlayerRef, volume],
diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx
index 719438b87931..5900e3097b96 100644
--- a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx
+++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx
@@ -40,7 +40,7 @@ function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel}: Video
)}
- {({anchor, report, action, checkIfContextMenuActive}) => (
+ {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => (
DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
- onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ onLongPress={(event) =>
+ showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs))
+ }
shouldUseHapticsOnLongPress
>
diff --git a/src/components/WorkspaceEmptyStateSection.tsx b/src/components/WorkspaceEmptyStateSection.tsx
index 6a00aa4bf5eb..252197fda96b 100644
--- a/src/components/WorkspaceEmptyStateSection.tsx
+++ b/src/components/WorkspaceEmptyStateSection.tsx
@@ -14,6 +14,9 @@ type WorkspaceEmptyStateSectionProps = {
/** The text to display in the subtitle of the section */
subtitle?: string;
+ /** The component to show in the subtitle of the section */
+ subtitleComponent?: React.ReactNode;
+
/** The icon to display along with the title */
icon: IconAsset;
@@ -24,7 +27,7 @@ type WorkspaceEmptyStateSectionProps = {
shouldStyleAsCard?: boolean;
};
-function WorkspaceEmptyStateSection({icon, subtitle, title, containerStyle, shouldStyleAsCard = true}: WorkspaceEmptyStateSectionProps) {
+function WorkspaceEmptyStateSection({icon, subtitle, title, containerStyle, shouldStyleAsCard = true, subtitleComponent}: WorkspaceEmptyStateSectionProps) {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -50,9 +53,9 @@ function WorkspaceEmptyStateSection({icon, subtitle, title, containerStyle, shou
{title}
- {!!subtitle && (
+ {(!!subtitle || !!subtitleComponent) && (
- {subtitle}
+ {subtitleComponent ?? {subtitle} }
)}
diff --git a/src/components/withNavigationTransitionEnd.tsx b/src/components/withNavigationTransitionEnd.tsx
index 417d8828c1e4..83f14a1d58ef 100644
--- a/src/components/withNavigationTransitionEnd.tsx
+++ b/src/components/withNavigationTransitionEnd.tsx
@@ -18,7 +18,7 @@ export default function (WrappedComponent: ComponentType | undefined {
+ return useContext(ActiveCentralPaneRouteContext);
+}
+
+export default useActiveCentralPaneRoute;
diff --git a/src/hooks/useActiveRoute.ts b/src/hooks/useActiveRoute.ts
deleted file mode 100644
index 812e7c634ee8..000000000000
--- a/src/hooks/useActiveRoute.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import {useContext} from 'react';
-import ActiveRouteContext from '@libs/Navigation/AppNavigator/Navigators/ActiveRouteContext';
-import type {AuthScreensParamList, NavigationPartialRoute} from '@libs/Navigation/types';
-
-function useActiveRoute(): NavigationPartialRoute | undefined {
- return useContext(ActiveRouteContext);
-}
-
-export default useActiveRoute;
diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts
index bbae790f23a2..e474a9455d4d 100644
--- a/src/hooks/useAnimatedHighlightStyle/index.ts
+++ b/src/hooks/useAnimatedHighlightStyle/index.ts
@@ -82,6 +82,7 @@ export default function useAnimatedHighlightStyle({
return;
}
+ // eslint-disable-next-line react-compiler/react-compiler
repeatableProgress.value = withSequence(
withDelay(highlightStartDelay, withTiming(1, {duration: highlightStartDuration, easing: Easing.inOut(Easing.ease)})),
withDelay(highlightEndDelay, withTiming(0, {duration: highlightEndDuration, easing: Easing.inOut(Easing.ease)})),
diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts
index 0927c0915716..2de6cc251560 100644
--- a/src/hooks/useArrowKeyFocusManager.ts
+++ b/src/hooks/useArrowKeyFocusManager.ts
@@ -74,7 +74,7 @@ export default function useArrowKeyFocusManager({
return;
}
onFocusedIndexChange(focusedIndex);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [focusedIndex, prevIsFocusedIndex]);
const arrowUpCallback = useCallback(() => {
diff --git a/src/hooks/useCopySelectionHelper.ts b/src/hooks/useCopySelectionHelper.ts
index ed379bfcf2e6..9bcb9b8b0139 100644
--- a/src/hooks/useCopySelectionHelper.ts
+++ b/src/hooks/useCopySelectionHelper.ts
@@ -1,7 +1,7 @@
import {useEffect} from 'react';
import Clipboard from '@libs/Clipboard';
import KeyboardShortcut from '@libs/KeyboardShortcut';
-import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser';
+import Parser from '@libs/Parser';
import SelectionScraper from '@libs/SelectionScraper';
import CONST from '@src/CONST';
@@ -11,10 +11,10 @@ function copySelectionToClipboard() {
return;
}
if (!Clipboard.canSetHtml()) {
- Clipboard.setString(parseHtmlToMarkdown(selection));
+ Clipboard.setString(Parser.htmlToMarkdown(selection));
return;
}
- Clipboard.setHtml(selection, parseHtmlToText(selection));
+ Clipboard.setHtml(selection, Parser.htmlToText(selection));
}
export default function useCopySelectionHelper() {
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/useGeographicalStateAndCountryFromRoute.ts b/src/hooks/useGeographicalStateAndCountryFromRoute.ts
new file mode 100644
index 000000000000..b94644bdd287
--- /dev/null
+++ b/src/hooks/useGeographicalStateAndCountryFromRoute.ts
@@ -0,0 +1,27 @@
+import {useRoute} from '@react-navigation/native';
+import {CONST as COMMON_CONST} from 'expensify-common';
+import CONST from '@src/CONST';
+
+type State = keyof typeof COMMON_CONST.STATES;
+type Country = keyof typeof CONST.ALL_COUNTRIES;
+type StateAndCountry = {state?: State; country?: Country};
+
+/**
+ * Extracts the 'state' and 'country' query parameters from the route/ url and validates it against COMMON_CONST.STATES and CONST.ALL_COUNTRIES.
+ * Example 1: Url: https://new.expensify.com/settings/profile/address?state=MO Returns: state=MO
+ * Example 2: Url: https://new.expensify.com/settings/profile/address?state=ASDF Returns: state=undefined
+ * Example 3: Url: https://new.expensify.com/settings/profile/address Returns: state=undefined
+ * Example 4: Url: https://new.expensify.com/settings/profile/address?state=MO-hash-a12341 Returns: state=MO
+ * Similarly for country parameter.
+ */
+export default function useGeographicalStateAndCountryFromRoute(stateParamName = 'state', countryParamName = 'country'): StateAndCountry {
+ const routeParams = useRoute().params as Record;
+
+ const stateFromUrlTemp = routeParams?.[stateParamName] as string | undefined;
+ const countryFromUrlTemp = routeParams?.[countryParamName] as string | undefined;
+
+ return {
+ state: COMMON_CONST.STATES[stateFromUrlTemp as State]?.stateISO,
+ country: Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFromUrlTemp) as Country,
+ };
+}
diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts
deleted file mode 100644
index 434d4c534d61..000000000000
--- a/src/hooks/useGeographicalStateFromRoute.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {useRoute} from '@react-navigation/native';
-import type {ParamListBase, RouteProp} from '@react-navigation/native';
-import {CONST as COMMON_CONST} from 'expensify-common';
-
-type CustomParamList = ParamListBase & Record>;
-type State = keyof typeof COMMON_CONST.STATES;
-
-/**
- * Extracts the 'state' (default) query parameter from the route/ url and validates it against COMMON_CONST.STATES, returning its ISO code or `undefined`.
- * Example 1: Url: https://new.expensify.com/settings/profile/address?state=MO Returns: MO
- * Example 2: Url: https://new.expensify.com/settings/profile/address?state=ASDF Returns: undefined
- * Example 3: Url: https://new.expensify.com/settings/profile/address Returns: undefined
- * Example 4: Url: https://new.expensify.com/settings/profile/address?state=MO-hash-a12341 Returns: MO
- */
-export default function useGeographicalStateFromRoute(stateParamName = 'state'): State | undefined {
- const route = useRoute>();
- const stateFromUrlTemp = route.params?.[stateParamName] as string | undefined;
-
- if (!stateFromUrlTemp) {
- return;
- }
- return COMMON_CONST.STATES[stateFromUrlTemp as State]?.stateISO;
-}
diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts
index 4705a170c3bd..022d6178877d 100644
--- a/src/hooks/useHtmlPaste/index.ts
+++ b/src/hooks/useHtmlPaste/index.ts
@@ -1,6 +1,7 @@
import {useNavigation} from '@react-navigation/native';
import {useCallback, useEffect} from 'react';
-import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser';
+import type {ClipboardEvent as PasteEvent} from 'react';
+import Parser from '@libs/Parser';
import type UseHtmlPaste from './types';
const insertByCommand = (text: string) => {
@@ -20,8 +21,10 @@ const insertAtCaret = (target: HTMLElement, text: string) => {
range.setEnd(node, node.length);
selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
- // Dispatch paste event to simulate real browser behavior
- target.dispatchEvent(new Event('paste', {bubbles: true}));
+ // Dispatch paste event to make Markdown Input properly set cursor position
+ const pasteEvent = new ClipboardEvent('paste', {bubbles: true, cancelable: true});
+ (pasteEvent as unknown as PasteEvent).isDefaultPrevented = () => false;
+ target.dispatchEvent(pasteEvent);
// Dispatch input event to trigger Markdown Input to parse the new text
target.dispatchEvent(new Event('input', {bubbles: true}));
} else {
@@ -61,7 +64,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
// eslint-disable-next-line no-empty
} catch (e) {}
// We only need to set the callback once.
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
/**
@@ -71,7 +74,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
*/
const handlePastedHTML = useCallback(
(html: string) => {
- paste(parseHtmlToMarkdown(html));
+ paste(Parser.htmlToMarkdown(html));
},
[paste],
);
@@ -131,7 +134,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
}
handlePastePlainText(event);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[handlePastedHTML, handlePastePlainText, preHtmlPasteCallback],
);
@@ -142,20 +145,20 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
let unsubscribeFocus: () => void;
let unsubscribeBlur: () => void;
if (removeListenerOnScreenBlur) {
- unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste));
- unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste));
+ unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste, true));
+ unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste, true));
}
- document.addEventListener('paste', handlePaste);
+ document.addEventListener('paste', handlePaste, true);
return () => {
if (removeListenerOnScreenBlur) {
unsubscribeFocus();
unsubscribeBlur();
}
- document.removeEventListener('paste', handlePaste);
+ document.removeEventListener('paste', handlePaste, true);
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
};
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/useKeyboardShortcut.ts b/src/hooks/useKeyboardShortcut.ts
index 1c5bbc426ef2..90f43b4520d1 100644
--- a/src/hooks/useKeyboardShortcut.ts
+++ b/src/hooks/useKeyboardShortcut.ts
@@ -63,6 +63,6 @@ export default function useKeyboardShortcut(shortcut: Shortcut, callback: (e?: G
return () => {
unsubscribe();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isActive, callback, captureOnInputs, excludedNodes, priority, shortcut.descriptionKey, shortcut.modifiers.join(), shortcut.shortcutKey, shouldBubble, shouldPreventDefault]);
}
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/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts
index e21e2a77268c..51076b0818d4 100644
--- a/src/hooks/useMarkdownStyle.ts
+++ b/src/hooks/useMarkdownStyle.ts
@@ -9,7 +9,8 @@ const defaultEmptyArray: Array = [];
function useMarkdownStyle(message: string | null = null, excludeStyles: Array = defaultEmptyArray): MarkdownStyle {
const theme = useTheme();
- const emojiFontSize = containsOnlyEmojis(message ?? '') ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal;
+ const hasMessageOnlyEmojis = message != null && message.length > 0 && containsOnlyEmojis(message);
+ const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal;
// this map is used to reset the styles that are not needed - passing undefined value can break the native side
const nonStylingDefaultValues: Record = useMemo(
diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts
index 44a82253b7c0..918831f27545 100644
--- a/src/hooks/usePaginatedReportActions.ts
+++ b/src/hooks/usePaginatedReportActions.ts
@@ -1,5 +1,6 @@
import {useMemo} from 'react';
import {useOnyx} from 'react-native-onyx';
+import PaginationUtils from '@libs/PaginationUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -10,23 +11,29 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) {
// Use `||` instead of `??` to handle empty string.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const reportIDWithDefault = reportID || '-1';
- const [sortedAllReportActions = []] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, {
+
+ const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, {
canEvict: false,
selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
});
+ const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`);
const reportActions = useMemo(() => {
- if (!sortedAllReportActions.length) {
+ if (!sortedAllReportActions?.length) {
return [];
}
- return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionID);
- }, [reportActionID, sortedAllReportActions]);
+ return PaginationUtils.getContinuousChain(sortedAllReportActions, reportActionPages ?? [], (reportAction) => reportAction.reportActionID, reportActionID);
+ }, [reportActionID, reportActionPages, sortedAllReportActions]);
- const linkedAction = useMemo(() => sortedAllReportActions.find((obj) => String(obj.reportActionID) === String(reportActionID)), [reportActionID, sortedAllReportActions]);
+ const linkedAction = useMemo(
+ () => sortedAllReportActions?.find((reportAction) => String(reportAction.reportActionID) === String(reportActionID)),
+ [reportActionID, sortedAllReportActions],
+ );
return {
reportActions,
linkedAction,
+ sortedAllReportActions,
};
}
diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx
index a90793857293..38622e17d2be 100644
--- a/src/hooks/useReportIDs.tsx
+++ b/src/hooks/useReportIDs.tsx
@@ -3,7 +3,6 @@ import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportUtils from '@libs/ReportUtils';
import SidebarUtils from '@libs/SidebarUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -13,7 +12,6 @@ import useActiveWorkspace from './useActiveWorkspace';
import useCurrentReportID from './useCurrentReportID';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
-type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean};
type PolicySelector = Pick;
type ReportActionsSelector = Array>;
@@ -25,56 +23,19 @@ type ReportIDsContextProviderProps = {
type ReportIDsContextValue = {
orderedReportIDs: string[];
currentReportID: string;
+ policyMemberAccountIDs: number[];
};
const ReportIDsContext = createContext({
orderedReportIDs: [],
currentReportID: '',
+ policyMemberAccountIDs: [],
});
/**
* This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering
* and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI.
*/
-const chatReportSelector = (report: OnyxEntry): ChatReportSelector =>
- (report && {
- reportID: report.reportID,
- participants: report.participants,
- isPinned: report.isPinned,
- isHidden: report.isHidden,
- notificationPreference: report.notificationPreference,
- errorFields: {
- addWorkspaceRoom: report.errorFields?.addWorkspaceRoom,
- },
- lastMessageText: report.lastMessageText,
- lastVisibleActionCreated: report.lastVisibleActionCreated,
- iouReportID: report.iouReportID,
- total: report.total,
- nonReimbursableTotal: report.nonReimbursableTotal,
- hasOutstandingChildRequest: report.hasOutstandingChildRequest,
- isWaitingOnBankAccount: report.isWaitingOnBankAccount,
- statusNum: report.statusNum,
- stateNum: report.stateNum,
- chatType: report.chatType,
- type: report.type,
- policyID: report.policyID,
- visibility: report.visibility,
- lastReadTime: report.lastReadTime,
- // Needed for name sorting:
- reportName: report.reportName,
- policyName: report.policyName,
- oldPolicyName: report.oldPolicyName,
- // Other less obvious properites considered for sorting:
- ownerAccountID: report.ownerAccountID,
- currency: report.currency,
- managerID: report.managerID,
- // Other important less obivous properties for filtering:
- parentReportActionID: report.parentReportActionID,
- parentReportID: report.parentReportID,
- isDeletedParentAction: report.isDeletedParentAction,
- isUnreadWithMention: ReportUtils.isUnreadWithMention(report),
- }) as ChatReportSelector;
-
const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector =>
(reportActions &&
Object.values(reportActions)
@@ -118,7 +79,7 @@ function ReportIDsContextProvider({
currentReportIDForTests,
}: ReportIDsContextProviderProps) {
const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE, {initialValue: CONST.PRIORITY_MODE.DEFAULT});
- const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: chatReportSelector});
+ const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policySelector});
const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: reportActionsSelector});
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
@@ -130,7 +91,7 @@ function ReportIDsContextProvider({
const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportID;
const {activeWorkspaceID} = useActiveWorkspace();
- const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID);
+ const policyMemberAccountIDs = useMemo(() => getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID), [policies, activeWorkspaceID, accountID]);
const getOrderedReportIDs = useCallback(
(currentReportID?: string) =>
@@ -146,7 +107,7 @@ function ReportIDsContextProvider({
policyMemberAccountIDs,
),
// we need reports draft in deps array for reloading of list when reportsDrafts will change
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, reportsDrafts],
);
@@ -157,15 +118,16 @@ function ReportIDsContextProvider({
// we first generate the list as if there was no current report, then we check if
// the current report is missing from the list, which should very rarely happen. In this
// case we re-generate the list a 2nd time with the current report included.
- if (derivedCurrentReportID && !orderedReportIDs.includes(derivedCurrentReportID)) {
- return {orderedReportIDs: getOrderedReportIDs(derivedCurrentReportID), currentReportID: derivedCurrentReportID ?? '-1'};
+ if (derivedCurrentReportID && derivedCurrentReportID !== '-1' && orderedReportIDs.indexOf(derivedCurrentReportID) === -1) {
+ return {orderedReportIDs: getOrderedReportIDs(derivedCurrentReportID), currentReportID: derivedCurrentReportID ?? '-1', policyMemberAccountIDs};
}
return {
orderedReportIDs,
currentReportID: derivedCurrentReportID ?? '-1',
+ policyMemberAccountIDs,
};
- }, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID]);
+ }, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID, policyMemberAccountIDs]);
return {children} ;
}
@@ -175,4 +137,4 @@ function useReportIDs() {
}
export {ReportIDsContext, ReportIDsContextProvider, policySelector, useReportIDs};
-export type {ChatReportSelector, PolicySelector, ReportActionsSelector};
+export type {PolicySelector, ReportActionsSelector};
diff --git a/src/hooks/useReviewDuplicatesNavigation.tsx b/src/hooks/useReviewDuplicatesNavigation.tsx
new file mode 100644
index 000000000000..e14731024c17
--- /dev/null
+++ b/src/hooks/useReviewDuplicatesNavigation.tsx
@@ -0,0 +1,52 @@
+import {useEffect, useMemo, useState} from 'react';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+type StepName = 'description' | 'merchant' | 'category' | 'billable' | 'tag' | 'taxCode' | 'reimbursable';
+
+function useReviewDuplicatesNavigation(stepNames: string[], currentScreenName: StepName, threadReportID: string) {
+ const [nextScreen, setNextScreen] = useState(currentScreenName);
+ const [currentScreenIndex, setCurrentScreenIndex] = useState(0);
+ const intersection = useMemo(() => CONST.REVIEW_DUPLICATES_ORDER.filter((element) => stepNames.includes(element)), [stepNames]);
+
+ useEffect(() => {
+ const currentIndex = intersection.indexOf(currentScreenName);
+ const nextScreenIndex = currentIndex + 1;
+ setCurrentScreenIndex(currentIndex);
+ setNextScreen(intersection[nextScreenIndex] ?? '');
+ }, [currentScreenName, intersection]);
+
+ const navigateToNextScreen = () => {
+ switch (nextScreen) {
+ case 'merchant':
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(threadReportID));
+ break;
+ case 'category':
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(threadReportID));
+ break;
+ case 'tag':
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(threadReportID));
+ break;
+ case 'description':
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(threadReportID));
+ break;
+ case 'taxCode':
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE.getRoute(threadReportID));
+ break;
+ case 'reimbursable':
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(threadReportID));
+ break;
+ case 'billable':
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(threadReportID));
+ break;
+ default:
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(threadReportID));
+ break;
+ }
+ };
+
+ return {navigateToNextScreen, currentScreenIndex};
+}
+
+export default useReviewDuplicatesNavigation;
diff --git a/src/hooks/useSubscriptionPossibleCostSavings.ts b/src/hooks/useSubscriptionPossibleCostSavings.ts
new file mode 100644
index 000000000000..059445ce002d
--- /dev/null
+++ b/src/hooks/useSubscriptionPossibleCostSavings.ts
@@ -0,0 +1,34 @@
+import {useOnyx} from 'react-native-onyx';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import usePreferredCurrency from './usePreferredCurrency';
+import useSubscriptionPlan from './useSubscriptionPlan';
+
+const POSSIBLE_COST_SAVINGS = {
+ [CONST.PAYMENT_CARD_CURRENCY.USD]: {
+ [CONST.POLICY.TYPE.TEAM]: 1000,
+ [CONST.POLICY.TYPE.CORPORATE]: 1800,
+ },
+ [CONST.PAYMENT_CARD_CURRENCY.AUD]: {
+ [CONST.POLICY.TYPE.TEAM]: 1400,
+ [CONST.POLICY.TYPE.CORPORATE]: 3000,
+ },
+ [CONST.PAYMENT_CARD_CURRENCY.NZD]: {
+ [CONST.POLICY.TYPE.TEAM]: 1600,
+ [CONST.POLICY.TYPE.CORPORATE]: 3200,
+ },
+} as const;
+
+function useSubscriptionPossibleCostSavings(): number {
+ const preferredCurrency = usePreferredCurrency();
+ const subscriptionPlan = useSubscriptionPlan();
+ const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);
+
+ if (!subscriptionPlan || !privateSubscription?.type) {
+ return 0;
+ }
+
+ return POSSIBLE_COST_SAVINGS[preferredCurrency][subscriptionPlan];
+}
+
+export default useSubscriptionPossibleCostSavings;
diff --git a/src/hooks/useSubscriptionPrice.ts b/src/hooks/useSubscriptionPrice.ts
index 0b71fe62c7c8..9279ff94757d 100644
--- a/src/hooks/useSubscriptionPrice.ts
+++ b/src/hooks/useSubscriptionPrice.ts
@@ -25,16 +25,6 @@ const SUBSCRIPTION_PRICES = {
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1400,
},
},
- [CONST.PAYMENT_CARD_CURRENCY.GBP]: {
- [CONST.POLICY.TYPE.CORPORATE]: {
- [CONST.SUBSCRIPTION.TYPE.ANNUAL]: 700,
- [CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1400,
- },
- [CONST.POLICY.TYPE.TEAM]: {
- [CONST.SUBSCRIPTION.TYPE.ANNUAL]: 400,
- [CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 800,
- },
- },
[CONST.PAYMENT_CARD_CURRENCY.NZD]: {
[CONST.POLICY.TYPE.CORPORATE]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 1600,
diff --git a/src/hooks/useSyncFocus/index.ts b/src/hooks/useSyncFocus/index.ts
index f8b3349bc223..6211b9988a82 100644
--- a/src/hooks/useSyncFocus/index.ts
+++ b/src/hooks/useSyncFocus/index.ts
@@ -21,7 +21,7 @@ const useSyncFocus = (ref: RefObject, isFocused: boolean, shouldSyncFocus
}
ref.current?.focus();
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [didScreenTransitionEnd, isFocused, ref]);
};
diff --git a/src/hooks/useTabNavigatorFocus/index.ts b/src/hooks/useTabNavigatorFocus/index.ts
index f160f4670b26..f85b630a0834 100644
--- a/src/hooks/useTabNavigatorFocus/index.ts
+++ b/src/hooks/useTabNavigatorFocus/index.ts
@@ -40,7 +40,7 @@ function useTabNavigatorFocus({tabIndex}: UseTabNavigatorFocusParams): boolean {
// Retrieve the animation value from the tab navigator, which ranges from 0 to the total number of pages displayed.
// Even a minimal scroll towards the camera page (e.g., a value of 0.001 at start) should activate the camera for immediate responsiveness.
// STOP!!!!!!! This is not a pattern to be followed! We are conditionally rendering this hook becase when used in the edit flow we'll never be inside a tab navigator.
- // eslint-disable-next-line react-hooks/rules-of-hooks
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/rules-of-hooks
tabPositionAnimation = useTabAnimation();
} catch (error) {
tabPositionAnimation = null;
diff --git a/src/hooks/useTackInputFocus/index.ts b/src/hooks/useTackInputFocus/index.ts
index 124f8460127c..e6caa15f9dde 100644
--- a/src/hooks/useTackInputFocus/index.ts
+++ b/src/hooks/useTackInputFocus/index.ts
@@ -1,5 +1,6 @@
import {useCallback, useEffect} from 'react';
import useDebouncedState from '@hooks/useDebouncedState';
+import * as Browser from '@libs/Browser';
/**
* Detects input or text area focus on browsers, to avoid scrolling on virtual viewports
@@ -28,7 +29,13 @@ export default function useTackInputFocus(enable = false): boolean {
);
const resetScrollPositionOnVisualViewport = useCallback(() => {
- window.scrollTo({top: 0});
+ if (Browser.isChromeIOS() && window.visualViewport?.offsetTop) {
+ // On Chrome iOS, the visual viewport triggers a scroll event when the keyboard is opened, but some time the scroll position is not correct.
+ // So this change is specific to Chrome iOS, helping to reset the viewport position correctly.
+ window.scrollTo({top: -window.visualViewport.offsetTop});
+ } else {
+ window.scrollTo({top: 0});
+ }
}, []);
useEffect(() => {
diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts
index 83c725a48db0..44b8e982139e 100644
--- a/src/hooks/useViolations.ts
+++ b/src/hooks/useViolations.ts
@@ -40,18 +40,34 @@ const violationFields: Record = {
smartscanFailed: 'receipt',
someTagLevelsRequired: 'tag',
tagOutOfPolicy: 'tag',
+ taxRateChanged: 'tax',
taxAmountChanged: 'tax',
taxOutOfPolicy: 'tax',
- taxRateChanged: 'tax',
taxRequired: 'tax',
hold: 'none',
};
type ViolationsMap = Map;
-function useViolations(violations: TransactionViolation[]) {
+// We don't want to show these violations on NewDot
+const excludedViolationsName = ['taxAmountChanged', 'taxRateChanged'];
+
+/**
+ * @param violations – List of transaction violations
+ * @param shouldShowOnlyViolations – Whether we should only show violations of type 'violation'
+ */
+function useViolations(violations: TransactionViolation[], shouldShowOnlyViolations: boolean) {
const violationsByField = useMemo((): ViolationsMap => {
- const filteredViolations = violations.filter((violation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION);
+ const filteredViolations = violations.filter((violation) => {
+ if (excludedViolationsName.includes(violation.name)) {
+ return false;
+ }
+ if (shouldShowOnlyViolations) {
+ return violation.type === CONST.VIOLATION_TYPES.VIOLATION;
+ }
+ return true;
+ });
+
const violationGroups = new Map();
for (const violation of filteredViolations) {
const field = violationFields[violation.name];
@@ -59,7 +75,7 @@ function useViolations(violations: TransactionViolation[]) {
violationGroups.set(field, [...existingViolations, violation]);
}
return violationGroups ?? new Map();
- }, [violations]);
+ }, [violations, shouldShowOnlyViolations]);
const getViolationsForField = useCallback(
(field: ViolationField, data?: TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => {
diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts
index 25757fda17e5..b391e45a61aa 100644
--- a/src/hooks/useWindowDimensions/index.ts
+++ b/src/hooks/useWindowDimensions/index.ts
@@ -23,7 +23,7 @@ export default function (useCachedViewportHeight = false): WindowDimensions {
unlockWindowDimensions: () => {},
};
- const isCachedViewportHeight = useCachedViewportHeight && Browser.isMobileSafari();
+ const isCachedViewportHeight = useCachedViewportHeight && Browser.isMobileWebKit();
const cachedViewportHeightWithKeyboardRef = useRef(initalViewportHeight);
const {width: windowWidth, height: windowHeight} = useWindowDimensions();
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 095cc12a3896..e0ed30050bb1 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1,7 +1,8 @@
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,
@@ -12,18 +13,26 @@ import type {
BeginningOfChatHistoryAnnounceRoomPartTwo,
BeginningOfChatHistoryDomainRoomPartOneParams,
CanceledRequestParams,
+ ChangeFieldParams,
+ ChangePolicyParams,
+ ChangeTypeParams,
CharacterLimitParams,
+ ConfirmHoldExpenseParams,
ConfirmThatParams,
DateShouldBeAfterParams,
DateShouldBeBeforeParams,
+ DelegateSubmitParams,
DeleteActionParams,
DeleteConfirmationParams,
+ DeleteExpenseTranslationParams,
DidSplitAmountMessageParams,
DistanceRateOperationsParams,
EditActionParams,
ElectronicFundsParams,
EnterMagicCodeParams,
+ ExportedToIntegrationParams,
FormattedMaxLengthParams,
+ ForwardedParams,
GoBackMessageParams,
GoToRoomParams,
InstantSummaryParams,
@@ -32,6 +41,8 @@ import type {
LogSizeParams,
ManagerApprovedAmountParams,
ManagerApprovedParams,
+ MarkedReimbursedParams,
+ MarkReimbursedFromIntegrationParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
@@ -65,10 +76,12 @@ import type {
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
SettleExpensifyCardParams,
+ ShareParams,
SignUpNewFaceCodeParams,
SizeExceededParams,
SplitAmountParams,
StepCounterParams,
+ StripePaidParams,
TaskCreatedActionParams,
TermsParams,
ThreadRequestReportNameParams,
@@ -76,6 +89,8 @@ import type {
ToValidateLoginParams,
TransferParams,
TranslationBase,
+ UnapprovedParams,
+ UnshareParams,
UntilTimeParams,
UpdatedTheDistanceParams,
UpdatedTheRequestParams,
@@ -88,6 +103,7 @@ import type {
ViolationsInvoiceMarkupParams,
ViolationsMaxAgeParams,
ViolationsMissingTagParams,
+ ViolationsModifiedAmountParams,
ViolationsOverCategoryLimitParams,
ViolationsOverLimitParams,
ViolationsPerDayLimitParams,
@@ -121,6 +137,7 @@ export default {
yes: 'Yes',
no: 'No',
ok: 'OK',
+ notNow: 'Not now',
learnMore: 'Learn more',
buttonConfirm: 'Got it',
name: 'Name',
@@ -157,6 +174,7 @@ export default {
wallet: 'Wallet',
preferences: 'Preferences',
view: 'View',
+ review: 'Review',
not: 'Not',
signIn: 'Sign in',
signInWithGoogle: 'Sign in with Google',
@@ -340,9 +358,16 @@ export default {
shared: 'Shared',
drafts: 'Drafts',
finished: 'Finished',
+ upgrade: 'Upgrade',
companyID: 'Company ID',
userID: 'User ID',
disable: 'Disable',
+ export: 'Export',
+ initialValue: 'Initial value',
+ currentDate: 'Current date',
+ value: 'Value',
+ downloadFailedTitle: 'Download failed',
+ downloadFailedDescription: "Your download couldn't be completed. Please try again later.",
},
location: {
useCurrent: 'Use current location',
@@ -372,6 +397,8 @@ export default {
notAllowedExtension: 'This file type is not allowed. Please try a different file type.',
folderNotAllowedMessage: 'Uploading a folder is not allowed. Please try a different file.',
protectedPDFNotSupported: 'Password-protected PDF is not supported',
+ attachmentImageResized: 'This image has been resized for previewing. Download for full resolution.',
+ attachmentImageTooLarge: 'This image is too large to preview before uploading.',
},
connectionComplete: {
title: 'Connection complete',
@@ -502,6 +529,7 @@ export default {
replyInThread: 'Reply in thread',
joinThread: 'Join thread',
leaveThread: 'Leave thread',
+ copyOnyxData: 'Copy Onyx data',
flagAsOffensive: 'Flag as offensive',
menu: 'Menu',
},
@@ -586,7 +614,7 @@ export default {
saveTheWorld: 'Save the world',
},
allSettingsScreen: {
- subscriptions: 'Subscriptions',
+ subscription: 'Subscription',
cardsAndDomains: 'Cards & Domains',
},
tabSelector: {
@@ -606,6 +634,10 @@ export default {
cameraAccess: 'Camera access is required to take pictures of receipts.',
cameraErrorTitle: 'Camera error',
cameraErrorMessage: 'An error occurred while taking a photo. Please try again.',
+ locationAccessTitle: 'Allow location access',
+ locationAccessMessage: 'We’ll use your location to accurately determine your default currency and timezone. You can edit access in your device’s settings anytime.',
+ locationErrorTitle: 'Enable location in settings',
+ locationErrorMessage: 'Allowing location access is required to help accurately determine your default currency and timezone. Tap Settings to update permissions.',
dropTitle: 'Let it go',
dropMessage: 'Drop your file here',
flash: 'flash',
@@ -622,7 +654,7 @@ export default {
splitBill: 'Split expense',
splitScan: 'Split receipt',
splitDistance: 'Split distance',
- paySomeone: ({name}: PaySomeoneParams) => `Pay ${name ?? 'someone'}`,
+ paySomeone: (name: string) => `Pay ${name ?? 'someone'}`,
assignTask: 'Assign task',
header: 'Quick action',
trackManual: 'Track expense',
@@ -682,8 +714,8 @@ export default {
`${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${
pendingReceipts > 0 ? `, ${pendingReceipts} pending` : ''
}`,
- deleteExpense: 'Delete expense',
- deleteConfirmation: 'Are you sure that you want to delete this expense?',
+ deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Delete ${Str.pluralize('expense', 'expenses', count)}`,
+ deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Are you sure that you want to delete ${Str.pluralize('this expense', 'these expenses', count)}?`,
settledExpensify: 'Paid',
settledElsewhere: 'Paid elsewhere',
individual: 'Individual',
@@ -775,9 +807,13 @@ export default {
reviewDuplicates: 'Review duplicates',
keepAll: 'Keep all',
confirmApprove: 'Confirm approval amount',
- confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.",
+ confirmApprovalAmount: 'Approve only compliant expenses, or approve the entire report.',
+ confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
+ `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`,
confirmPay: 'Confirm payment amount',
- confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.",
+ confirmPayAmount: "Pay what's not on hold, or pay the entire report.",
+ confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
+ `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`,
payOnly: 'Pay only',
approveOnly: 'Approve only',
holdEducationalTitle: 'This expense is on',
@@ -792,6 +828,11 @@ export default {
removed: 'removed',
transactionPending: 'Transaction pending.',
chooseARate: ({unit}: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`,
+ unapprove: 'Unapprove',
+ unapproveReport: 'Unapprove report',
+ headsUp: 'Heads up!',
+ unapproveWithIntegrationWarning: (accountingIntegration: string) =>
+ `This report has already been exported to ${accountingIntegration}. Changes to this report in Expensify may lead to data discrepancies and Expensify Card reconciliation issues. Are you sure you want to unapprove this report?`,
},
notificationPreferencesPage: {
header: 'Notification preferences',
@@ -910,7 +951,7 @@ export default {
timezonePage: {
timezone: 'Timezone',
isShownOnProfile: 'Your timezone is shown on your profile.',
- getLocationAutomatically: 'Automatically determine your location.',
+ getLocationAutomatically: 'Automatically determine your location',
},
updateRequiredView: {
updateRequired: 'Update required',
@@ -960,6 +1001,8 @@ export default {
deviceCredentials: 'Device credentials',
invalidate: 'Invalidate',
destroy: 'Destroy',
+ maskExportOnyxStateData: 'Mask fragile user data while exporting Onyx state',
+ exportOnyxState: 'Export Onyx state',
},
debugConsole: {
saveLog: 'Save log',
@@ -984,7 +1027,7 @@ export default {
},
returnToClassic: 'Switch to Expensify Classic',
help: 'Help',
- accountSettings: 'Account Settings',
+ accountSettings: 'Account settings',
account: 'Account',
general: 'General',
},
@@ -1028,6 +1071,11 @@ export default {
enabled: 'Two-factor authentication is now enabled!',
congrats: 'Congrats, now you’ve got that extra security.',
copy: 'Copy',
+ disable: 'Disable',
+ enableTwoFactorAuth: 'Enable two-factor authentication',
+ pleaseEnableTwoFactorAuth: 'Please enable two-factor authentication.',
+ twoFactorAuthIsRequiredDescription: 'Two-factor authentication is required for connecting to Xero. Please enable two-factor authentication to continue.',
+ twoFactorAuthIsRequiredForAdminsDescription: 'Two-factor authentication is required for Xero workspace admins. Please enable two-factor authentication to continue.',
},
recoveryCodeForm: {
error: {
@@ -1123,9 +1171,9 @@ export default {
deleteAccount: 'Delete account',
deleteConfirmation: 'Are you sure you want to delete this account?',
error: {
- notOwnerOfBankAccount: 'There was an error setting this bank account as your default payment method.',
+ notOwnerOfBankAccount: 'An error occurred while setting this bank account as your default payment method.',
invalidBankAccount: 'This bank account is temporarily suspended.',
- notOwnerOfFund: 'There was an error setting this card as your default payment method.',
+ notOwnerOfFund: 'An error occurred while setting this card as your default payment method.',
setDefaultFailure: 'Something went wrong. Please chat with Concierge for further assistance.',
},
addBankAccountFailure: 'An unexpected error occurred while trying to add your bank account. Please try again.',
@@ -1433,6 +1481,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',
@@ -1450,6 +1499,7 @@ export default {
error: {
containsReservedWord: 'Name cannot contain the words Expensify or Concierge.',
hasInvalidCharacter: 'Name cannot contain a comma or semicolon.',
+ requiredFirstName: 'First name cannot be empty.',
},
},
privatePersonalDetails: {
@@ -1587,7 +1637,7 @@ export default {
phrase4: 'verify your account here',
},
hasPhoneLoginError: 'To add a verified bank account please ensure your primary login is a valid email and try again. You can add your phone number as a secondary login.',
- hasBeenThrottledError: 'There was an error adding your bank account. Please wait a few minutes and try again.',
+ hasBeenThrottledError: 'An error occurred while adding your bank account. Please wait a few minutes and try again.',
hasCurrencyError: 'Oops! It appears that your workspace currency is set to a different currency than USD. To proceed, please set it to USD and try again.',
error: {
youNeedToSelectAnOption: 'You need to select an option to proceed.',
@@ -1653,7 +1703,7 @@ export default {
verifyIdentity: 'Verify identity',
letsVerifyIdentity: "Let's verify your identity.",
butFirst: `But first, the boring stuff. Read up on the legalese in the next step and click "Accept" when you're ready.`,
- genericError: 'There was an error while processing this step. Please try again.',
+ genericError: 'An error occurred while processing this step. Please try again.',
cameraPermissionsNotGranted: 'Enable camera access',
cameraRequestMessage: 'We need access to your camera to complete bank account verification. Please enable via Settings > New Expensify.',
microphonePermissionsNotGranted: 'Enable microphone access',
@@ -1963,6 +2013,7 @@ export default {
workspace: {
common: {
card: 'Cards',
+ expensifyCard: 'Expensify Card',
workflows: 'Workflows',
workspace: 'Workspace',
edit: 'Edit workspace',
@@ -1973,7 +2024,8 @@ export default {
reimburse: 'Reimbursements',
categories: 'Categories',
tags: 'Tags',
- reportFields: 'Report Fields',
+ reportFields: 'Report fields',
+ reportField: 'Report field',
taxes: 'Taxes',
bills: 'Bills',
invoices: 'Invoices',
@@ -2006,6 +2058,24 @@ 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',
+ markAsExported: 'Mark as manually entered',
+ exportIntegrationSelected: (connectionName: ConnectionName) => `Export to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
+ letsDoubleCheck: "Let's double check that everything looks right.",
+ lineItemLevel: 'Line-item level',
+ reportLevel: 'Report level',
+ appliedOnExport: 'Not imported into Expensify, applied on export',
+ shareNote: {
+ header: 'Easily share your workspace with other members.',
+ content: {
+ firstPart:
+ 'Share this QR code or copy the link below to make it easy for members to request access to your workspace. All requests to join the workspace will show up in the',
+ secondPart: 'room for your review.',
+ },
+ },
+ createNewConnection: 'Create new connection',
+ reuseExistingConnection: 'Reuse existing connection',
+ existingConnections: 'Existing connections',
+ lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Last synced ${formattedDate}`,
},
qbo: {
importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.',
@@ -2024,10 +2094,7 @@ export default {
outOfPocketLocationEnabledDescription:
'QuickBooks Online doesn’t support locations on vendor bills or checks. As you have locations enabled on your workspace, these export options are unavailable.',
taxesJournalEntrySwitchNote: "QuickBooks Online doesn't support taxes on journal entries. Please change your export option to vendor bill or check.",
- export: 'Export',
- exportAs: 'Export as',
exportDescription: 'Configure how Expensify data exports to QuickBooks Online.',
- preferredExporter: 'Preferred exporter',
date: 'Export date',
exportExpenses: 'Export out-of-pocket expenses as',
exportInvoices: 'Export invoices to',
@@ -2058,11 +2125,7 @@ export default {
exportInvoicesDescription: 'Use this account when exporting invoices to QuickBooks Online.',
exportCompanyCardsDescription: 'Set how company card purchases export to QuickBooks Online.',
vendor: 'Vendor',
- defaultVendor: 'Default vendor',
defaultVendorDescription: 'Set a default vendor that will apply to all credit card transactions upon export.',
- 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.',
exportOutOfPocketExpensesDescription: 'Set how out-of-pocket expenses export to QuickBooks Online.',
exportCheckDescription: "We'll create an itemized check for each Expensify report and send it from the bank account below.",
exportJournalEntryDescription: "We'll create an itemized journal entry for each Expensify report and post it to the account below.",
@@ -2074,6 +2137,7 @@ export default {
accountsPayableDescription: 'Choose where to create vendor bills.',
bankAccount: 'Bank account',
bankAccountDescription: 'Choose where to send checks from.',
+ creditCardAccount: 'Credit card account',
companyCardsLocationEnabledDescription:
"QuickBooks Online doesn't support locations on vendor bill exports. As you have locations enabled on your workspace, this export option is unavailable.",
outOfPocketTaxEnabledDescription:
@@ -2081,14 +2145,11 @@ export default {
outOfPocketTaxEnabledError: 'Journal entries are unavailable when taxes are enabled. Please choose a different export option.',
outOfPocketLocationEnabledError: 'Vendor bills are unavailable when locations are enabled. Please choose a different export option.',
advancedConfig: {
- advanced: 'Advanced',
- autoSync: 'Auto-sync',
autoSyncDescription: 'Expensify will automatically sync with QuickBooks Online every day.',
inviteEmployees: 'Invite employees',
inviteEmployeesDescription: 'Import Quickbooks Online employee records and invite employees to this workspace.',
createEntities: 'Auto-create entities',
createEntitiesDescription: "Expensify will automatically create vendors in QuickBooks Online if they don't exist already, and auto-create customers when exporting invoices.",
- reimbursedReports: 'Sync reimbursed reports',
reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Quickbooks Online account below.',
qboBillPaymentAccount: 'QuickBooks bill payment account',
qboInvoiceCollectionAccount: 'QuickBooks invoice collections account',
@@ -2139,15 +2200,13 @@ export default {
default: 'Xero contact default',
tag: 'Tags',
},
- export: 'Export',
exportDescription: 'Configure how Expensify data exports to Xero.',
exportCompanyCard: 'Export company card expenses as',
purchaseBill: 'Purchase bill',
- exportDeepDiveCompanyCard: 'Each exported expense posts as a bank transaction to the Xero bank account below, and transaction dates will match the dates on your bank statement.',
+ exportDeepDiveCompanyCard: 'Exported expenses will post as bank transactions to the Xero bank account below, and transaction dates will match the dates on your bank statement.',
bankTransactions: 'Bank transactions',
xeroBankAccount: 'Xero bank account',
xeroBankAccountDescription: 'Choose where expenses will post as bank transactions.',
- preferredExporter: 'Preferred exporter',
exportExpenses: 'Export out-of-pocket expenses as',
exportExpensesDescription: 'Reports will export as a purchase bill with the date and status selected below.',
purchaseBillDate: 'Purchase bill date',
@@ -2155,11 +2214,8 @@ export default {
salesInvoice: 'Sales invoice',
exportInvoicesDescription: 'Sales invoices always display the date on which the invoice was sent.',
advancedConfig: {
- advanced: 'Advanced',
- autoSync: 'Auto-sync',
autoSyncDescription: 'Expensify will automatically sync with Xero every day.',
purchaseBillStatusTitle: 'Purchase bill status',
- reimbursedReports: 'Sync reimbursed reports',
reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Xero account below.',
xeroBillPaymentAccount: 'Xero bill payment account',
xeroInvoiceCollectionAccount: 'Xero invoice collections account',
@@ -2193,17 +2249,359 @@ export default {
[CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_PAYMENT]: 'Awaiting payment',
},
},
+ noAccountsFound: 'No accounts found',
+ noAccountsFoundDescription: 'Please 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 out-of-pocket expenses as',
+ description: 'Set how out-of-pocket expenses export to Sage Intacct.',
+ values: {
+ [CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT]: 'Expense reports',
+ [CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL]: 'Vendor bills',
+ },
+ },
+ nonReimbursableExpenses: {
+ label: 'Export company cards as',
+ description: 'Set how company card purchases export to Sage Intacct.',
+ values: {
+ [CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE]: 'Credit cards',
+ [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 Expensify data exports to Sage Intacct.',
exportPreferredExporterNote:
- 'The preferred exporter can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.',
+ '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 Xero and sync the connection again.',
+ noAccountsFoundDescription: `Please add the account in Sage Intacct and sync the connection again.`,
+ autoSync: 'Auto-sync',
+ autoSyncDescription: 'Expensify will automatically sync with Sage Intacct 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: 'Any time a report is paid using Expensify ACH, the corresponding bill payment 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.",
+ exportDescription: 'Configure how Expensify data exports to NetSuite.',
+ exportReimbursable: 'Export reimbursable expenses as',
+ exportNonReimbursable: 'Export non-reimbursable expenses as',
+ exportInvoices: 'Export invoices to',
+ journalEntriesTaxPostingAccount: 'Journal entries tax posting account',
+ journalEntriesProvTaxPostingAccount: 'Journal entries provincial tax posting account',
+ foreignCurrencyAmount: 'Export foreign currency amount',
+ exportToNextOpenPeriod: 'Export to next open period',
+ nonReimbursableJournalPostingAccount: 'Non-reimbursable journal posting account',
+ reimbursableJournalPostingAccount: 'Reimbursable journal posting account',
+ journalPostingPreference: {
+ label: 'Journal entries posting preference',
+ values: {
+ [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE]: 'Single, itemized entry for each report',
+ [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_TOTAL_LINE]: 'Single entry for each individual expense',
+ },
+ },
+ invoiceItem: {
+ label: 'Invoice item',
+ values: {
+ [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: {
+ label: 'Create one for me',
+ description: 'We\'ll create an "Expensify invoice line item" for you upon export (if one doesn’t exist already).',
+ },
+ [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: {
+ label: 'Select existing',
+ description: "We'll tie invoices from Expensify to the item selected below.",
+ },
+ },
+ },
+ exportDate: {
+ label: 'Export date',
+ description: 'Use this date when exporting reports to NetSuite.',
+ values: {
+ [CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE]: {
+ label: 'Date of last expense',
+ description: 'Date of the most recent expense on the report.',
+ },
+ [CONST.NETSUITE_EXPORT_DATE.EXPORTED]: {
+ label: 'Export date',
+ description: 'Date the report was exported to NetSuite.',
+ },
+ [CONST.NETSUITE_EXPORT_DATE.SUBMITTED]: {
+ label: 'Submitted date',
+ description: 'Date the report was submitted for approval.',
+ },
+ },
+ },
+ exportDestination: {
+ values: {
+ [CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT]: {
+ label: 'Expense reports',
+ reimbursableDescription: 'Reimbursable expenses will export as expense reports to NetSuite.',
+ nonReimbursableDescription: 'Non-reimbursable expenses will export as expense reports to NetSuite.',
+ },
+ [CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL]: {
+ label: 'Vendor bills',
+ reimbursableDescription:
+ 'Reimbursable expenses will export as bills payable to the NetSuite vendor specified below.\n' +
+ '\n' +
+ 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.',
+ nonReimbursableDescription:
+ 'Non-reimbursable expenses will export as bills payable to the NetSuite vendor specified below.\n' +
+ '\n' +
+ 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.',
+ },
+ [CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY]: {
+ label: 'Journal Entries',
+ reimbursableDescription:
+ 'Reimbursable expenses will export as journal entries to the NetSuite account specified below.\n' +
+ '\n' +
+ 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.',
+ nonReimbursableDescription:
+ 'Non-reimbursable expenses will export as journal entries to the NetSuite account specified below.\n' +
+ '\n' +
+ 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.',
+ },
+ },
+ },
+ advancedConfig: {
+ autoSyncDescription: 'Expensify will automatically sync with NetSuite every day.',
+ reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the NetSuite account below.',
+ reimbursementsAccount: 'Reimbursements account',
+ reimbursementsAccountDescription: "Choose the bank account you'll use for reimbursements, and we'll create the associated payment in NetSuite.",
+ collectionsAccount: 'Collections account',
+ collectionsAccountDescription: 'Once an invoice is marked as paid in Expensify and exported to NetSuite, it’ll appear against the account below.',
+ approvalAccount: 'A/P approval account',
+ approvalAccountDescription:
+ 'Choose the account that transactions will be approved against in NetSuite. If you’re syncing reimbursed reports, this is also the account that bill payments will be created against.',
+ defaultApprovalAccount: 'NetSuite default',
+ inviteEmployees: 'Invite employees and set approvals',
+ inviteEmployeesDescription:
+ 'Import NetSuite employee records and invite employees to this workspace. Your approval workflow will default to manager approval and can be further configured on the *Members* page.',
+ autoCreateEntities: 'Auto-create employees/vendors',
+ enableCategories: 'Enable newly imported categories',
+ customFormID: 'Custom form ID',
+ customFormIDDescription:
+ 'By default, Expensify will create entries using the preferred transaction form set in NetSuite. Alternatively, you have the option to designate a specific transaction form to be used.',
+ customFormIDReimbursable: 'Reimbursable expense',
+ customFormIDNonReimbursable: 'Non-reimbursable expense',
+ exportReportsTo: {
+ label: 'Expense report approval level',
+ description: 'Once an expense report is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.',
+ values: {
+ [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_APPROVED_NONE]: 'NetSuite default preference',
+ [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_SUPERVISOR_APPROVED]: 'Only supervisor approved',
+ [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_ACCOUNTING_APPROVED]: 'Only accounting approved',
+ [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_APPROVED_BOTH]: 'Supervisor and accounting approved',
+ },
+ },
+ exportVendorBillsTo: {
+ label: 'Vendor bill approval level',
+ description: 'Once a vendor bill is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.',
+ values: {
+ [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVED_NONE]: 'NetSuite default preference',
+ [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVAL_PENDING]: 'Pending approval',
+ [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVED]: 'Approved for posting',
+ },
+ },
+ exportJournalsTo: {
+ label: 'Journal entry approval level',
+ description: 'Once a journal entry is approved in Expensify and exported to NetSuite, you can set an additional level of approval in NetSuite prior to posting.',
+ values: {
+ [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED_NONE]: 'NetSuite default preference',
+ [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVAL_PENDING]: 'Pending approval',
+ [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED]: 'Approved for posting',
+ },
+ },
+ error: {
+ customFormID: 'Please enter a valid numeric custom form ID.',
+ },
+ },
+ noAccountsFound: 'No accounts found',
+ noAccountsFoundDescription: 'Add the account in NetSuite and sync the connection again.',
+ noVendorsFound: 'No vendors found',
+ noVendorsFoundDescription: 'Add vendors in NetSuite and sync the connection again.',
+ noItemsFound: 'No invoice items found',
+ noItemsFoundDescription: 'Add invoice items in NetSuite and sync the connection again.',
noSubsidiariesFound: 'No subsidiaries found',
noSubsidiariesFoundDescription: 'Add the subsidiary in NetSuite and sync the connection again.',
+ tokenInput: {
+ title: 'NetSuite setup',
+ formSteps: {
+ installBundle: {
+ title: 'Install the Expensify bundle',
+ description: 'In NetSuite, go to *Customization > SuiteBundler > Search & Install Bundles* > search for "Expensify" > install the bundle.',
+ },
+ enableTokenAuthentication: {
+ title: 'Enable token-based authentication',
+ description: 'In NetSuite, go to *Setup > Company > Enable Features > SuiteCloud* > enable *token-based authentication*.',
+ },
+ enableSoapServices: {
+ title: 'Enable SOAP web services',
+ description: 'In NetSuite, go to *Setup > Company > Enable Features > SuiteCloud* > enable *SOAP Web Services*.',
+ },
+ createAccessToken: {
+ title: 'Create an access token',
+ description:
+ 'In NetSuite, go to *Setup > Users/Roles > Access Tokens* > create an access token for the "Expensify" app and either the "Expensify Integration" or "Administrator" role.\n\n*Important:* Make sure you save the *Token ID* and *Token Secret* from this step. You\'ll need it for the next step.',
+ },
+ enterCredentials: {
+ title: 'Enter your NetSuite credentials',
+ formInputs: {
+ netSuiteAccountID: 'NetSuite Account ID',
+ netSuiteTokenID: 'Token ID',
+ netSuiteTokenSecret: 'Token Secret',
+ },
+ netSuiteAccountIDDescription: 'In NetSuite, go to *Setup > Integration > SOAP Web Services Preferences*.',
+ },
+ },
+ },
+ import: {
+ expenseCategories: 'Expense categories',
+ expenseCategoriesDescription: 'Your NetSuite expense categories will import into Expensify as categories.',
+ crossSubsidiaryCustomers: 'Cross-subsidiary customer/projects',
+ importFields: {
+ departments: {
+ title: 'Departments',
+ subtitle: 'Choose how to handle the NetSuite *departments* in Expensify.',
+ },
+ classes: {
+ title: 'Classes',
+ subtitle: 'Choose how to handle *classes* in Expensify.',
+ },
+ locations: {
+ title: 'Locations',
+ subtitle: 'Choose how to handle *locations* in Expensify.',
+ },
+ },
+ customersOrJobs: {
+ title: 'Customers / projects',
+ subtitle: 'Choose how to handle NetSuite *customers* and *projects* in Expensify.',
+ importCustomers: 'Import customers',
+ importJobs: 'Import projects',
+ customers: 'customers',
+ jobs: 'projects',
+ label: (importFields: string[], importType: string) => `${importFields.join(' and ')}, ${importType}`,
+ },
+ importTaxDescription: 'Import tax groups from NetSuite.',
+ importCustomFields: {
+ 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]: {
+ label: 'NetSuite employee default',
+ description: 'Not imported into Expensify, applied on export',
+ footerContent: (importField: string) =>
+ `If you use ${importField} in NetSuite, we'll apply the default set on the employee record upon export to Expense Report or Journal Entry.`,
+ },
+ [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: {
+ label: 'Tags',
+ description: 'Line-item level',
+ footerContent: (importField: string) => `${startCase(importField)} will be selectable for each individual expense on an employee's report.`,
+ },
+ [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: {
+ label: 'Report fields',
+ description: 'Report level',
+ footerContent: (importField: string) => `${startCase(importField)} selection will apply to all expense on an employee's report.`,
+ },
+ },
+ },
},
intacct: {
sageIntacctSetup: 'Sage Intacct setup',
@@ -2211,16 +2609,79 @@ export default {
downloadExpensifyPackage: 'Download the Expensify package for Sage Intacct',
followSteps: 'Follow the steps in our How-to: Connect to Sage Intacct instructions',
enterCredentials: 'Enter your Sage Intacct credentials',
- createNewConnection: 'Create new connection',
- reuseExistingConnection: 'Reuse existing connection',
- existingConnections: 'Existing connections',
- sageIntacctLastSync: (formattedDate: string) => `Sage Intacct - Last synced ${formattedDate}`,
+ entity: 'Entity',
+ 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: 'Your Sage Intacct expense types will 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',
control: 'Control',
collect: 'Collect',
},
+ expensifyCard: {
+ issueCard: 'Issue card',
+ name: 'Name',
+ lastFour: 'Last 4',
+ limit: 'Limit',
+ currentBalance: 'Current balance',
+ currentBalanceDescription: 'Current balance is the sum of all posted Expensify Card transactions that have occurred since the last settlement date.',
+ cardLimit: 'Card limit',
+ remainingLimit: 'Remaining limit',
+ requestLimitIncrease: 'Request limit increase',
+ remainingLimitDescription:
+ 'We consider a number of factors when calculating your remaining limit: your tenure as a customer, the business-related information you provided during signup, and the available cash in your business bank account. Your remaining limit can fluctuate on a daily basis.',
+ cashBack: 'Cash back',
+ cashBackDescription: 'Cash back balance is based on settled monthly Expensify Card spend across your workspace.',
+ issueNewCard: 'Issue new card',
+ finishSetup: 'Finish setup',
+ chooseBankAccount: 'Choose bank account',
+ chooseExistingBank: 'Choose an existing business bank account to pay your Expensify Card balance, or add a new bank account',
+ accountEndingIn: 'Account ending in',
+ addNewBankAccount: 'Add a new bank account',
+ cardDetails: 'Card details',
+ virtual: 'Virtual',
+ physical: 'Physical',
+ deactivate: 'Deactivate card',
+ changeCardLimit: 'Change card limit',
+ changeLimit: 'Change limit',
+ smartLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined until you approve more expenses on the card.`,
+ monthlyLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined until next month.`,
+ fixedLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined.`,
+ },
categories: {
deleteCategories: 'Delete categories',
deleteCategoriesPrompt: 'Are you sure you want to delete these categories?',
@@ -2248,6 +2709,10 @@ export default {
existingCategoryError: 'A category with this name already exists.',
invalidCategoryName: 'Invalid category name.',
importedFromAccountingSoftware: 'The categories below are imported from your',
+ payrollCode: 'Payroll code',
+ updatePayrollCodeFailureMessage: 'An error occurred while updating the payroll code, please try again.',
+ glCode: 'GL code',
+ updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.',
},
moreFeatures: {
spendSection: {
@@ -2266,6 +2731,23 @@ export default {
title: 'Distance rates',
subtitle: 'Add, update, and enforce rates.',
},
+ expensifyCard: {
+ title: 'Expensify Card',
+ subtitle: 'Gain insights and control over spend',
+ disableCardTitle: 'Disable Expensify Card',
+ disableCardPrompt: 'You can’t disable the Expensify Card because it’s already in use. Reach out to Concierge for next steps.',
+ disableCardButton: 'Chat with Concierge',
+ feed: {
+ title: 'Get the Expensify Card',
+ subTitle: 'Streamline your business with the Expensify Card',
+ features: {
+ cashBack: 'Up to 2% cash back on every US purchase',
+ unlimited: 'Issue unlimited virtual cards',
+ spend: 'Spend controls and custom limits',
+ },
+ ctaTitle: 'Issue new card',
+ },
+ },
workflows: {
title: 'Workflows',
subtitle: 'Configure how spend is approved and paid.',
@@ -2300,14 +2782,50 @@ 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.',
},
- subtitle: "Report fields apply to all spend and can be helpful when you'd like to prompt for extra information",
+ subtitle: "Report fields apply to all spend and can be helpful when you'd like to prompt for extra information.",
disableReportFields: 'Disable report fields',
disableReportFieldsConfirmation: 'Are you sure? Text and date fields will be deleted, and lists will be disabled.',
+ importedFromAccountingSoftware: 'The report fields below are imported from your',
+ textType: 'Text',
+ dateType: 'Date',
+ dropdownType: 'List',
+ textAlternateText: 'Add a field for free text input.',
+ dateAlternateText: 'Add a calendar for date selection.',
+ dropdownAlternateText: 'Add a list of options to choose from.',
+ nameInputSubtitle: 'Choose a name for the report field.',
+ typeInputSubtitle: 'Choose what type of report field to use.',
+ initialValueInputSubtitle: 'Enter a starting value to show in the report field.',
+ listValuesInputSubtitle: 'These values will appear in your report field dropdown. Enabled values can be selected by members.',
+ listInputSubtitle: 'These values will appear in your report field list. Enabled values can be selected by members.',
+ deleteValue: 'Delete value',
+ deleteValues: 'Delete values',
+ disableValue: 'Disable value',
+ disableValues: 'Disable values',
+ enableValue: 'Enable value',
+ enableValues: 'Enable values',
+ emptyReportFieldsValues: {
+ title: "You haven't created any list values",
+ subtitle: 'Add custom values to appear on reports.',
+ },
+ deleteValuePrompt: 'Are you sure you want to delete this list value?',
+ deleteValuesPrompt: 'Are you sure you want to delete these list values?',
+ listValueRequiredError: 'Please enter a list value name',
+ existingListValueError: 'A list value with this name already exists',
+ editValue: 'Edit value',
+ listValues: 'List values',
+ addValue: 'Add value',
+ existingReportFieldNameError: 'A report field with this name already exists',
+ reportFieldNameRequiredError: 'Please enter a report field name',
+ reportFieldTypeRequiredError: 'Please choose a report field type',
+ reportFieldInitialValueRequiredError: 'Please choose a report field initial value',
+ genericFailureMessage: 'An error occurred while updating the report field. Please try again.',
},
tags: {
tagName: 'Tag name',
@@ -2333,6 +2851,8 @@ export default {
existingTagError: 'A tag with this name already exists.',
genericFailureMessage: 'An error occurred while updating the tag, please try again.',
importedFromAccountingSoftware: 'The tags below are imported from your',
+ glCode: 'GL code',
+ updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.',
},
taxes: {
subtitle: 'Add tax names, rates, and set defaults.',
@@ -2345,6 +2865,7 @@ export default {
taxRate: 'Tax rate',
error: {
taxRateAlreadyExists: 'This tax name is already in use.',
+ taxCodeAlreadyExists: 'This tax code is already in use.',
valuePercentageRange: 'Please enter a valid percentage between 0 and 100.',
customNameRequired: 'Custom tax name is required.',
deleteFailureMessage: 'An error occurred while deleting the tax rate. Please try again or ask Concierge for help.',
@@ -2363,6 +2884,8 @@ export default {
enableMultiple: 'Enable rates',
},
importedFromAccountingSoftware: 'The taxes below are imported from your',
+ taxCode: 'Tax code',
+ updateTaxCodeFailureMessage: 'An error occurred while updating the tax code, please try again.',
},
emptyWorkspace: {
title: 'Create a workspace',
@@ -2448,6 +2971,11 @@ export default {
limitType: 'Limit type',
name: 'Name',
},
+ deactivateCardModal: {
+ deactivate: 'Deactivate',
+ deactivateCard: 'Deactivate card',
+ deactivateConfirmation: 'Deactivating this card will decline all future transactions and can’t be undone.',
+ },
},
reimburse: {
captureReceipts: 'Capture receipts',
@@ -2476,8 +3004,23 @@ export default {
xero: 'Xero',
netsuite: 'NetSuite',
intacct: 'Sage Intacct',
+ connectionName: (integration: ConnectionName) => {
+ switch (integration) {
+ case CONST.POLICY.CONNECTIONS.NAME.QBO:
+ return 'Quickbooks Online';
+ case CONST.POLICY.CONNECTIONS.NAME.XERO:
+ return 'Xero';
+ case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
+ return 'NetSuite';
+ case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT:
+ return 'Sage Intacct';
+ default: {
+ return '';
+ }
+ }
+ },
setup: 'Connect',
- lastSync: 'Last synced just now',
+ lastSync: (relativeDate: string) => `Last synced ${relativeDate}`,
import: 'Import',
export: 'Export',
advanced: 'Advanced',
@@ -2515,6 +3058,7 @@ export default {
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NOT_IMPORTED]: 'Not imported',
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE]: 'Not imported',
[CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Imported as report fields',
+ [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite employee default',
},
disconnectPrompt: (currentIntegration?: ConnectionName): string => {
const integrationName =
@@ -2572,6 +3116,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':
@@ -2630,6 +3176,30 @@ export default {
}
},
},
+ preferredExporter: 'Preferred exporter',
+ 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.',
+ exportAs: 'Export as',
+ defaultVendor: 'Default vendor',
+ autoSync: 'Auto-sync',
+ reimbursedReports: 'Sync reimbursed reports',
+ cardReconciliation: 'Card reconciliation',
+ reconciliationAccount: 'Reconciliation account',
+ continuousReconciliation: 'Continuous Reconciliation',
+ saveHoursOnReconciliation:
+ 'Save hours on reconciliation each accounting period by having Expensify continuously reconcile Expensify Card statements and settlements on your behalf.',
+ enableContinuousReconciliation: 'In order to enable Continuous Reconciliation, please enable ',
+ chooseReconciliationAccount: {
+ chooseBankAccount: 'Choose the bank account that your Expensify Card payments will be reconciled against.',
+ accountMatches: 'Make sure this account matches your ',
+ settlementAccount: 'Expensify Card settlement account ',
+ reconciliationWorks: (lastFourPAN: string) => `(ending in ${lastFourPAN}) so Continuous Reconciliation works properly.`,
+ },
+ },
+ export: {
+ notReadyHeading: 'Not ready to export',
+ notReadyDescription: 'Draft or pending expense reports cannot be exported to the accounting system. Please approve or pay these expenses before exporting them.',
},
bills: {
manageYourBills: 'Manage your bills',
@@ -2672,7 +3242,7 @@ export default {
member: 'Invite member',
members: 'Invite members',
invitePeople: 'Invite new members',
- genericFailureMessage: 'An error occurred inviting the member to the workspace. Please try again.',
+ genericFailureMessage: 'An error occurred while inviting the member to the workspace. Please try again.',
pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
user: 'user',
users: 'users',
@@ -2686,7 +3256,7 @@ export default {
inviteMessageTitle: 'Add message',
inviteMessagePrompt: 'Make your invitation extra special by adding a message below',
personalMessagePrompt: 'Message',
- genericFailureMessage: 'An error occurred inviting the member to the workspace. Please try again.',
+ genericFailureMessage: 'An error occurred while inviting the member to the workspace. Please try again.',
inviteNoMembersError: 'Please select at least one member to invite.',
},
distanceRates: {
@@ -2712,13 +3282,15 @@ export default {
editor: {
descriptionInputLabel: 'Description',
nameInputLabel: 'Name',
+ typeInputLabel: 'Type',
+ initialValueInputLabel: 'Initial value',
nameInputHelpText: "This is the name you'll see on your workspace.",
nameIsRequiredError: "You'll need to give your workspace a name.",
currencyInputLabel: 'Default currency',
currencyInputHelpText: 'All expenses on this workspace will be converted to this currency.',
currencyInputDisabledText: "The default currency can't be changed because this workspace is linked to a USD bank account.",
save: 'Save',
- genericFailureMessage: 'An error occurred updating the workspace. Please try again.',
+ genericFailureMessage: 'An error occurred while updating the workspace. Please try again.',
avatarUploadFailureMessage: 'An error occurred uploading the avatar. Please try again.',
addressContext: 'A Workspace Address is required to enable Expensify Travel. Please enter an address associated with your business.',
},
@@ -2790,6 +3362,57 @@ export default {
errorDescriptionPartTwo: 'reach out to Concierge',
errorDescriptionPartThree: 'for help.',
},
+ exportAgainModal: {
+ title: 'Careful!',
+ description: (reportName: string, connectionName: ConnectionName) =>
+ `The following reports have already been exported to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}:\n\n${reportName}\n\nAre you sure you want to export them again?`,
+ confirmText: 'Yes, export again',
+ cancelText: 'Cancel',
+ },
+ upgrade: {
+ reportFields: {
+ title: 'Report fields',
+ description: `Report fields let you specify header-level details, distinct from tags that pertain to expenses on individual line items. These details can encompass specific project names, business trip information, locations, and more.`,
+ onlyAvailableOnPlan: 'Report fields are only available on the Control plan, starting at ',
+ },
+ [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
+ title: 'NetSuite',
+ description: `Enjoy automated syncing and reduce manual entries with the Expensify + NetSuite integration. Gain in-depth, realtime financial insights with native and custom segment support, including project and customer mapping.`,
+ onlyAvailableOnPlan: 'Our NetSuite integration is only available on the Control plan, starting at ',
+ },
+ [CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]: {
+ title: 'Sage Intacct',
+ description: `Enjoy automated syncing and reduce manual entries with the Expensify + Sage Intacct integration. Gain in-depth, real-time financial insights with user-defined dimensions, as well as expense coding by department, class, location, customer, and project (job).`,
+ onlyAvailableOnPlan: 'Our Sage Intacct integration is only available on the Control plan, starting at ',
+ },
+ glCodes: {
+ title: 'GL codes',
+ description: `Add GL codes to your categories and tags for easy export of expenses to your accounting and payroll systems.`,
+ onlyAvailableOnPlan: 'GL codes are only available on the Control plan, starting at ',
+ },
+ glAndPayrollCodes: {
+ title: 'GL & Payroll codes',
+ description: `Add GL & Payroll codes to your categories for easy export of expenses to your accounting and payroll systems.`,
+ onlyAvailableOnPlan: 'GL & Payroll codes are only available on the Control plan, starting at ',
+ },
+ pricing: {
+ amount: '$9 ',
+ perActiveMember: 'per active member per month.',
+ },
+ note: {
+ upgradeWorkspace: 'Upgrade your workspace to access this feature, or',
+ learnMore: 'learn more',
+ aboutOurPlans: 'about our plans and pricing.',
+ },
+ upgradeToUnlock: 'Unlock this feature',
+ completed: {
+ headline: `You've upgraded your workspace!`,
+ successMessage: (policyName: string) => `You've successfully upgraded your ${policyName} workspace to the Control plan!`,
+ viewSubscription: 'View your subscription',
+ moreDetails: 'for more details.',
+ gotIt: 'Got it, thanks',
+ },
+ },
restrictedAction: {
restricted: 'Restricted',
actionsAreCurrentlyRestricted: ({workspaceName}) => `Actions on the ${workspaceName} workspace are currently restricted`,
@@ -2889,7 +3512,7 @@ export default {
},
markAsComplete: 'Mark as complete',
markAsIncomplete: 'Mark as incomplete',
- assigneeError: 'There was an error assigning this task. Please try another assignee.',
+ assigneeError: 'An error occurred while assigning this task. Please try another assignee.',
genericCreateTaskFailureMessage: 'There was an error creating this task. Please try again later.',
deleteTask: 'Delete task',
deleteConfirmation: 'Are you sure you want to delete this task?',
@@ -2915,6 +3538,7 @@ export default {
screenShareRequest: 'Expensify is inviting you to a screen share',
},
search: {
+ selectMultiple: 'Select multiple',
resultsAreLimited: 'Search results are limited.',
searchResults: {
emptyResults: {
@@ -2923,6 +3547,20 @@ export default {
},
},
groupedExpenses: 'grouped expenses',
+ bulkActions: {
+ delete: 'Delete',
+ hold: 'Hold',
+ unhold: 'Unhold',
+ noOptionsAvailable: 'No options available for the selected group of expenses.',
+ },
+ offlinePrompt: "You can't take this action right now.",
+ filtersHeader: 'Filters',
+ filters: {
+ date: {
+ before: 'Before',
+ after: 'After',
+ },
+ },
},
genericErrorPage: {
title: 'Uh-oh, something went wrong!',
@@ -3022,6 +3660,40 @@ export default {
genericUpdateReportFieldFailureMessage: 'Unexpected error updating the field. Please try again later.',
genericUpdateReporNameEditFailureMessage: 'Unexpected error renaming the report. Please try again later.',
noActivityYet: 'No activity yet',
+ actions: {
+ type: {
+ changeField: ({oldValue, newValue, fieldName}: ChangeFieldParams) => `changed ${fieldName} from ${oldValue} to ${newValue}`,
+ changeFieldEmpty: ({newValue, fieldName}: ChangeFieldParams) => `changed ${fieldName} to ${newValue}`,
+ changePolicy: ({fromPolicy, toPolicy}: ChangePolicyParams) => `changed policy from ${fromPolicy} to ${toPolicy}`,
+ changeType: ({oldType, newType}: ChangeTypeParams) => `changed type from ${oldType} to ${newType}`,
+ delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `sent this report to ${delegateUser} since ${originalManager} is on vacation`,
+ exportedToCSV: `exported this report to CSV`,
+ exportedToIntegration: {
+ automatic: ({label}: ExportedToIntegrationParams) => `exported this report to ${label}.`,
+ manual: ({label}: ExportedToIntegrationParams) => `marked this report as manually exported to ${label}.`,
+ reimburseableLink: 'View out of pocket expenses.',
+ nonReimbursableLink: 'View company card expenses.',
+ pending: ({label}: ExportedToIntegrationParams) => `started exporting this report to ${label}...`,
+ },
+ forwarded: ({amount, currency}: ForwardedParams) => `approved ${currency}${amount}`,
+ integrationsMessage: (errorMessage: string, label: string) => `failed to export this report to ${label} ("${errorMessage}").`,
+ managerAttachReceipt: `added a receipt`,
+ managerDetachReceipt: `removed a receipt`,
+ markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `paid ${currency}${amount} elsewhere`,
+ markedReimbursedFromIntegration: ({amount, currency}: MarkReimbursedFromIntegrationParams) => `paid ${currency}${amount} via integration`,
+ outdatedBankAccount: `couldn’t process the payment due to a problem with the payer’s bank account`,
+ reimbursementACHBounce: `couldn’t process the payment, as the payer doesn’t have sufficient funds`,
+ reimbursementACHCancelled: `canceled the payment`,
+ reimbursementAccountChanged: `couldn’t process the payment, as the payer changed bank accounts`,
+ reimbursementDelayed: `processed the payment but it’s delayed by 1-2 more business days`,
+ selectedForRandomAudit: `[randomly selected](https://help.expensify.com/articles/expensify-classic/reports/Set-a-random-report-audit-schedule) for review`,
+ share: ({to}: ShareParams) => `invited user ${to}`,
+ unshare: ({to}: UnshareParams) => `removed user ${to}`,
+ stripePaid: ({amount, currency}: StripePaidParams) => `paid ${currency}${amount}`,
+ takeControl: `took control`,
+ unapproved: ({amount, currency}: UnapprovedParams) => `unapproved ${currency}${amount}`,
+ },
+ },
},
chronos: {
oooEventSummaryFullDay: ({summary, dayCount, date}: OOOEventSummaryFullDayParams) => `${summary} for ${dayCount} ${dayCount === 1 ? 'day' : 'days'} until ${date}`,
@@ -3251,7 +3923,19 @@ export default {
missingCategory: 'Missing category',
missingComment: 'Description required for selected category',
missingTag: ({tagName}: ViolationsMissingTagParams) => `Missing ${tagName ?? 'tag'}`,
- modifiedAmount: 'Amount greater than scanned receipt',
+ modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams): string => {
+ switch (type) {
+ case 'distance':
+ return 'Amount differs from calculated distance';
+ case 'card':
+ return 'Amount greater than card transaction';
+ default:
+ if (displayPercentVariance) {
+ return `Amount ${displayPercentVariance}% greater than scanned receipt`;
+ }
+ return 'Amount greater than scanned receipt';
+ }
+ },
modifiedDate: 'Date differs from scanned receipt',
nonExpensiworksExpense: 'Non-Expensiworks expense',
overAutoApprovalLimit: ({formattedLimit}: ViolationsOverLimitParams) => `Expense exceeds auto approval limit of ${formattedLimit}`,
@@ -3260,7 +3944,19 @@ export default {
overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`,
perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Amount over daily ${formattedLimit}/person category limit`,
receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.',
- receiptRequired: (params: ViolationsReceiptRequiredParams) => `Receipt required${params ? ` over ${params.formattedLimit}${params.category ? ' category limit' : ''}` : ''}`,
+ receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams) => {
+ let message = 'Receipt required';
+ if (formattedLimit ?? category) {
+ message += ' over';
+ if (formattedLimit) {
+ message += ` ${formattedLimit}`;
+ }
+ if (category) {
+ message += ' category limit';
+ }
+ }
+ return message;
+ },
reviewRequired: 'Review required',
rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => {
if (brokenBankConnection) {
@@ -3281,7 +3977,17 @@ export default {
taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'Tax'} no longer valid`,
taxRateChanged: 'Tax rate was modified',
taxRequired: 'Missing tax rate',
+ none: 'None',
+ taxCodeToKeep: 'Choose which tax code to keep',
+ tagToKeep: 'Choose which tag to keep',
+ isTransactionReimbursable: 'Choose if transaction is reimbursable',
+ merchantToKeep: 'Choose which merchant to keep',
+ descriptionToKeep: 'Choose which description to keep',
+ categoryToKeep: 'Choose which category to keep',
+ isTransactionBillable: 'Choose if transaction is billable',
keepThisOne: 'Keep this one',
+ confirmDetails: `Confirm the details you're keeping`,
+ confirmDuplicatesInfo: `The duplicate requests you don't keep will be held for the member to delete`,
hold: 'Hold',
},
violationDismissal: {
@@ -3326,7 +4032,7 @@ export default {
"You appear to be offline. Unfortunately, Expensify Classic doesn't work offline, but New Expensify does. If you prefer to use Expensify Classic, try again when you have an internet connection.",
},
listBoundary: {
- errorMessage: 'There was an error loading more messages.',
+ errorMessage: 'An error occurred while loading more messages.',
tryAgain: 'Try again',
},
systemMessage: {
@@ -3334,16 +4040,76 @@ export default {
},
subscription: {
mobileReducedFunctionalityMessage: 'You can’t make changes to your subscription in the mobile app.',
+ badge: {
+ freeTrial: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`,
+ },
billingBanner: {
+ policyOwnerAmountOwed: {
+ title: 'Your payment info is outdated',
+ subtitle: ({date}) => `Update your payment card by ${date} to continue using all of your favorite features.`,
+ },
+ policyOwnerAmountOwedOverdue: {
+ title: 'Your payment info is outdated',
+ subtitle: 'Please update your payment information.',
+ },
+ policyOwnerUnderInvoicing: {
+ title: 'Your payment info is outdated',
+ subtitle: ({date}) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption.`,
+ },
+ policyOwnerUnderInvoicingOverdue: {
+ title: 'Your payment info is outdated',
+ subtitle: 'Your payment is past due. Please pay your invoice.',
+ },
+ billingDisputePending: {
+ title: 'Your card couldn’t be charged',
+ subtitle: ({amountOwed, cardEnding}) =>
+ `You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`,
+ },
+ cardAuthenticationRequired: {
+ title: 'Your card couldn’t be charged',
+ subtitle: ({cardEnding}) =>
+ `Your payment card hasn’t been fully authenticated. Please complete the authentication process to activate your payment card ending in ${cardEnding}.`,
+ },
+ insufficientFunds: {
+ title: 'Your card couldn’t be charged',
+ subtitle: ({amountOwed}) =>
+ `Your payment card was declined due to insufficient funds. Please retry or add a new payment card to clear your ${amountOwed} outstanding balance.`,
+ },
+ cardExpired: {
+ title: 'Your card couldn’t be charged',
+ subtitle: ({amountOwed}) => `Your payment card expired. Please add a new payment card to clear your ${amountOwed} outstanding balance.`,
+ },
+ cardExpireSoon: {
+ title: 'Your card is expiring soon',
+ subtitle: 'Your payment card will expire at the end of this month. Click the three-dot menu below to update it and continue using all your favorite features.',
+ },
+ retryBillingSuccess: {
+ title: 'Success!',
+ subtitle: 'Your card has been billed successfully.',
+ },
+ retryBillingError: {
+ title: 'Your card couldn’t be charged',
+ subtitle: 'Before retrying, please call your bank directly to authorize Expensify charges and remove any holds. Otherwise, try adding a different payment card.',
+ },
+ cardOnDispute: ({amountOwed, cardEnding}) =>
+ `You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`,
preTrial: {
title: 'Start a free trial',
- subtitle: 'To get started, ',
- subtitleLink: 'complete your setup checklist here',
+ subtitle: 'Almost there! Just complete your ',
+ subtitleLink: 'setup checklist.',
+ },
+ trialStarted: {
+ title: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left!`,
+ subtitle: 'Add a payment card to continue using all of your favorite features.',
+ },
+ trialEnded: {
+ title: 'Your free trial has ended',
+ subtitle: 'Add a payment card to continue using all of your favorite features.',
},
},
cardSection: {
title: 'Payment',
- subtitle: 'Add a payment card to pay for your Expensify subscription.',
+ subtitle: 'Add a card to pay for your Expensify subscription.',
addCardButton: 'Add payment card',
cardNextPayment: ({nextPaymentDate}) => `Your next payment date is ${nextPaymentDate}.`,
cardEnding: ({cardNumber}) => `Card ending in ${cardNumber}`,
@@ -3352,6 +4118,13 @@ export default {
changeCurrency: 'Change payment currency',
cardNotFound: 'No payment card added',
retryPaymentButton: 'Retry payment',
+ requestRefund: 'Request refund',
+ requestRefundModal: {
+ phrase1: 'Getting a refund is easy, just downgrade your account before your next billing date and you’ll receive a refund.',
+ phrase2:
+ 'Heads up: Downgrading your account means your workspace(s) will be deleted. This action can’t be undone, but you can always create a new workspace if you change your mind.',
+ confirm: 'Delete workspace(s) and downgrade',
+ },
viewPaymentHistory: 'View payment history',
},
yourPlan: {
@@ -3412,7 +4185,7 @@ export default {
},
paymentCard: {
addPaymentCard: 'Add payment card',
- enterPaymentCardDetails: 'Enter your payment card details.',
+ enterPaymentCardDetails: 'Enter your payment card details',
security: 'Expensify is PCI-DSS compliant, uses bank-level encryption, and utilizes redundant infrastructure to protect your data.',
learnMoreAboutSecurity: 'Learn more about our security.',
},
@@ -3420,7 +4193,7 @@ export default {
title: 'Subscription settings',
autoRenew: 'Auto-renew',
autoIncrease: 'Auto-increase annual seats',
- saveUpTo: ({amountSaved}) => `Save up to $${amountSaved}/month per active member`,
+ saveUpTo: ({amountWithCurrency}) => `Save up to ${amountWithCurrency}/month per active member`,
automaticallyIncrease:
'Automatically increase your annual seats to accommodate for active members that exceed your subscription size. Note: This will extend your annual subscription end date.',
disableAutoRenew: 'Disable auto-renew',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index e2a00222c62a..e56ae303a813 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,
@@ -11,19 +11,27 @@ import type {
BeginningOfChatHistoryAnnounceRoomPartTwo,
BeginningOfChatHistoryDomainRoomPartOneParams,
CanceledRequestParams,
+ ChangeFieldParams,
+ ChangePolicyParams,
+ ChangeTypeParams,
CharacterLimitParams,
+ ConfirmHoldExpenseParams,
ConfirmThatParams,
DateShouldBeAfterParams,
DateShouldBeBeforeParams,
+ DelegateSubmitParams,
DeleteActionParams,
DeleteConfirmationParams,
+ DeleteExpenseTranslationParams,
DidSplitAmountMessageParams,
DistanceRateOperationsParams,
EditActionParams,
ElectronicFundsParams,
EnglishTranslation,
EnterMagicCodeParams,
+ ExportedToIntegrationParams,
FormattedMaxLengthParams,
+ ForwardedParams,
GoBackMessageParams,
GoToRoomParams,
InstantSummaryParams,
@@ -32,6 +40,8 @@ import type {
LogSizeParams,
ManagerApprovedAmountParams,
ManagerApprovedParams,
+ MarkedReimbursedParams,
+ MarkReimbursedFromIntegrationParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
@@ -65,16 +75,20 @@ import type {
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
SettleExpensifyCardParams,
+ ShareParams,
SignUpNewFaceCodeParams,
SizeExceededParams,
SplitAmountParams,
StepCounterParams,
+ StripePaidParams,
TaskCreatedActionParams,
TermsParams,
ThreadRequestReportNameParams,
ThreadSentMoneyReportNameParams,
ToValidateLoginParams,
TransferParams,
+ UnapprovedParams,
+ UnshareParams,
UntilTimeParams,
UpdatedTheDistanceParams,
UpdatedTheRequestParams,
@@ -87,6 +101,7 @@ import type {
ViolationsInvoiceMarkupParams,
ViolationsMaxAgeParams,
ViolationsMissingTagParams,
+ ViolationsModifiedAmountParams,
ViolationsOverAutoApprovalLimitParams,
ViolationsOverCategoryLimitParams,
ViolationsOverLimitParams,
@@ -112,6 +127,7 @@ export default {
yes: 'Sí',
no: 'No',
ok: 'OK',
+ notNow: 'Ahora no',
learnMore: 'Más información',
buttonConfirm: 'Ok, entendido',
name: 'Nombre',
@@ -140,7 +156,7 @@ export default {
magicCode: 'Código mágico',
twoFactorCode: 'Autenticación de dos factores',
workspaces: 'Espacios de trabajo',
- inbox: 'Bandeja de entrada',
+ inbox: 'Recibidos',
group: 'Grupo',
profile: 'Perfil',
referral: 'Remisión',
@@ -148,6 +164,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',
@@ -331,9 +348,16 @@ export default {
shared: 'Compartidos',
drafts: 'Borradores',
finished: 'Finalizados',
+ upgrade: 'Mejora',
companyID: 'Empresa ID',
userID: 'Usuario ID',
disable: 'Deshabilitar',
+ export: 'Exportar',
+ initialValue: 'Valor inicial',
+ currentDate: 'Fecha actual',
+ value: 'Valor',
+ downloadFailedTitle: 'Error en la descarga',
+ downloadFailedDescription: 'No se pudo completar la descarga. Por favor, inténtalo más tarde.',
},
connectionComplete: {
title: 'Conexión completa',
@@ -354,8 +378,8 @@ export default {
cameraPermissionRequired: 'Permiso para acceder a la cámara',
expensifyDoesntHaveAccessToCamera: 'Expensify no puede tomar fotos sin acceso a la cámara. Haz click en configuración para actualizar los permisos.',
attachmentError: 'Error al adjuntar archivo',
- errorWhileSelectingAttachment: 'Ha ocurrido un error al seleccionar un archivo adjunto. Por favor, inténtalo de nuevo.',
- errorWhileSelectingCorruptedAttachment: 'Ha ocurrido un error al seleccionar un archivo adjunto corrupto. Por favor, inténtalo con otro archivo.',
+ errorWhileSelectingAttachment: 'Se ha producido un error al seleccionar un archivo adjunto. Por favor, inténtalo de nuevo.',
+ errorWhileSelectingCorruptedAttachment: 'Se ha producido un error al seleccionar un archivo adjunto corrupto. Por favor, inténtalo con otro archivo.',
takePhoto: 'Hacer una foto',
chooseFromGallery: 'Elegir de la galería',
chooseDocument: 'Elegir un archivo',
@@ -367,6 +391,8 @@ export default {
notAllowedExtension: 'Este tipo de archivo no es compatible',
folderNotAllowedMessage: 'Subir una carpeta no está permitido. Prueba con otro archivo.',
protectedPDFNotSupported: 'Los PDFs con contraseña no son compatibles',
+ attachmentImageResized: 'Se ha cambiado el tamaño de esta imagen para obtener una vista previa. Descargar para resolución completa.',
+ attachmentImageTooLarge: 'Esta imagen es demasiado grande para obtener una vista previa antes de subirla.',
},
avatarCropModal: {
title: 'Editar foto',
@@ -495,6 +521,7 @@ export default {
replyInThread: 'Responder en el hilo',
joinThread: 'Unirse al hilo',
leaveThread: 'Dejar hilo',
+ copyOnyxData: 'Copiar datos de Onyx',
flagAsOffensive: 'Marcar como ofensivo',
menu: 'Menú',
},
@@ -580,7 +607,7 @@ export default {
saveTheWorld: 'Salvar el mundo',
},
allSettingsScreen: {
- subscriptions: 'Suscripciones',
+ subscription: 'Suscripcion',
cardsAndDomains: 'Tarjetas y Dominios',
},
tabSelector: {
@@ -599,7 +626,13 @@ export default {
takePhoto: 'Haz una foto',
cameraAccess: 'Se requiere acceso a la cámara para hacer fotos de los recibos.',
cameraErrorTitle: 'Error en la cámara',
- cameraErrorMessage: 'Se produjo un error al hacer una foto. Por favor, inténtalo de nuevo.',
+ locationAccessTitle: 'Permitir acceso a la ubicación',
+ locationAccessMessage:
+ 'Usaremos tu ubicación para determinar con precisión la moneda y zona horaria predeterminadas. Puedes editar el acceso en la configuración de tu dispositivo en cualquier momento.',
+ locationErrorTitle: 'Habilitar ubicación en la configuración',
+ locationErrorMessage:
+ 'Es necesario permitir el acceso a la ubicación para ayudar a determinar con precisión su moneda y zona horaria predeterminadas. Haz click en Configuración para actualizar los permisos.',
+ cameraErrorMessage: 'Se ha producido un error al hacer una foto. Por favor, inténtalo de nuevo.',
dropTitle: 'Suéltalo',
dropMessage: 'Suelta tu archivo aquí',
flash: 'flash',
@@ -616,7 +649,7 @@ export default {
splitBill: 'Dividir gasto',
splitScan: 'Dividir recibo',
splitDistance: 'Dividir distancia',
- paySomeone: ({name}: PaySomeoneParams) => `Pagar a ${name ?? 'alguien'}`,
+ paySomeone: (name: string) => `Pagar a ${name ?? 'alguien'}`,
assignTask: 'Assignar tarea',
header: 'Acción rápida',
trackManual: 'Crear gasto',
@@ -676,8 +709,9 @@ export default {
`${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${
pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : ''
}`,
- deleteExpense: 'Eliminar gasto',
- deleteConfirmation: '¿Estás seguro de que quieres eliminar esta solicitud?',
+
+ deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Eliminar ${Str.pluralize('gasto', 'gastos', count)}`,
+ deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `¿Estás seguro de que quieres eliminar ${Str.pluralize('esta solicitud', 'estas solicitudes', count)}?`,
settledExpensify: 'Pagado',
settledElsewhere: 'Pagado de otra forma',
individual: 'Individual',
@@ -769,9 +803,21 @@ export default {
reviewDuplicates: 'Revisar duplicados',
keepAll: 'Mantener todos',
confirmApprove: 'Confirmar importe a aprobar',
- confirmApprovalAmount: 'Aprueba lo que no está bloqueado, o aprueba todo el informe.',
+ confirmApprovalAmount: 'Aprueba sólo los gastos conformes, o aprueba todo el informe.',
+ confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
+ `${Str.pluralize('Este gasto está bloqueado', 'Estos gastos están bloqueados', transactionCount)}. ¿Quieres ${Str.pluralize(
+ 'aprobar',
+ 'aprobarlos',
+ transactionCount,
+ )} de todos modos?`,
confirmPay: 'Confirmar importe de pago',
- confirmPayAmount: 'Paga lo que no está bloqueado, o paga todos los gastos por cuenta propia.',
+ confirmPayAmount: 'Paga lo que no está bloqueado, o paga el informe completo.',
+ confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
+ `${Str.pluralize('Este gasto está bloqueado', 'Estos gastos están bloqueados', transactionCount)}. ¿Quieres ${Str.pluralize(
+ 'pagar',
+ 'pagarlo',
+ transactionCount,
+ )} de todos modos?`,
payOnly: 'Solo pagar',
approveOnly: 'Solo aprobar',
hold: 'Bloquear',
@@ -788,6 +834,11 @@ export default {
removed: 'eliminó',
transactionPending: 'Transacción pendiente.',
chooseARate: ({unit}: ReimbursementRateParams) => `Selecciona una tasa de reembolso por ${unit} del espacio de trabajo`,
+ unapprove: 'Desaprobar',
+ unapproveReport: 'Anular la aprobación del informe',
+ headsUp: 'Atención!',
+ unapproveWithIntegrationWarning: (accountingIntegration: string) =>
+ `Este informe ya se ha exportado a ${accountingIntegration}. Los cambios realizados en este informe en Expensify pueden provocar discrepancias en los datos y problemas de conciliación de la tarjeta Expensify. ¿Está seguro de que desea anular la aprobación de este informe?`,
},
notificationPreferencesPage: {
header: 'Preferencias de avisos',
@@ -864,13 +915,13 @@ export default {
'Este es tu método de contacto predeterminado. Antes de poder eliminarlo, tendrás que elegir otro método de contacto y haz clic en "Establecer como predeterminado".',
removeContactMethod: 'Eliminar método de contacto',
removeAreYouSure: '¿Estás seguro de que quieres eliminar este método de contacto? Esta acción no se puede deshacer.',
- failedNewContact: 'Hubo un error al añadir este método de contacto.',
+ failedNewContact: 'Se ha producido un error al añadir este método de contacto.',
genericFailureMessages: {
requestContactMethodValidateCode: 'No se ha podido enviar un nuevo código mágico. Espera un rato y vuelve a intentarlo.',
validateSecondaryLogin: 'Código mágico incorrecto o no válido. Inténtalo de nuevo o solicita otro código.',
deleteContactMethod: 'No se ha podido eliminar este método de contacto. Por favor, contacta con Concierge para obtener ayuda.',
setDefaultContactMethod: 'No se pudo establecer un nuevo método de contacto predeterminado. Por favor contacta con Concierge para obtener ayuda.',
- addContactMethod: 'Hubo un error al añadir este método de contacto. Por favor, contacta con Concierge para obtener ayuda.',
+ addContactMethod: 'Se ha producido un error al añadir este método de contacto. Por favor, contacta con Concierge para obtener ayuda.',
enteredMethodIsAlreadySubmited: 'El método de contacto ingresado ya existe.',
passwordRequired: 'Se requiere contraseña',
contactMethodRequired: 'Se requiere método de contacto.',
@@ -907,7 +958,7 @@ export default {
timezonePage: {
timezone: 'Zona horaria',
isShownOnProfile: 'Tu zona horaria se muestra en tu perfil.',
- getLocationAutomatically: 'Detecta tu ubicación automáticamente.',
+ getLocationAutomatically: 'Detecta tu ubicación automáticamente',
},
updateRequiredView: {
updateRequired: 'Actualización requerida',
@@ -957,6 +1008,8 @@ export default {
deviceCredentials: 'Credenciales del dispositivo',
invalidate: 'Invalidar',
destroy: 'Destruir',
+ maskExportOnyxStateData: 'Enmascare los datos frágiles del usuario mientras exporta el estado Onyx',
+ exportOnyxState: 'Exportar estado Onyx',
},
debugConsole: {
saveLog: 'Guardar registro',
@@ -1026,6 +1079,12 @@ export default {
enabled: '¡La autenticación de dos factores está ahora habilitada!',
congrats: 'Felicidades, ahora tienes esa seguridad adicional.',
copy: 'Copiar',
+ disable: 'Deshabilitar',
+ enableTwoFactorAuth: 'Activar la autenticación de dos factores',
+ pleaseEnableTwoFactorAuth: 'Activa la autenticación de dos factores.',
+ twoFactorAuthIsRequiredDescription: 'La autenticación de dos factores es necesaria para conectarse a Xero. Activa la autenticación de dos factores para continuar.',
+ twoFactorAuthIsRequiredForAdminsDescription:
+ 'La autenticación de dos factores es necesaria para los administradores del área de trabajo de Xero. Activa la autenticación de dos factores para continuar.',
},
recoveryCodeForm: {
error: {
@@ -1087,7 +1146,7 @@ export default {
addressStreet: 'Por favor, introduce una dirección de facturación válida que no sea un apartado postal.',
addressState: 'Por favor, selecciona un estado.',
addressCity: 'Por favor, introduce una ciudad.',
- genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Por favor, vuelva a intentarlo.',
+ genericFailureMessage: 'Se ha producido un error al añadir tu tarjeta. Por favor, vuelva a intentarlo.',
password: 'Por favor, introduce tu contraseña de Expensify.',
},
},
@@ -1110,7 +1169,7 @@ export default {
addressStreet: 'Por favor, introduce una dirección de facturación válida que no sea un apartado postal.',
addressState: 'Por favor, selecciona un estado.',
addressCity: 'Por favor, introduce una ciudad.',
- genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Por favor, vuelva a intentarlo.',
+ genericFailureMessage: 'Se ha producido un error al añadir tu tarjeta. Por favor, vuelva a intentarlo.',
password: 'Por favor, introduce tu contraseña de Expensify.',
},
},
@@ -1121,9 +1180,9 @@ export default {
deleteAccount: 'Eliminar cuenta',
deleteConfirmation: '¿Estás seguro de que quieres eliminar esta cuenta?',
error: {
- notOwnerOfBankAccount: 'Ha ocurrido un error al establecer esta cuenta bancaria como método de pago predeterminado.',
+ notOwnerOfBankAccount: 'Se ha producido un error al establecer esta cuenta bancaria como método de pago predeterminado.',
invalidBankAccount: 'Esta cuenta bancaria está temporalmente suspendida.',
- notOwnerOfFund: 'Ha ocurrido un error al establecer esta tarjeta de crédito como método de pago predeterminado.',
+ notOwnerOfFund: 'Se ha producido un error al establecer esta tarjeta de crédito como método de pago predeterminado.',
setDefaultFailure: 'No se ha podido configurar el método de pago.',
},
addBankAccountFailure: 'Ocurrió un error inesperado al intentar añadir la cuenta bancaria. Inténtalo de nuevo.',
@@ -1338,7 +1397,7 @@ export default {
groupMembersListTitle: 'Directorio de los miembros del grupo.',
lastMemberTitle: '¡Atención!',
lastMemberWarning: 'Ya que eres la última persona aquí, si te vas, este chat quedará inaccesible para todos los miembros. ¿Estás seguro de que quieres salir del chat?',
- defaultReportName: ({displayName}: {displayName: string}) => `Chat de group de ${displayName}`,
+ defaultReportName: ({displayName}: {displayName: string}) => `Chat de grupo de ${displayName}`,
},
languagePage: {
language: 'Idioma',
@@ -1434,6 +1493,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',
@@ -1451,6 +1511,7 @@ export default {
error: {
containsReservedWord: 'El nombre no puede contener las palabras Expensify o Concierge.',
hasInvalidCharacter: 'El nombre no puede contener una coma o un punto y coma.',
+ requiredFirstName: 'El nombre no puede estar vacío.',
},
},
privatePersonalDetails: {
@@ -1500,7 +1561,7 @@ export default {
localTime: 'Hora local',
},
newChatPage: {
- startGroup: 'Grupo de inicio',
+ startGroup: 'Crear grupo',
addToGroup: 'Añadir al grupo',
},
yearPickerPage: {
@@ -1606,7 +1667,7 @@ export default {
},
hasPhoneLoginError:
'Para añadir una cuenta bancaria verificada, asegúrate de que tu nombre de usuario principal sea un correo electrónico válido y vuelve a intentarlo. Puedes añadir tu número de teléfono como nombre de usuario secundario.',
- hasBeenThrottledError: 'Se produjo un error al intentar añadir tu cuenta bancaria. Por favor, espera unos minutos e inténtalo de nuevo.',
+ hasBeenThrottledError: 'Se ha producido un error al intentar añadir tu cuenta bancaria. Por favor, espera unos minutos e inténtalo de nuevo.',
hasCurrencyError:
'¡Ups! Parece que la moneda de tu espacio de trabajo está configurada en una moneda diferente a USD. Para continuar, por favor configúrala en USD e inténtalo nuevamente.',
error: {
@@ -1652,7 +1713,7 @@ export default {
unknownFilename: 'Archivo desconocido',
passwordRequired: 'Por favor, introduce tu contraseña',
passwordIncorrect: 'Contraseña incorrecta. Por favor, inténtalo de nuevo.',
- failedToLoadPDF: 'Hubo un error al intentar cargar el PDF.',
+ failedToLoadPDF: 'Se ha producido un error al intentar cargar el PDF.',
pdfPasswordForm: {
title: 'PDF protegido con contraseña',
infoText: 'Este PDF esta protegido con contraseña.',
@@ -1674,7 +1735,7 @@ export default {
verifyIdentity: 'Verificar identidad',
letsVerifyIdentity: '¡Vamos a verificar tu identidad!',
butFirst: 'Pero primero, lo aburrido. Lee la jerga legal en el siguiente paso y haz clic en "Aceptar" cuando estés listo.',
- genericError: 'Hubo un error al procesar este paso. Inténtalo de nuevo.',
+ genericError: 'Se ha producido un error al procesar este paso. Inténtalo de nuevo.',
cameraPermissionsNotGranted: 'Permiso para acceder a la cámara',
cameraRequestMessage: 'Necesitamos acceso a tu cámara para completar la verificación de tu cuenta de banco. Por favor habilita los permisos en Configuración > Nuevo Expensify.',
microphonePermissionsNotGranted: 'Permiso para acceder al micrófono',
@@ -1987,6 +2048,7 @@ export default {
workspace: {
common: {
card: 'Tarjetas',
+ expensifyCard: 'Tarjeta Expensify',
workflows: 'Flujos de trabajo',
workspace: 'Espacio de trabajo',
edit: 'Editar espacio de trabajo',
@@ -2030,6 +2092,25 @@ 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',
+ markAsExported: 'Marcar como introducido manualmente',
+ exportIntegrationSelected: (connectionName: ConnectionName) => `Exportar a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
+ 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',
+ shareNote: {
+ header: 'Comparte fácilmente tu espacio de trabajo con otros miembros.',
+ content: {
+ firstPart:
+ 'Comparte este código QR o copia el enlace de abajo para facilitar que los miembros soliciten acceso a tu espacio de trabajo. Todas las solicitudes para unirse al espacio de trabajo aparecerán en la sala',
+ secondPart: 'para tu revisión.',
+ },
+ },
+ createNewConnection: 'Crear una nueva conexión',
+ reuseExistingConnection: 'Reutilizar la conexión existente',
+ existingConnections: 'Conexiones existentes',
+ lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Última sincronización ${formattedDate}`,
},
qbo: {
importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.',
@@ -2046,13 +2127,10 @@ export default {
taxesJournalEntrySwitchNote: 'QuickBooks Online no permite impuestos en los asientos contables. Por favor, cambia la opción de exportación a factura de proveedor o cheque.',
locationsAdditionalDescription:
'QuickBooks Online no permite lugares en facturas de proveedores o cheques. Como tienes activadas los lugares en tu espacio de trabajo, estas opciones de exportación no están disponibles.',
- export: 'Exportar',
- exportAs: 'Exportar cómo',
exportExpenses: 'Exportar gastos de bolsillo como',
exportInvoices: 'Exportar facturas a',
exportCompany: 'Exportar tarjetas de empresa como',
exportDescription: 'Configura cómo se exportan los datos de Expensify a QuickBooks Online.',
- preferredExporter: 'Exportador preferido',
date: 'Fecha de exportación',
deepDiveExpensifyCard: 'Las transacciones de la Tarjeta Expensify se exportan automáticamente a una "Cuenta de Responsabilidad de la Tarjeta Expensify" creada con',
deepDiveExpensifyCardIntegration: 'nuestra integración.',
@@ -2082,17 +2160,14 @@ export default {
account: 'Cuenta',
accountDescription: 'Elige dónde contabilizar los asientos contables.',
vendor: 'Proveedor',
- defaultVendor: 'Proveedor predeterminado',
defaultVendorDescription: 'Establece un proveedor predeterminado que se aplicará a todas las transacciones con tarjeta de crédito al momento de exportarlas.',
accountsPayable: 'Cuentas por pagar',
accountsPayableDescription: 'Elige dónde crear las facturas de proveedores.',
bankAccount: 'Cuenta bancaria',
bankAccountDescription: 'Elige desde dónde enviar los cheques.',
+ creditCardAccount: 'Cuenta de la tarjeta de crédito',
companyCardsLocationEnabledDescription:
'QuickBooks Online no permite lugares en las exportaciones de facturas de proveedores. Como tienes activadas los lugares en tu espacio de trabajo, esta opción de exportación no está disponible.',
- exportPreferredExporterNote:
- 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.',
- exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en tu cuenta.',
exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Online.',
exportCheckDescription: 'Crearemos un cheque desglosado para cada informe de Expensify y lo enviaremos desde la cuenta bancaria a continuación.',
exportJournalEntryDescription: 'Crearemos una entrada contable desglosada para cada informe de Expensify y lo contabilizaremos en la cuenta a continuación.',
@@ -2107,14 +2182,11 @@ export default {
'QuickBooks Online no permite lugares en facturas de proveedores o cheques. Como tienes activadas los lugares en tu espacio de trabajo, estas opciones de exportación no están disponibles.',
advancedConfig: {
- advanced: 'Avanzado',
- autoSync: 'Autosincronización',
autoSyncDescription: 'Expensify se sincronizará automáticamente con QuickBooks Online todos los días.',
inviteEmployees: 'Invitar empleados',
inviteEmployeesDescription: 'Importe los registros de los empleados de Quickbooks Online e invítelos a este espacio de trabajo.',
createEntities: 'Crear entidades automáticamente',
createEntitiesDescription: 'Expensify creará automáticamente proveedores en QuickBooks Online si aún no existen, y creará automáticamente clientes al exportar facturas.',
- reimbursedReports: 'Sincronizar informes reembolsados',
reimbursedReportsDescription:
'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Quickbooks Online indicadas a continuación.',
qboBillPaymentAccount: 'Cuenta de pago de las facturas de QuickBooks',
@@ -2170,7 +2242,6 @@ export default {
default: 'Contacto de Xero por defecto',
tag: 'Etiquetas',
},
- export: 'Exportar',
exportDescription: 'Configura cómo se exportan los datos de Expensify a Xero.',
exportCompanyCard: 'Exportar gastos de la tarjeta de empresa como',
purchaseBill: 'Factura de compra',
@@ -2179,7 +2250,6 @@ export default {
bankTransactions: 'Transacciones bancarias',
xeroBankAccount: 'Cuenta bancaria de Xero',
xeroBankAccountDescription: 'Elige dónde se contabilizarán los gastos como transacciones bancarias.',
- preferredExporter: 'Exportador preferido',
exportExpenses: 'Exportar gastos por cuenta propia como',
exportExpensesDescription: 'Los informes se exportarán como una factura de compra utilizando la fecha y el estado que seleccione a continuación',
purchaseBillDate: 'Fecha de la factura de compra',
@@ -2187,11 +2257,8 @@ export default {
salesInvoice: 'Factura de venta',
exportInvoicesDescription: 'Las facturas de venta siempre muestran la fecha en la que se envió la factura.',
advancedConfig: {
- advanced: 'Avanzado',
- autoSync: 'Autosincronización',
autoSyncDescription: 'Expensify se sincronizará automáticamente con Xero todos los días.',
purchaseBillStatusTitle: 'Estado de la factura de compra',
- reimbursedReports: 'Sincronizar informes reembolsados',
reimbursedReportsDescription:
'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Xero indicadas a continuación.',
xeroBillPaymentAccount: 'Cuenta de pago de las facturas de Xero',
@@ -2226,17 +2293,364 @@ export default {
[CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_PAYMENT]: 'Pendiente de pago',
},
},
+ 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: 'Exportar gastos por cuenta propia como',
+ description: 'Establece cómo se exportan los gastos por cuenta propia a Sage Intacct.',
+ 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 tarjetas de empresa como',
+ description: 'Establece cómo se exportan las compras con tarjeta de empresa a Sage Intacct.',
+ values: {
+ [CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE]: 'Tarjetas 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:
- 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.',
+ '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 Xero y sincroniza de nuevo la conexión.',
+ 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.',
+ exportDescription: 'Configura cómo se exportan los datos de Expensify a NetSuite.',
+ exportReimbursable: 'Exportar gastos reembolsables como',
+ exportNonReimbursable: 'Exportar gastos no reembolsables como',
+ exportInvoices: 'Exportar facturas a',
+ journalEntriesTaxPostingAccount: 'Cuenta de registro de impuestos de asientos contables',
+ journalEntriesProvTaxPostingAccount: 'Cuenta de registro de impuestos provinciales de asientos contables',
+ foreignCurrencyAmount: 'Exportar importe en moneda extranjera',
+ exportToNextOpenPeriod: 'Exportar al siguiente período abierto',
+ nonReimbursableJournalPostingAccount: 'Cuenta de registro de diario no reembolsable',
+ reimbursableJournalPostingAccount: 'Cuenta de registro de diario reembolsable',
+ journalPostingPreference: {
+ label: 'Preferencia de registro de asientos contables',
+ values: {
+ [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE]: 'Entrada única y detallada para cada informe',
+ [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_TOTAL_LINE]: 'Entrada única para cada gasto individual',
+ },
+ },
+ invoiceItem: {
+ label: 'Artículo de la factura',
+ values: {
+ [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: {
+ label: 'Crear uno para mí',
+ description: "Crearemos un 'Artículo de línea de factura de Expensify' para ti al exportar (si aún no existe).",
+ },
+ [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: {
+ label: 'Seleccionar existente',
+ description: 'Asociaremos las facturas de Expensify al artículo seleccionado a continuación.',
+ },
+ },
+ },
+ exportDate: {
+ label: 'Fecha de exportación',
+ description: 'Usa esta fecha al exportar informe a NetSuite.',
+ values: {
+ [CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE]: {
+ label: 'Fecha del último gasto',
+ description: 'Fecha del gasto mas reciente en el informe.',
+ },
+ [CONST.NETSUITE_EXPORT_DATE.EXPORTED]: {
+ label: 'Fecha de exportación',
+ description: 'Fecha de exportación del informe a NetSuite.',
+ },
+ [CONST.NETSUITE_EXPORT_DATE.SUBMITTED]: {
+ label: 'Fecha de envío',
+ description: 'Fecha en la que el informe se envió para su aprobación.',
+ },
+ },
+ },
+ exportDestination: {
+ values: {
+ [CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT]: {
+ label: 'Informes de gastos',
+ reimbursableDescription: 'Los gastos reembolsables se exportarán como informes de gastos a NetSuite.',
+ nonReimbursableDescription: 'Los gastos no reembolsables se exportarán como informes de gastos a NetSuite.',
+ },
+ [CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL]: {
+ label: 'Facturas de proveedores',
+ reimbursableDescription:
+ 'Los gastos reembolsables se exportarán como facturas pagaderas al proveedor especificado en NetSuite.\n' +
+ '\n' +
+ 'Si deseas establecer un proveedor específico para cada tarjeta, ve a *Configuraciones > Dominios > Tarjetas de Empresa*.',
+ nonReimbursableDescription:
+ 'Los gastos no reembolsables se exportarán como facturas pagaderas al proveedor especificado en NetSuite.\n' +
+ '\n' +
+ 'Si deseas establecer un proveedor específico para cada tarjeta, ve a *Configuraciones > Dominios > Tarjetas de Empresa*.',
+ },
+ [CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY]: {
+ label: 'Asientos contables',
+ reimbursableDescription:
+ 'Los gastos reembolsables se exportarán como asientos contables a la cuenta especificada en NetSuite.\n' +
+ '\n' +
+ 'Si deseas establecer un proveedor específico para cada tarjeta, ve a *Configuraciones > Dominios > Tarjetas de Empresa*.',
+ nonReimbursableDescription:
+ 'Los gastos no reembolsables se exportarán como asientos contables a la cuenta especificada en NetSuite.\n' +
+ '\n' +
+ 'Si deseas establecer un proveedor específico para cada tarjeta, ve a *Configuraciones > Dominios > Tarjetas de Empresa*.',
+ },
+ },
+ },
+ advancedConfig: {
+ autoSyncDescription: 'Expensify se sincronizará automáticamente con NetSuite todos los días.',
+ reimbursedReportsDescription:
+ 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de NetSuite indicadas a continuación.',
+ reimbursementsAccount: 'Cuenta de reembolsos',
+ reimbursementsAccountDescription: 'Elija la cuenta bancaria que utilizará para los reembolsos y crearemos el pago asociado en NetSuite.',
+ collectionsAccount: 'Cuenta de cobros',
+ collectionsAccountDescription: 'Una vez que una factura se marca como pagada en Expensify y se exporta a NetSuite, aparecerá contra la cuenta de abajo.',
+ approvalAccount: 'Cuenta de aprobación de cuentas por pagar',
+ approvalAccountDescription:
+ 'Elija la cuenta con la que se aprobarán las transacciones en NetSuite. Si está sincronizando informes reembolsados, esta es también la cuenta con la que se crearán los pagos de facturas.',
+ defaultApprovalAccount: 'Preferencia predeterminada de NetSuite',
+ inviteEmployees: 'Invitar empleados y establecer aprobaciones',
+ inviteEmployeesDescription:
+ 'Importar registros de empleados de NetSuite e invitar a 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 más en la página *Miembros*.',
+ autoCreateEntities: 'Crear automáticamente empleados/proveedores',
+ enableCategories: 'Activar categorías recién importadas',
+ customFormID: 'ID de formulario personalizado',
+ customFormIDDescription:
+ 'Por defecto, Expensify creará entradas utilizando el formulario de transacción preferido configurado en NetSuite. Alternativamente, tienes la opción de designar un formulario de transacción específico para ser utilizado.',
+ customFormIDReimbursable: 'Gasto reembolsable',
+ customFormIDNonReimbursable: 'Gasto no reembolsable',
+ exportReportsTo: {
+ label: 'Nivel de aprobación del informe de gastos',
+ description:
+ 'Una vez aprobado un informe de gastos en Expensify y exportado a NetSuite, puede establecer un nivel adicional de aprobación en NetSuite antes de su contabilización.',
+ values: {
+ [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_APPROVED_NONE]: 'Preferencia predeterminada de NetSuite',
+ [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_SUPERVISOR_APPROVED]: 'Solo aprobado por el supervisor',
+ [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_ACCOUNTING_APPROVED]: 'Solo aprobado por contabilidad',
+ [CONST.NETSUITE_REPORTS_APPROVAL_LEVEL.REPORTS_APPROVED_BOTH]: 'Aprobado por supervisor y contabilidad',
+ },
+ },
+ exportVendorBillsTo: {
+ label: 'Nivel de aprobación de facturas de proveedores',
+ description:
+ 'Una vez aprobada una factura de proveedor en Expensify y exportada a NetSuite, puede establecer un nivel adicional de aprobación en NetSuite antes de su contabilización.',
+ values: {
+ [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVED_NONE]: 'Preferencia predeterminada de NetSuite',
+ [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVAL_PENDING]: 'Aprobación pendiente',
+ [CONST.NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL.VENDOR_BILLS_APPROVED]: 'Aprobado para publicación',
+ },
+ },
+ exportJournalsTo: {
+ label: 'Nivel de aprobación de asientos contables',
+ description: 'Una vez aprobado un asiento en Expensify y exportado a NetSuite, puede establecer un nivel adicional de aprobación en NetSuite antes de contabilizarlo.',
+ values: {
+ [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED_NONE]: 'Preferencia predeterminada de NetSuite',
+ [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVAL_PENDING]: 'Aprobación pendiente',
+ [CONST.NETSUITE_JOURNALS_APPROVAL_LEVEL.JOURNALS_APPROVED]: 'Aprobado para publicación',
+ },
+ },
+ error: {
+ customFormID: 'Introduzca un ID numérico válido para el formulario personalizado.',
+ },
+ },
+ noAccountsFound: 'No se han encontrado cuentas',
+ noAccountsFoundDescription: 'Añade la cuenta en NetSuite y sincroniza la conexión de nuevo.',
+ noVendorsFound: 'No se han encontrado proveedores',
+ noVendorsFoundDescription: 'Añade proveedores en NetSuite y sincroniza la conexión de nuevo.',
+ noItemsFound: 'No se han encontrado artículos de factura',
+ noItemsFoundDescription: 'Añade artículos de factura en NetSuite y sincroniza la conexión de nuevo.',
noSubsidiariesFound: 'No se ha encontrado subsidiarias',
noSubsidiariesFoundDescription: 'Añade la subsidiaria en NetSuite y sincroniza de nuevo la conexión.',
+ tokenInput: {
+ title: 'Netsuite configuración',
+ formSteps: {
+ installBundle: {
+ title: 'Instala el paquete de Expensify',
+ description: 'En NetSuite, ir a *Personalización > SuiteBundler > Buscar e Instalar Paquetes* > busca "Expensify" > instala el paquete.',
+ },
+ enableTokenAuthentication: {
+ title: 'Habilitar la autenticación basada en token',
+ description: 'En NetSuite, ir a *Configuración > Empresa > Habilitar Funciones > SuiteCloud* > activar *autenticación basada en token*.',
+ },
+ enableSoapServices: {
+ title: 'Habilitar servicios web SOAP',
+ description: 'En NetSuite, ir a *Configuración > Empresa > Habilitar funciones > SuiteCloud* > habilitar *Servicios Web SOAP*.',
+ },
+ createAccessToken: {
+ title: 'Crear un token de acceso',
+ description:
+ 'En NetSuite, ir a *Configuración > Usuarios/Roles > Tokens de Acceso* > crear un token de acceso para la aplicación "Expensify" y tambiém para el rol de "Integración Expensify" o "Administrador".\n\n*Importante:* Asegúrese de guardar el ID y el secreto del Token en este paso. Los necesitará para el siguiente paso.',
+ },
+ enterCredentials: {
+ title: 'Ingresa tus credenciales de NetSuite',
+ formInputs: {
+ netSuiteAccountID: 'ID de Cuenta NetSuite',
+ netSuiteTokenID: 'ID de Token',
+ netSuiteTokenSecret: 'Secreto de Token',
+ },
+ netSuiteAccountIDDescription: 'En NetSuite, ir a *Configuración > Integración > Preferencias de Servicios Web SOAP*.',
+ },
+ },
+ },
+ import: {
+ expenseCategories: 'Categorías de gastos',
+ expenseCategoriesDescription: 'Las categorías de gastos de NetSuite se importan a Expensify como categorías.',
+ crossSubsidiaryCustomers: 'Clientes/proyectos entre subsidiaria',
+ importFields: {
+ departments: {
+ title: 'Departamentos',
+ subtitle: 'Elige cómo manejar los *departamentos* de NetSuite en Expensify.',
+ },
+ classes: {
+ title: 'Clases',
+ subtitle: 'Elige cómo manejar las *clases* en Expensify.',
+ },
+ locations: {
+ title: 'Ubicaciones',
+ subtitle: 'Elija cómo manejar *ubicaciones* en Expensify.',
+ },
+ },
+ customersOrJobs: {
+ title: 'Clientes / proyectos',
+ subtitle: 'Elija cómo manejar los *clientes* y *proyectos* de NetSuite en Expensify.',
+ importCustomers: 'Importar clientes',
+ importJobs: 'Importar proyectos',
+ customers: 'clientes',
+ jobs: 'proyectos',
+ label: (importFields: string[], importType: string) => `${importFields.join(' y ')}, ${importType}`,
+ },
+ importTaxDescription: 'Importar grupos de impuestos desde NetSuite.',
+ importCustomFields: {
+ 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]: {
+ label: 'Predeterminado del empleado NetSuite',
+ description: 'No importado a Expensify, aplicado en exportación',
+ footerContent: (importField: string) =>
+ `Si usa ${importField} en NetSuite, aplicaremos el conjunto predeterminado en el registro del empleado al exportarlo a Informe de gastos o Entrada de diario.`,
+ },
+ [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: {
+ label: 'Etiquetas',
+ description: 'Nivel de línea de pedido',
+ footerContent: (importField: string) => `Se podrán seleccionar ${importField} para cada gasto individual en el informe de un empleado.`,
+ },
+ [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: {
+ label: 'Campos de informe',
+ description: 'Nivel de informe',
+ footerContent: (importField: string) => `La selección de ${importField} se aplicará a todos los gastos en el informe de un empleado.`,
+ },
+ },
+ },
},
intacct: {
sageIntacctSetup: 'Sage Intacct configuración',
@@ -2244,16 +2658,81 @@ export default {
downloadExpensifyPackage: 'Descargar el paquete Expensify para Sage Intacct',
followSteps: 'Siga los pasos de nuestras instrucciones Cómo: Instrucciones para conectarse a Sage Intacct',
enterCredentials: 'Introduzca sus credenciales de Sage Intacct',
- createNewConnection: 'Crear una nueva conexión',
- reuseExistingConnection: 'Reutilizar la conexión existente',
- existingConnections: 'Conexiones existentes',
- sageIntacctLastSync: (formattedDate: string) => `Sage Intacct - Última sincronización ${formattedDate}`,
+ entity: 'Entidad',
+ 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',
control: 'Control',
collect: 'Recolectar',
},
+ expensifyCard: {
+ issueCard: 'Emitir tarjeta',
+ name: 'Nombre',
+ lastFour: '4 últimos',
+ limit: 'Limite',
+ currentBalance: 'Saldo actual',
+ currentBalanceDescription:
+ 'El saldo actual es la suma de todas las transacciones contabilizadas con la Tarjeta Expensify que se han producido desde la última fecha de liquidación.',
+ cardLimit: 'Límite de la tarjeta',
+ remainingLimit: 'Límite restante',
+ requestLimitIncrease: 'Solicitar aumento de límite',
+ remainingLimitDescription:
+ 'A la hora de calcular tu límite restante, tenemos en cuenta una serie de factores: su antigüedad como cliente, la información relacionada con tu negocio que nos facilitaste al darte de alta y el efectivo disponible en tu cuenta bancaria comercial. Tu límite restante puede fluctuar a diario.',
+ cashBack: 'Reembolso',
+ cashBackDescription: 'El saldo de devolución se basa en el gasto mensual realizado con la tarjeta Expensify en tu espacio de trabajo.',
+ issueNewCard: '',
+ finishSetup: 'Terminar configuración',
+ chooseBankAccount: 'Elegir cuenta bancaria',
+ chooseExistingBank: 'Elige una cuenta bancaria comercial existente para pagar el saldo de su Tarjeta Expensify o añade una nueva cuenta bancaria.',
+ accountEndingIn: 'Cuenta terminada en',
+ addNewBankAccount: 'Añadir nueva cuenta bancaria',
+ cardDetails: 'Datos de la tarjeta',
+ virtual: 'Virtual',
+ physical: 'Física',
+ deactivate: 'Desactivar tarjeta',
+ changeCardLimit: 'Modificar el límite de la tarjeta',
+ changeLimit: 'Modificar límite',
+ smartLimitWarning: (limit: string) =>
+ `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta que apruebes antiguos gastos de la tarjeta.`,
+ monthlyLimitWarning: (limit: string) => `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta el próximo mes.`,
+ fixedLimitWarning: (limit: string) => `Si cambias el límite de esta tarjeta a ${limit}, se rechazarán las nuevas transacciones.`,
+ },
categories: {
deleteCategories: 'Eliminar categorías',
deleteCategoriesPrompt: '¿Estás seguro de que quieres eliminar estas categorías?',
@@ -2281,6 +2760,10 @@ export default {
existingCategoryError: 'Ya existe una categoría con este nombre.',
invalidCategoryName: 'Lo nombre de la categoría es invalido.',
importedFromAccountingSoftware: 'Categorías importadas desde',
+ payrollCode: 'Código de nómina',
+ updatePayrollCodeFailureMessage: 'Se produjo un error al actualizar el código de nómina, por favor intente nuevamente.',
+ glCode: 'Código de Libro Mayor',
+ updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Inténtelo nuevamente.',
},
moreFeatures: {
spendSection: {
@@ -2295,6 +2778,23 @@ export default {
title: 'Integrar',
subtitle: 'Conecta Expensify a otros productos financieros populares.',
},
+ expensifyCard: {
+ title: 'Tarjeta Expensify',
+ subtitle: 'Obtén información y control sobre tus gastos',
+ disableCardTitle: 'Deshabilitar la Tarjeta Expensify',
+ disableCardPrompt: 'No puedes deshabilitar la Tarjeta Expensify porque ya está en uso. Por favor, contacta con Concierge para conocer los pasos a seguir.',
+ disableCardButton: 'Chatear con Concierge',
+ feed: {
+ title: 'Consigue la Tarjeta Expensify',
+ subTitle: 'Optimiza tu negocio con la Tarjeta Expensify',
+ features: {
+ cashBack: 'Hasta un 2% de devolución en cada compra en Estadios Unidos',
+ unlimited: 'Emitir un número ilimitado de tarjetas virtuales',
+ spend: 'Controles de gastos y límites personalizados',
+ },
+ ctaTitle: 'Emitir nueva tarjeta',
+ },
+ },
distanceRates: {
title: 'Tasas de distancia',
subtitle: 'Añade, actualiza y haz cumplir las tasas.',
@@ -2332,15 +2832,51 @@ 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.',
},
- subtitle: 'Los campos de informe se aplican a todos los gastos y pueden ser útiles cuando desees solicitar información adicional',
+ subtitle: 'Los campos de informe se aplican a todos los gastos y pueden ser útiles cuando quieras solicitar información adicional.',
disableReportFields: 'Desactivar campos de informe',
disableReportFieldsConfirmation: 'Estás seguro? Se eliminarán los campos de texto y fecha y se desactivarán las listas.',
+ importedFromAccountingSoftware: 'Campos de informes importadas desde',
+ textType: 'Texto',
+ dateType: 'Fecha',
+ dropdownType: 'Lista',
+ textAlternateText: 'Añade un campo para introducir texto libre.',
+ dateAlternateText: 'Añade un calendario para la selección de fechas.',
+ dropdownAlternateText: 'Añade una lista de opciones para elegir.',
+ nameInputSubtitle: 'Elige un nombre para el campo del informe.',
+ typeInputSubtitle: 'Elige qué tipo de campo de informe utilizar.',
+ initialValueInputSubtitle: 'Ingresa un valor inicial para mostrar en el campo del informe.',
+ listValuesInputSubtitle: 'Estos valores aparecerán en el desplegable del campo de tu informe. Los miembros pueden seleccionar los valores habilitados.',
+ listInputSubtitle: 'Estos valores aparecerán en la lista de campos de tu informe. Los miembros pueden seleccionar los valores habilitados.',
+ deleteValue: 'Eliminar valor',
+ deleteValues: 'Eliminar valores',
+ disableValue: 'Desactivar valor',
+ disableValues: 'Desactivar valores',
+ enableValue: 'Habilitar valor',
+ enableValues: 'Habilitar valores',
+ emptyReportFieldsValues: {
+ title: 'No has creado ningún valor en la lista',
+ subtitle: 'Añade valores personalizados para que aparezcan en los informes.',
+ },
+ deleteValuePrompt: '¿Estás seguro de que quieres eliminar este valor de la lista?',
+ deleteValuesPrompt: '¿Estás seguro de que quieres eliminar estos valores de la lista?',
+ listValueRequiredError: 'Ingresa un nombre para el valor de la lista',
+ existingListValueError: 'Ya existe un valor en la lista con este nombre',
+ editValue: 'Editar valor',
+ listValues: 'Valores de la lista',
+ addValue: 'Añade valor',
+ existingReportFieldNameError: 'Ya existe un campo de informe con este nombre',
+ reportFieldNameRequiredError: 'Ingresa un nombre de campo de informe',
+ reportFieldTypeRequiredError: 'Elige un tipo de campo de informe',
+ reportFieldInitialValueRequiredError: 'Elige un valor inicial de campo de informe',
+ genericFailureMessage: 'Se ha producido un error al actualizar el campo de informe. Por favor, inténtalo de nuevo.',
},
tags: {
tagName: 'Nombre de etiqueta',
@@ -2364,8 +2900,10 @@ export default {
deleteFailureMessage: 'Se ha producido un error al intentar eliminar la etiqueta. Por favor, inténtalo más tarde.',
tagRequiredError: 'Lo nombre de la etiqueta es obligatorio.',
existingTagError: 'Ya existe una etiqueta con este nombre.',
- genericFailureMessage: 'Se produjo un error al actualizar la etiqueta. Por favor, inténtelo nuevamente.',
+ genericFailureMessage: 'Se ha producido un error al actualizar la etiqueta. Por favor, inténtelo nuevamente.',
importedFromAccountingSoftware: 'Etiquetas importadas desde',
+ glCode: 'Código de Libro Mayor',
+ updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Por favor, inténtelo nuevamente.',
},
taxes: {
subtitle: 'Añade nombres, tasas y establezca valores por defecto para los impuestos.',
@@ -2378,6 +2916,7 @@ export default {
taxReclaimableOn: 'Impuesto recuperable en',
error: {
taxRateAlreadyExists: 'Ya existe un impuesto con este nombre.',
+ taxCodeAlreadyExists: 'Ya existe un código de impuesto con este nombre.',
customNameRequired: 'El nombre del impuesto es obligatorio.',
valuePercentageRange: 'Por favor, introduce un porcentaje entre 0 y 100.',
deleteFailureMessage: 'Se ha producido un error al intentar eliminar la tasa de impuesto. Por favor, inténtalo más tarde.',
@@ -2396,6 +2935,8 @@ export default {
enableMultiple: 'Activar tasas',
},
importedFromAccountingSoftware: 'Impuestos importadas desde',
+ taxCode: 'Código de impuesto',
+ updateTaxCodeFailureMessage: 'Se produjo un error al actualizar el código tributario, inténtelo nuevamente.',
},
emptyWorkspace: {
title: 'Crea un espacio de trabajo',
@@ -2449,8 +2990,23 @@ export default {
xero: 'Xero',
netsuite: 'NetSuite',
intacct: 'Sage Intacct',
+ connectionName: (integration: ConnectionName) => {
+ switch (integration) {
+ case CONST.POLICY.CONNECTIONS.NAME.QBO:
+ return 'Quickbooks Online';
+ case CONST.POLICY.CONNECTIONS.NAME.XERO:
+ return 'Xero';
+ case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
+ return 'NetSuite';
+ case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT:
+ return 'Sage Intacct';
+ default: {
+ return '';
+ }
+ }
+ },
setup: 'Configurar',
- lastSync: 'Recién sincronizado',
+ lastSync: (relativeDate: string) => `Recién sincronizado ${relativeDate}`,
import: 'Importar',
export: 'Exportar',
advanced: 'Avanzado',
@@ -2487,6 +3043,7 @@ export default {
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NOT_IMPORTED]: 'No importado',
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE]: 'No importado',
[CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Importado como campos de informe',
+ [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Predeterminado del empleado NetSuite',
},
disconnectPrompt: (currentIntegration?: ConnectionName): string => {
const integrationName =
@@ -2542,6 +3099,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':
@@ -2597,11 +3156,32 @@ 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}`;
}
}
},
},
+ preferredExporter: 'Exportador preferido',
+ exportPreferredExporterNote:
+ 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.',
+ exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en tu cuenta.',
+ exportAs: 'Exportar cómo',
+ defaultVendor: 'Proveedor predeterminado',
+ autoSync: 'Autosincronización',
+ reimbursedReports: 'Sincronizar informes reembolsados',
+ cardReconciliation: 'Conciliación de tarjetas',
+ reconciliationAccount: 'Cuenta de conciliación',
+ continuousReconciliation: 'Conciliación continua',
+ saveHoursOnReconciliation:
+ 'Ahorra horas de conciliación en cada período contable haciendo que Expensify concilie continuamente los extractos y liquidaciones de la Tarjeta Expensify en tu nombre.',
+ enableContinuousReconciliation: 'Para activar la Conciliación Continua, activa la ',
+ chooseReconciliationAccount: {
+ chooseBankAccount: 'Elige la cuenta bancaria con la que se conciliarán los pagos de tu Tarjeta Expensify.',
+ accountMatches: 'Asegúrate de que esta cuenta coincide con ',
+ settlementAccount: 'la cuenta de liquidación de tu Tarjeta Expensify ',
+ reconciliationWorks: (lastFourPAN: string) => `(que termina en ${lastFourPAN}) para que la conciliación continua funcione correctamente.`,
+ },
},
card: {
header: 'Desbloquea Tarjetas Expensify gratis',
@@ -2644,6 +3224,11 @@ export default {
limitType: 'Tipo de limite',
name: 'Nombre',
},
+ deactivateCardModal: {
+ deactivate: 'Desactivar',
+ deactivateCard: 'Desactivar tarjeta',
+ deactivateConfirmation: 'Al desactivar esta tarjeta, se rechazarán todas las transacciones futuras y no se podrá deshacer.',
+ },
},
reimburse: {
captureReceipts: 'Captura recibos',
@@ -2664,6 +3249,11 @@ export default {
invalidRateError: 'Por favor, introduce una tarifa válida.',
lowRateError: 'La tarifa debe ser mayor que 0.',
},
+ export: {
+ notReadyHeading: 'No está listo para exportar',
+ notReadyDescription:
+ 'Los borradores o informes de gastos pendientes no se pueden exportar al sistema contabilidad. Por favor, apruebe o pague estos gastos antes de exportarlos.',
+ },
bills: {
manageYourBills: 'Gestiona tus facturas',
askYourVendorsBeforeEmail: 'Pide a tus proveedores que envíen sus facturas a ',
@@ -2705,7 +3295,7 @@ export default {
member: 'Invitar miembros',
members: 'Invitar miembros',
invitePeople: 'Invitar nuevos miembros',
- genericFailureMessage: 'Se produjo un error al invitar al miembro al espacio de trabajo. Vuelva a intentarlo..',
+ genericFailureMessage: 'Se ha producido un error al invitar al miembro al espacio de trabajo. Vuelva a intentarlo.',
pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
user: 'miembro',
users: 'miembros',
@@ -2720,7 +3310,7 @@ export default {
inviteMessagePrompt: 'Añadir un mensaje para hacer tu invitación destacar',
personalMessagePrompt: 'Mensaje',
inviteNoMembersError: 'Por favor, selecciona al menos un miembro a invitar.',
- genericFailureMessage: 'Se produjo un error al invitar al miembro al espacio de trabajo. Por favor, vuelva a intentarlo..',
+ genericFailureMessage: 'Se ha producido un error al invitar al miembro al espacio de trabajo. Por favor, vuelva a intentarlo.',
},
distanceRates: {
oopsNotSoFast: 'Ups! No tan rápido...',
@@ -2745,13 +3335,15 @@ export default {
editor: {
nameInputLabel: 'Nombre',
descriptionInputLabel: 'Descripción',
+ typeInputLabel: 'Tipo',
+ initialValueInputLabel: 'Valor inicial',
nameInputHelpText: 'Este es el nombre que verás en tu espacio de trabajo.',
nameIsRequiredError: 'Debes definir un nombre para tu espacio de trabajo.',
currencyInputLabel: 'Moneda por defecto',
currencyInputHelpText: 'Todas los gastos en este espacio de trabajo serán convertidos a esta moneda.',
currencyInputDisabledText: 'La moneda predeterminada no se puede cambiar porque este espacio de trabajo está vinculado a una cuenta bancaria en USD.',
save: 'Guardar',
- genericFailureMessage: 'Se produjo un error al guardar el espacio de trabajo. Por favor, inténtalo de nuevo.',
+ genericFailureMessage: 'Se ha producido un error al guardar el espacio de trabajo. Por favor, inténtalo de nuevo.',
avatarUploadFailureMessage: 'No se pudo subir el avatar. Por favor, inténtalo de nuevo.',
addressContext: 'Se requiere una dirección para habilitar Expensify Travel. Por favor, introduce una dirección asociada con tu negocio.',
},
@@ -2824,6 +3416,58 @@ export default {
errorDescriptionPartTwo: 'contacta con el conserje',
errorDescriptionPartThree: 'por ayuda.',
},
+
+ exportAgainModal: {
+ title: '¡Cuidado!',
+ description: (reportName: string, connectionName: ConnectionName) =>
+ `Los siguientes informes ya se han exportado a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}:\n\n${reportName}\n\n¿Estás seguro de que deseas exportarlos de nuevo?`,
+ confirmText: 'Sí, exportar de nuevo',
+ cancelText: 'Cancelar',
+ },
+ upgrade: {
+ reportFields: {
+ title: 'Los campos',
+ description: `Los campos de informe permiten especificar detalles a nivel de cabecera, distintos de las etiquetas que pertenecen a los gastos en partidas individuales. Estos detalles pueden incluir nombres de proyectos específicos, información sobre viajes de negocios, ubicaciones, etc.`,
+ onlyAvailableOnPlan: 'Los campos de informe sólo están disponibles en el plan Control, a partir de ',
+ },
+ [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
+ title: 'NetSuite',
+ description: `Disfruta de la sincronización automática y reduce las entradas manuales con la integración Expensify + NetSuite. Obtén información financiera en profundidad y en tiempo real con la compatibilidad nativa y personalizada con segmentos, incluida la asignación de proyectos y clientes.`,
+ onlyAvailableOnPlan: 'Nuestra integración NetSuite sólo está disponible en el plan Control, a partir de ',
+ },
+ [CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]: {
+ title: 'Sage Intacct',
+ description: `Disfruta de una sincronización automatizada y reduce las entradas manuales con la integración Expensify + Sage Intacct. Obtén información financiera en profundidad y en tiempo real con dimensiones definidas por el usuario, así como codificación de gastos por departamento, clase, ubicación, cliente y proyecto (trabajo).`,
+ onlyAvailableOnPlan: 'Nuestra integración Sage Intacct sólo está disponible en el plan Control, a partir de ',
+ },
+ glCodes: {
+ title: 'Códigos de libro mayor',
+ description: `Añada códigos de libro mayor a sus categorías para exportar fácilmente los gastos a sus sistemas de contabilidad y nómina.`,
+ onlyAvailableOnPlan: 'Los códigos de libro mayor solo están disponibles en el plan Control, a partir de ',
+ },
+ glAndPayrollCodes: {
+ title: 'Códigos de libro mayor y nómina',
+ description: `Añada códigos de libro mayor y nómina a sus categorías para exportar fácilmente los gastos a sus sistemas de contabilidad y nómina.`,
+ onlyAvailableOnPlan: 'Los códigos de libro mayor y nómina solo están disponibles en el plan Control, a partir de ',
+ },
+ note: {
+ upgradeWorkspace: 'Mejore su espacio de trabajo para acceder a esta función, o',
+ learnMore: 'más información',
+ aboutOurPlans: 'sobre nuestros planes y precios.',
+ },
+ pricing: {
+ amount: '$9 ',
+ perActiveMember: 'por miembro activo al mes.',
+ },
+ upgradeToUnlock: 'Desbloquear esta función',
+ completed: {
+ headline: 'Has mejorado tu espacio de trabajo.',
+ successMessage: (policyName: string) => `Ha mejorado correctamente su espacio de trabajo ${policyName} al plan Control.`,
+ viewSubscription: 'Ver su suscripción',
+ moreDetails: 'para obtener más información.',
+ gotIt: 'Entendido, gracias.',
+ },
+ },
restrictedAction: {
restricted: 'Restringido',
actionsAreCurrentlyRestricted: ({workspaceName}) => `Las acciones en el espacio de trabajo ${workspaceName} están actualmente restringidas`,
@@ -2924,8 +3568,8 @@ export default {
},
markAsComplete: 'Marcar como completada',
markAsIncomplete: 'Marcar como incompleta',
- assigneeError: 'Hubo un error al asignar esta tarea. Por favor, inténtalo con otro miembro.',
- genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea. Por favor, inténtalo más tarde.',
+ assigneeError: 'Se ha producido un error al asignar esta tarea. Por favor, inténtalo con otro miembro.',
+ genericCreateTaskFailureMessage: 'Error inesperado al crear la tarea. Por favor, inténtalo más tarde.',
deleteTask: 'Eliminar tarea',
deleteConfirmation: '¿Estás seguro de que quieres eliminar esta tarea?',
},
@@ -2950,6 +3594,7 @@ export default {
screenShareRequest: 'Expensify te está invitando a compartir la pantalla',
},
search: {
+ selectMultiple: 'Seleccionar varios',
resultsAreLimited: 'Los resultados de búsqueda están limitados.',
searchResults: {
emptyResults: {
@@ -2958,6 +3603,20 @@ export default {
},
},
groupedExpenses: 'gastos agrupados',
+ bulkActions: {
+ delete: 'Eliminar',
+ hold: 'Bloquear',
+ unhold: 'Desbloquear',
+ noOptionsAvailable: 'No hay opciones disponibles para el grupo de gastos seleccionado.',
+ },
+ offlinePrompt: 'No puedes realizar esta acción ahora mismo.',
+ filtersHeader: 'Filtros',
+ filters: {
+ date: {
+ before: 'Antes de',
+ after: 'Después de',
+ },
+ },
},
genericErrorPage: {
title: '¡Oh-oh, algo salió mal!',
@@ -3058,6 +3717,40 @@ export default {
genericUpdateReportFieldFailureMessage: 'Error inesperado al actualizar el campo. Por favor, inténtalo más tarde.',
genericUpdateReporNameEditFailureMessage: 'Error inesperado al cambiar el nombre del informe. Por favor, intentarlo más tarde.',
noActivityYet: 'Sin actividad todavía',
+ actions: {
+ type: {
+ changeField: ({oldValue, newValue, fieldName}: ChangeFieldParams) => `cambió ${fieldName} de ${oldValue} a ${newValue}`,
+ changeFieldEmpty: ({newValue, fieldName}: ChangeFieldParams) => `cambió ${fieldName} a ${newValue}`,
+ changePolicy: ({fromPolicy, toPolicy}: ChangePolicyParams) => `cambió policy de ${fromPolicy} a ${toPolicy}`,
+ changeType: ({oldType, newType}: ChangeTypeParams) => `cambió type de ${oldType} a ${newType}`,
+ delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `envié este informe a ${delegateUser} ya que ${originalManager} está de vacaciones`,
+ exportedToCSV: `exportó este informe a CSV`,
+ exportedToIntegration: {
+ automatic: ({label}: ExportedToIntegrationParams) => `exportó este informe a ${label}.`,
+ manual: ({label}: ExportedToIntegrationParams) => `marcó este informe como exportado manualmente a ${label}.`,
+ reimburseableLink: 'Ver los gastos por cuenta propia.',
+ nonReimbursableLink: 'Ver los gastos de la tarjeta de empresa.',
+ pending: ({label}: ExportedToIntegrationParams) => `comenzó a exportar este informe a ${label}...`,
+ },
+ forwarded: ({amount, currency}: ForwardedParams) => `aprobado ${currency}${amount}`,
+ integrationsMessage: (errorMessage: string, label: string) => `no se pudo exportar este informe a ${label} ("${errorMessage}").`,
+ managerAttachReceipt: `agregó un recibo`,
+ managerDetachReceipt: `quitó un recibo`,
+ markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `pagó ${currency}${amount} en otro lugar`,
+ markedReimbursedFromIntegration: ({amount, currency}: MarkReimbursedFromIntegrationParams) => `pagó ${currency}${amount} mediante integración`,
+ outdatedBankAccount: `no se pudo procesar el pago debido a un problema con la cuenta bancaria del pagador`,
+ reimbursementACHBounce: `no se pudo procesar el pago porque el pagador no tiene fondos suficientes`,
+ reimbursementACHCancelled: `canceled the payment`,
+ reimbursementAccountChanged: `no se pudo procesar el pago porque el pagador cambió de cuenta bancaria`,
+ reimbursementDelayed: `procesó el pago pero se retrasó entre 1 y 2 días hábiles más`,
+ selectedForRandomAudit: `[seleccionado al azar](https://help.expensify.com/articles/expensify-classic/reports/Set-a-random-report-audit-schedule) para revisión`,
+ share: ({to}: ShareParams) => `usuario invitado ${to}`,
+ unshare: ({to}: UnshareParams) => `usuario eliminado ${to}`,
+ stripePaid: ({amount, currency}: StripePaidParams) => `pagado ${currency}${amount}`,
+ takeControl: `tomó el control`,
+ unapproved: ({amount, currency}: UnapprovedParams) => `no aprobado ${currency}${amount}`,
+ },
+ },
},
chronos: {
oooEventSummaryFullDay: ({summary, dayCount, date}: OOOEventSummaryFullDayParams) => `${summary} por ${dayCount} ${dayCount === 1 ? 'día' : 'días'} hasta el ${date}`,
@@ -3748,7 +4441,19 @@ export default {
missingCategory: 'Falta categoría',
missingComment: 'Descripción obligatoria para la categoría seleccionada',
missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName ?? 'etiqueta'}`,
- modifiedAmount: 'Importe superior al del recibo escaneado',
+ modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => {
+ switch (type) {
+ case 'distance':
+ return 'Importe difiere del calculado basado en distancia';
+ case 'card':
+ return 'Importe mayor al de la transacción de la tarjeta';
+ default:
+ if (displayPercentVariance) {
+ return `Importe ${displayPercentVariance}% mayor al del recibo escaneado`;
+ }
+ return 'Importe mayor al del recibo escaneado';
+ }
+ },
modifiedDate: 'Fecha difiere del recibo escaneado',
nonExpensiworksExpense: 'Gasto no proviene de Expensiworks',
overAutoApprovalLimit: ({formattedLimit}: ViolationsOverAutoApprovalLimitParams) =>
@@ -3758,8 +4463,19 @@ export default {
overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Importe supera el límite${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`,
perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Importe supera el límite diario de la categoría${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`,
receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma tu exactitud',
- receiptRequired: (params: ViolationsReceiptRequiredParams) =>
- `Recibo obligatorio${params ? ` para importes sobre${params.formattedLimit ? ` ${params.formattedLimit}` : ''}${params.category ? ' el límite de la categoría' : ''}` : ''}`,
+ receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams) => {
+ let message = 'Recibo obligatorio';
+ if (formattedLimit ?? category) {
+ message += ' para importes sobre';
+ if (formattedLimit) {
+ message += ` ${formattedLimit}`;
+ }
+ if (category) {
+ message += ' el límite de la categoría';
+ }
+ }
+ return message;
+ },
reviewRequired: 'Revisión requerida',
rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => {
if (brokenBankConnection) {
@@ -3781,7 +4497,17 @@ export default {
taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'El impuesto'} ya no es válido`,
taxRateChanged: 'La tasa de impuesto fue modificada',
taxRequired: 'Falta la tasa de impuesto',
+ none: 'Ninguno',
+ taxCodeToKeep: 'Elige qué tasa de impuesto quieres conservar',
+ tagToKeep: 'Elige qué etiqueta quieres conservar',
+ isTransactionReimbursable: 'Elige si la transacción es reembolsable',
+ merchantToKeep: 'Elige qué comerciante quieres conservar',
+ descriptionToKeep: 'Elige qué descripción quieres conservar',
+ categoryToKeep: 'Elige qué categoría quieres conservar',
+ isTransactionBillable: 'Elige si la transacción es facturable',
keepThisOne: 'Mantener éste',
+ confirmDetails: 'Confirma los detalles que conservas',
+ confirmDuplicatesInfo: 'Los duplicados que no conserves se guardarán para que el usuario los elimine',
hold: 'Bloqueado',
},
violationDismissal: {
@@ -3826,7 +4552,7 @@ export default {
'Parece que estás desconectado. Desafortunadamente, Expensify Classic no funciona sin conexión, pero New Expensify sí. Si prefieres utilizar Expensify Classic, inténtalo de nuevo cuando tengas conexión a internet.',
},
listBoundary: {
- errorMessage: 'Se produjo un error al cargar más mensajes.',
+ errorMessage: 'Se ha producido un error al cargar más mensajes.',
tryAgain: 'Inténtalo de nuevo',
},
systemMessage: {
@@ -3834,16 +4560,78 @@ export default {
},
subscription: {
mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.',
+ badge: {
+ freeTrial: ({numOfDays}) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`,
+ },
billingBanner: {
+ policyOwnerAmountOwed: {
+ title: 'Tu información de pago está desactualizada',
+ subtitle: ({date}) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`,
+ },
+ policyOwnerAmountOwedOverdue: {
+ title: 'Tu información de pago está desactualizada',
+ subtitle: 'Por favor, actualiza tu información de pago.',
+ },
+ policyOwnerUnderInvoicing: {
+ title: 'Tu información de pago está desactualizada',
+ subtitle: ({date}) => `Tu pago está vencido. Por favor, paga tu factura antes del ${date} para evitar la interrupción del servicio.`,
+ },
+ policyOwnerUnderInvoicingOverdue: {
+ title: 'Tu información de pago está desactualizada',
+ subtitle: 'Tu pago está vencido. Por favor, paga tu factura.',
+ },
+ billingDisputePending: {
+ title: 'No se ha podido realizar el cobro a tu tarjeta',
+ subtitle: ({amountOwed, cardEnding}) =>
+ `Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`,
+ },
+ cardAuthenticationRequired: {
+ title: 'No se ha podido realizar el cobro a tu tarjeta',
+ subtitle: ({cardEnding}) =>
+ `Tu tarjeta de pago no ha sido autenticada completamente. Por favor, completa el proceso de autenticación para activar tu tarjeta de pago que termina en ${cardEnding}.`,
+ },
+ insufficientFunds: {
+ title: 'No se ha podido realizar el cobro a tu tarjeta',
+ subtitle: ({amountOwed}) =>
+ `Tu tarjeta de pago fue rechazada por falta de fondos. Vuelve a intentarlo o añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`,
+ },
+ cardExpired: {
+ title: 'No se ha podido realizar el cobro a tu tarjeta',
+ subtitle: ({amountOwed}) => `Tu tarjeta de pago ha expirado. Por favor, añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`,
+ },
+ cardExpireSoon: {
+ title: 'Tu tarjeta caducará pronto',
+ subtitle:
+ 'Tu tarjeta de pago caducará a finales de este mes. Haz clic en el menú de tres puntos que aparece a continuación para actualizarla y continuar utilizando todas tus herramientas favoritas.',
+ },
+ retryBillingSuccess: {
+ title: 'Éxito!',
+ subtitle: 'Tu tarjeta fue facturada correctamente.',
+ },
+ retryBillingError: {
+ title: 'No se ha podido realizar el cobro a tu tarjeta',
+ subtitle:
+ 'Antes de volver a intentarlo, llama directamente a tu banco para que autorice los cargos de Expensify y elimine las retenciones. De lo contrario, añade una tarjeta de pago diferente.',
+ },
+ cardOnDispute: ({amountOwed, cardEnding}) =>
+ `Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`,
preTrial: {
title: 'Iniciar una prueba gratuita',
- subtitle: 'Para empezar, ',
- subtitleLink: 'completa la lista de configuración aquí',
+ subtitle: '¡Ya casi estamos! Completa ',
+ subtitleLink: 'la lista de configuración.',
+ },
+ trialStarted: {
+ title: ({numOfDays}) => `Prueba gratuita: ¡${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}!`,
+ subtitle: 'Añade una tarjeta de pago para seguir utilizando tus funciones favoritas.',
+ },
+ trialEnded: {
+ title: 'Tu prueba gratuita ha terminado',
+ subtitle: 'Añade una tarjeta de pago para seguir utilizando tus funciones favoritas.',
},
},
cardSection: {
title: 'Pago',
- subtitle: 'Añade una tarjeta de pago para abonar tu suscripción a Expensify',
+ subtitle: 'Añade una tarjeta para pagar tu suscripción a Expensify.',
addCardButton: 'Añade tarjeta de pago',
cardNextPayment: ({nextPaymentDate}) => `Tu próxima fecha de pago es ${nextPaymentDate}.`,
cardEnding: ({cardNumber}) => `Tarjeta terminada en ${cardNumber}`,
@@ -3852,6 +4640,13 @@ export default {
changeCurrency: 'Cambiar moneda de pago',
cardNotFound: 'No se ha añadido ninguna tarjeta de pago',
retryPaymentButton: 'Reintentar el pago',
+ requestRefund: 'Solicitar reembolso',
+ requestRefundModal: {
+ phrase1: 'Obtener un reembolso es fácil, simplemente baja tu cuenta de categoría antes de la próxima fecha de facturación y recibirás un reembolso.',
+ phrase2:
+ 'Atención: Bajar tu cuenta de categoría significa que tu(s) espacio(s) de trabajo será(n) eliminado(s). Esta acción no se puede deshacer, pero siempre puedes crear un nuevo espacio de trabajo si cambias de opinión.',
+ confirm: 'Eliminar y degradar',
+ },
viewPaymentHistory: 'Ver historial de pagos',
},
yourPlan: {
@@ -3912,7 +4707,7 @@ export default {
},
paymentCard: {
addPaymentCard: 'Añade tarjeta de pago',
- enterPaymentCardDetails: 'Introduce los datos de tu tarjeta de pago.',
+ enterPaymentCardDetails: 'Introduce los datos de tu tarjeta de pago',
security: 'Expensify es PCI-DSS obediente, utiliza cifrado a nivel bancario, y emplea infraestructura redundante para proteger tus datos.',
learnMoreAboutSecurity: 'Conozca más sobre nuestra seguridad.',
},
@@ -3920,7 +4715,7 @@ export default {
title: 'Configuración de suscripción',
autoRenew: 'Auto-renovación',
autoIncrease: 'Auto-incremento',
- saveUpTo: ({amountSaved}) => `Ahorre hasta $${amountSaved} al mes por miembro activo`,
+ saveUpTo: ({amountWithCurrency}) => `Ahorre hasta ${amountWithCurrency} al mes por miembro activo`,
automaticallyIncrease:
'Aumenta automáticamente tus plazas anuales para dar lugar a los miembros activos que superen el tamaño de tu suscripción. Nota: Esto ampliará la fecha de finalización de tu suscripción anual.',
disableAutoRenew: 'Desactivar auto-renovación',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index c38fb4aadae5..24117f257d8f 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -1,5 +1,6 @@
import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx';
import type {Unit} from '@src/types/onyx/Policy';
+import type {ViolationDataType} from '@src/types/onyx/TransactionViolation';
import type en from './en';
type AddressLineParams = {
@@ -222,6 +223,8 @@ type ViolationsMaxAgeParams = {maxAge: number};
type ViolationsMissingTagParams = {tagName?: string};
+type ViolationsModifiedAmountParams = {type?: ViolationDataType; displayPercentVariance?: number};
+
type ViolationsOverAutoApprovalLimitParams = {formattedLimit?: string};
type ViolationsOverCategoryLimitParams = {formattedLimit?: string};
@@ -298,11 +301,53 @@ type DistanceRateOperationsParams = {count: number};
type ReimbursementRateParams = {unit: Unit};
+type ConfirmHoldExpenseParams = {transactionCount: number};
+
+type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string};
+
+type ChangePolicyParams = {fromPolicy: string; toPolicy: string};
+
+type ChangeTypeParams = {oldType: string; newType: string};
+
+type DelegateSubmitParams = {delegateUser: string; originalManager: string};
+
+type ExportedToIntegrationParams = {label: string};
+
+type ForwardedParams = {amount: string; currency: string};
+
+type IntegrationsMessageParams = {
+ label: string;
+ result: {
+ code?: number;
+ messages?: string[];
+ title?: string;
+ link?: {
+ url: string;
+ text: string;
+ };
+ };
+};
+
+type MarkedReimbursedParams = {amount: string; currency: string};
+
+type MarkReimbursedFromIntegrationParams = {amount: string; currency: string};
+
+type ShareParams = {to: string};
+
+type UnshareParams = {to: string};
+
+type StripePaidParams = {amount: string; currency: string};
+
+type UnapprovedParams = {amount: string; currency: string};
type RemoveMembersWarningPrompt = {
memberName: string;
ownerName: string;
};
+type DeleteExpenseTranslationParams = {
+ count: number;
+};
+
export type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -314,6 +359,7 @@ export type {
BeginningOfChatHistoryDomainRoomPartOneParams,
CanceledRequestParams,
CharacterLimitParams,
+ ConfirmHoldExpenseParams,
ConfirmThatParams,
DateShouldBeAfterParams,
DateShouldBeBeforeParams,
@@ -392,6 +438,7 @@ export type {
ViolationsInvoiceMarkupParams,
ViolationsMaxAgeParams,
ViolationsMissingTagParams,
+ ViolationsModifiedAmountParams,
ViolationsOverAutoApprovalLimitParams,
ViolationsOverCategoryLimitParams,
ViolationsOverLimitParams,
@@ -407,5 +454,19 @@ export type {
WelcomeNoteParams,
WelcomeToRoomParams,
ZipCodeExampleFormatParams,
+ ChangeFieldParams,
+ ChangePolicyParams,
+ ChangeTypeParams,
+ ExportedToIntegrationParams,
+ DelegateSubmitParams,
+ ForwardedParams,
+ IntegrationsMessageParams,
+ MarkedReimbursedParams,
+ MarkReimbursedFromIntegrationParams,
+ ShareParams,
+ UnshareParams,
+ StripePaidParams,
+ UnapprovedParams,
RemoveMembersWarningPrompt,
+ DeleteExpenseTranslationParams,
};
diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index ef9ba57767af..65fd2b6ad015 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -1,5 +1,6 @@
import type {OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
+import type {SetRequired} from 'type-fest';
import Log from '@libs/Log';
import * as Middleware from '@libs/Middleware';
import * as SequentialQueue from '@libs/Network/SequentialQueue';
@@ -7,11 +8,10 @@ import * as Pusher from '@libs/Pusher/pusher';
import * as Request from '@libs/Request';
import * as PersistedRequests from '@userActions/PersistedRequests';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
import type OnyxRequest from '@src/types/onyx/Request';
+import type {PaginatedRequest, PaginationConfig} from '@src/types/onyx/Request';
import type Response from '@src/types/onyx/Response';
-import pkg from '../../../package.json';
-import type {ApiRequest, ApiRequestCommandParameters, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types';
+import type {ApiCommand, ApiRequestCommandParameters, ApiRequestType, CommandOfType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types';
// Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next).
// Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next.
@@ -29,6 +29,8 @@ Request.use(Middleware.Reauthentication);
// If an optimistic ID is not used by the server, this will update the remaining serialized requests using that optimistic ID to use the correct ID instead.
Request.use(Middleware.HandleUnusedOptimisticID);
+Request.use(Middleware.Pagination);
+
// SaveResponseInOnyx - Merges either the successData or failureData (or finallyData, if included in place of the former two values) into Onyx depending on if the call was successful or not. This needs to be the LAST middleware we use, don't add any
// middlewares after this, because the SequentialQueue depends on the result of this middleware to pause the queue (if needed) to bring the app to an up-to-date state.
Request.use(Middleware.SaveResponseInOnyx);
@@ -40,70 +42,84 @@ type OnyxData = {
finallyData?: OnyxUpdate[];
};
-// For all write requests, we'll send the lastUpdateID that is applied to this client. This will
-// allow us to calculate previousUpdateID faster.
-let lastUpdateIDAppliedToClient = -1;
-Onyx.connect({
- key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
- callback: (value) => {
- if (value) {
- lastUpdateIDAppliedToClient = value;
- } else {
- lastUpdateIDAppliedToClient = -1;
- }
- },
-});
-
/**
- * All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData (or finallyData, if included in place of the former two values).
- * This is so that if the network is unavailable or the app is closed, we can send the WRITE request later.
- *
- * @param command - Name of API command to call.
- * @param apiCommandParameters - Parameters to send to the API.
- * @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
- * into Onyx before and after a request is made. Each nested object will be formatted in
- * the same way as an API response.
- * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
- * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
- * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
- * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
+ * Prepare the request to be sent. Bind data together with request metadata and apply optimistic Onyx data.
*/
-function write(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) {
- Log.info('Called API write', false, {command, ...apiCommandParameters});
- const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData;
+function prepareRequest(command: TCommand, type: ApiRequestType, params: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): OnyxRequest {
+ Log.info('[API] Preparing request', false, {command, type});
- // Optimistically update Onyx
+ const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData;
if (optimisticData) {
+ Log.info('[API] Applying optimistic data', false, {command, type});
Onyx.update(optimisticData);
}
- // Assemble the data we'll send to the API
+ const isWriteRequest = type === CONST.API_REQUEST_TYPE.WRITE;
+
+ // Prepare the data we'll send to the API
const data = {
- ...apiCommandParameters,
- appversion: pkg.version,
- apiRequestType: CONST.API_REQUEST_TYPE.WRITE,
+ ...params,
+ apiRequestType: type,
// We send the pusherSocketID with all write requests so that the api can include it in push events to prevent Pusher from sending the events to the requesting client. The push event
// is sent back to the requesting client in the response data instead, which prevents a replay effect in the UI. See https://github.com/Expensify/App/issues/12775.
- pusherSocketID: Pusher.getPusherSocketID(),
+ pusherSocketID: isWriteRequest ? Pusher.getPusherSocketID() : undefined,
};
- // Assemble all the request data we'll be storing in the queue
- const request: OnyxRequest = {
+ // Assemble all request metadata (used by middlewares, and for persisted requests stored in Onyx)
+ const request: SetRequired = {
command,
- data: {
- ...data,
-
- // This should be removed once we are no longer using deprecatedAPI https://github.com/Expensify/Expensify/issues/215650
- shouldRetry: true,
- canCancel: true,
- clientUpdateID: lastUpdateIDAppliedToClient,
- },
+ data,
...onyxDataWithoutOptimisticData,
};
+ if (isWriteRequest) {
+ // This should be removed once we are no longer using deprecatedAPI https://github.com/Expensify/Expensify/issues/215650
+ request.data.shouldRetry = true;
+ request.data.canCancel = true;
+ }
+
+ return request;
+}
+
+/**
+ * Process a prepared request according to its type.
+ */
+function processRequest(request: OnyxRequest, type: ApiRequestType): Promise {
// Write commands can be saved and retried, so push it to the SequentialQueue
- SequentialQueue.push(request);
+ if (type === CONST.API_REQUEST_TYPE.WRITE) {
+ SequentialQueue.push(request);
+ return Promise.resolve();
+ }
+
+ // Read requests are processed right away, but don't return the response to the caller
+ if (type === CONST.API_REQUEST_TYPE.READ) {
+ Request.processWithMiddleware(request);
+ return Promise.resolve();
+ }
+
+ // Requests with side effects process right away, and return the response to the caller
+ return Request.processWithMiddleware(request);
+}
+
+/**
+ * All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData (or finallyData, if included in place of the former two values).
+ * This is so that if the network is unavailable or the app is closed, we can send the WRITE request later.
+ *
+ * @param command - Name of API command to call.
+ * @param apiCommandParameters - Parameters to send to the API.
+ * @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
+ * into Onyx before and after a request is made. Each nested object will be formatted in
+ * the same way as an API response.
+ * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
+ * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
+ * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
+ * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
+ */
+function write(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void {
+ Log.info('[API] Called API write', false, {command, ...apiCommandParameters});
+ const request = prepareRequest(command, CONST.API_REQUEST_TYPE.WRITE, apiCommandParameters, onyxData);
+ processRequest(request, CONST.API_REQUEST_TYPE.WRITE);
}
/**
@@ -123,41 +139,30 @@ function write(command: TCommand, apiCommandParam
* @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
* @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
- * @param [apiRequestType] - Can be either 'read', 'write', or 'makeRequestWithSideEffects'. We use this to either return the chained
- * response back to the caller or to trigger reconnection callbacks when re-authentication is required.
* @returns
*/
-function makeRequestWithSideEffects(
+function makeRequestWithSideEffects(
command: TCommand,
apiCommandParameters: ApiRequestCommandParameters[TCommand],
onyxData: OnyxData = {},
- apiRequestType: ApiRequest = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS,
): Promise {
- Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters});
- const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData;
-
- // Optimistically update Onyx
- if (optimisticData) {
- Onyx.update(optimisticData);
- }
-
- // Assemble the data we'll send to the API
- const data = {
- ...apiCommandParameters,
- appversion: pkg.version,
- apiRequestType,
- clientUpdateID: lastUpdateIDAppliedToClient,
- };
-
- // Assemble all the request data we'll be storing
- const request: OnyxRequest = {
- command,
- data,
- ...onyxDataWithoutOptimisticData,
- };
+ Log.info('[API] Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters});
+ const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData);
// Return a promise containing the response from HTTPS
- return Request.processWithMiddleware(request);
+ return processRequest(request, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS);
+}
+
+/**
+ * Ensure all write requests on the sequential queue have finished responding before running read requests.
+ * Responses from read requests can overwrite the optimistic data inserted by
+ * write requests that use the same Onyx keys and haven't responded yet.
+ */
+function waitForWrites(command: TCommand) {
+ if (PersistedRequests.getLength() > 0) {
+ Log.info(`[API] '${command}' is waiting on ${PersistedRequests.getLength()} write commands`);
+ }
+ return SequentialQueue.waitForIdle();
}
/**
@@ -173,14 +178,57 @@ function makeRequestWithSideEffects(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) {
- // Ensure all write requests on the sequential queue have finished responding before running read requests.
- // Responses from read requests can overwrite the optimistic data inserted by
- // write requests that use the same Onyx keys and haven't responded yet.
- if (PersistedRequests.getLength() > 0) {
- Log.info(`[API] '${command}' is waiting on ${PersistedRequests.getLength()} write commands`);
+function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void {
+ Log.info('[API] Called API.read', false, {command, ...apiCommandParameters});
+
+ waitForWrites(command).then(() => {
+ const request = prepareRequest(command, CONST.API_REQUEST_TYPE.READ, apiCommandParameters, onyxData);
+ processRequest(request, CONST.API_REQUEST_TYPE.READ);
+ });
+}
+
+function paginate>(
+ type: TRequestType,
+ command: TCommand,
+ apiCommandParameters: ApiRequestCommandParameters[TCommand],
+ onyxData: OnyxData,
+ config: PaginationConfig,
+): Promise;
+function paginate>(
+ type: TRequestType,
+ command: TCommand,
+ apiCommandParameters: ApiRequestCommandParameters[TCommand],
+ onyxData: OnyxData,
+ config: PaginationConfig,
+): void;
+function paginate>(
+ type: TRequestType,
+ command: TCommand,
+ apiCommandParameters: ApiRequestCommandParameters[TCommand],
+ onyxData: OnyxData,
+ config: PaginationConfig,
+): Promise | void {
+ Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters});
+ const request: PaginatedRequest = {
+ ...prepareRequest(command, type, apiCommandParameters, onyxData),
+ ...config,
+ ...{
+ isPaginated: true,
+ },
+ };
+
+ switch (type) {
+ case CONST.API_REQUEST_TYPE.WRITE:
+ processRequest(request, type);
+ return;
+ case CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS:
+ return processRequest(request, type);
+ case CONST.API_REQUEST_TYPE.READ:
+ waitForWrites(command as ReadCommand).then(() => processRequest(request, type));
+ return;
+ default:
+ throw new Error('Unknown API request type');
}
- SequentialQueue.waitForIdle().then(() => makeRequestWithSideEffects(command, apiCommandParameters, onyxData, CONST.API_REQUEST_TYPE.READ));
}
-export {write, makeRequestWithSideEffects, read};
+export {write, makeRequestWithSideEffects, read, paginate};
diff --git a/src/libs/API/parameters/AddPaymentCardParams.ts b/src/libs/API/parameters/AddPaymentCardParams.ts
index 1c9b1fc4fa30..3a59c678ac4f 100644
--- a/src/libs/API/parameters/AddPaymentCardParams.ts
+++ b/src/libs/API/parameters/AddPaymentCardParams.ts
@@ -8,7 +8,7 @@ type AddPaymentCardParams = {
cardCVV: string;
addressName: string;
addressZip: string;
- currency: ValueOf;
+ currency: ValueOf;
isP2PDebitCard: boolean;
};
export default AddPaymentCardParams;
diff --git a/src/libs/API/parameters/BeginSignInParams.ts b/src/libs/API/parameters/BeginSignInParams.ts
index 0dec2587ed14..2f85a3335c62 100644
--- a/src/libs/API/parameters/BeginSignInParams.ts
+++ b/src/libs/API/parameters/BeginSignInParams.ts
@@ -1,6 +1,5 @@
type BeginSignInParams = {
email: string;
- useNewBeginSignIn: boolean;
};
export default BeginSignInParams;
diff --git a/src/libs/API/parameters/CompleteSplitBillParams.ts b/src/libs/API/parameters/CompleteSplitBillParams.ts
index a1731d32fcc4..67ca011b70d9 100644
--- a/src/libs/API/parameters/CompleteSplitBillParams.ts
+++ b/src/libs/API/parameters/CompleteSplitBillParams.ts
@@ -10,6 +10,7 @@ type CompleteSplitBillParams = {
splits: string;
taxCode?: string;
taxAmount?: number;
+ billable?: boolean;
};
export default CompleteSplitBillParams;
diff --git a/src/libs/API/parameters/ConnectPolicyToNetSuiteParams.ts b/src/libs/API/parameters/ConnectPolicyToNetSuiteParams.ts
new file mode 100644
index 000000000000..2143ca1b039c
--- /dev/null
+++ b/src/libs/API/parameters/ConnectPolicyToNetSuiteParams.ts
@@ -0,0 +1,8 @@
+type ConnectPolicyToNetSuiteParams = {
+ policyID: string;
+ netSuiteAccountID: string;
+ netSuiteTokenID: string;
+ netSuiteTokenSecret: string;
+};
+
+export default ConnectPolicyToNetSuiteParams;
diff --git a/src/libs/API/parameters/CopyExistingPolicyConnectionParams.ts b/src/libs/API/parameters/CopyExistingPolicyConnectionParams.ts
new file mode 100644
index 000000000000..78384e277f16
--- /dev/null
+++ b/src/libs/API/parameters/CopyExistingPolicyConnectionParams.ts
@@ -0,0 +1,9 @@
+import type {ConnectionName} from '@src/types/onyx/Policy';
+
+type CopyExistingPolicyConnectionParams = {
+ policyID: string;
+ targetPolicyID: string;
+ connectionName: ConnectionName;
+};
+
+export default CopyExistingPolicyConnectionParams;
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