diff --git a/.eslintignore b/.eslintignore
index aa8b769dfede..3d966d096add 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -10,3 +10,4 @@ docs/vendor/**
docs/assets/**
web/gtm.js
**/.expo/**
+src/libs/SearchParser/searchParser.js
diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml
index 47e13f6313a0..8995c7681d40 100644
--- a/.github/actions/composite/buildAndroidE2EAPK/action.yml
+++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml
@@ -74,7 +74,7 @@ runs:
shell: bash
- name: Upload APK
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3
+ uses: actions/upload-artifact@v4
with:
name: ${{ inputs.ARTIFACT_NAME }}
path: ${{ inputs.APP_OUTPUT_PATH }}
diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml
index ffce73644263..add4879d8de1 100644
--- a/.github/workflows/e2ePerformanceTests.yml
+++ b/.github/workflows/e2ePerformanceTests.yml
@@ -156,7 +156,7 @@ jobs:
run: mkdir zip
- name: Download baseline APK
- uses: actions/download-artifact@348754975ef0295bfa2c111cba996120cfdf8a5d
+ uses: actions/download-artifact@v4
id: downloadBaselineAPK
with:
name: baseline-apk-${{ needs.buildBaseline.outputs.VERSION }}
@@ -170,7 +170,7 @@ jobs:
run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease.apk"
- name: Download delta APK
- uses: actions/download-artifact@348754975ef0295bfa2c111cba996120cfdf8a5d
+ uses: actions/download-artifact@v4
id: downloadDeltaAPK
with:
name: delta-apk-${{ needs.buildDelta.outputs.DELTA_REF }}
@@ -184,7 +184,7 @@ jobs:
- name: Copy e2e code into zip folder
run: cp tests/e2e/dist/index.js zip/testRunner.ts
-
+
- name: Copy profiler binaries into zip folder
run: cp -r node_modules/@perf-profiler/android/cpp-profiler/bin zip/bin
@@ -257,12 +257,12 @@ jobs:
- name: Check if test failed, if so post the results and add the DeployBlocker label
id: checkIfRegressionDetected
run: |
- if grep -q '🔴' ./output.md; then
+ if grep -q '🔴' "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md"; then
# Create an output to the GH action that the test failed:
echo "performanceRegressionDetected=true" >> "$GITHUB_OUTPUT"
gh pr edit ${{ inputs.PR_NUMBER }} --add-label DeployBlockerCash
- gh pr comment ${{ inputs.PR_NUMBER }} -F ./output.md
+ gh pr comment ${{ inputs.PR_NUMBER }} -F "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md"
gh pr comment ${{ inputs.PR_NUMBER }} -b "@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker."
else
echo "performanceRegressionDetected=false" >> "$GITHUB_OUTPUT"
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 640d1eaa1172..e1534bfba1c7 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -94,14 +94,14 @@ jobs:
VERSION: ${{ env.VERSION_CODE }}
- name: Archive Android sourcemaps
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: android-sourcemap-${{ github.ref_name }}
path: android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map
- name: Upload Android version to GitHub artifacts
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: app-production-release.aab
path: android/app/build/outputs/bundle/productionRelease/app-production-release.aab
@@ -246,14 +246,14 @@ jobs:
APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }}
- name: Archive iOS sourcemaps
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: ios-sourcemap-${{ github.ref_name }}
path: main.jsbundle.map
- name: Upload iOS version to GitHub artifacts
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: New Expensify.ipa
path: /Users/runner/work/App/App/New Expensify.ipa
diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml
index 53b3d3374a9e..30a30918f4f6 100644
--- a/.github/workflows/sendReassurePerfData.yml
+++ b/.github/workflows/sendReassurePerfData.yml
@@ -25,7 +25,7 @@ jobs:
shell: bash
run: |
set -e
- npx reassure --baseline
+ NODE_OPTIONS=--experimental-vm-modules npx reassure --baseline
- name: Get merged pull request
id: getMergedPullRequest
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 10912aaeb436..5d70c16c28e0 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -125,7 +125,7 @@ jobs:
MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }}
- name: Upload Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: android
path: ./android_paths.json
@@ -217,7 +217,7 @@ jobs:
S3_REGION: us-east-1
- name: Upload Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: ios
path: ./ios_paths.json
@@ -321,7 +321,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }}
- name: Download Artifact
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }}
- name: Read JSONs with android paths
diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml
index 476b01f87b07..3bfc0ed28d1a 100644
--- a/.github/workflows/typecheck.yml
+++ b/.github/workflows/typecheck.yml
@@ -34,7 +34,7 @@ jobs:
# - git diff is used to see the files that were added on this branch
# - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main
# - wc counts the words in the result of the intersection
- count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l)
+ count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l)
if [ "$count_new_js" -gt "0" ]; then
echo "ERROR: Found new JavaScript files in the project; use TypeScript instead."
exit 1
diff --git a/.prettierignore b/.prettierignore
index 09de20ba30b0..a9f7e1464529 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -19,3 +19,6 @@ package-lock.json
src/libs/E2E/reactNativeLaunchingTest.ts
# Temporary while we keep react-compiler in our repo
lib/**
+
+# Automatically generated files
+src/libs/SearchParser/searchParser.js
diff --git a/android/app/build.gradle b/android/app/build.gradle
index c53ad2cd3cc7..d6de6cf4fae0 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -15,6 +15,7 @@ fullstory {
org 'o-1WN56P-na1'
enabledVariants 'all'
logcatLevel 'debug'
+ recordOnStart false
}
react {
@@ -107,8 +108,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009000512
- versionName "9.0.5-12"
+ versionCode 1009000603
+ versionName "9.0.6-3"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/assets/images/simple-illustrations/simple-illustration__empty-state.svg b/assets/images/simple-illustrations/simple-illustration__empty-state.svg
new file mode 100644
index 000000000000..154b2269c285
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__empty-state.svg
@@ -0,0 +1,102 @@
+
+
+
diff --git a/assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg b/assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg
new file mode 100644
index 000000000000..01669d07c0f0
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg
@@ -0,0 +1,50 @@
+
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__tire.svg b/assets/images/simple-illustrations/simple-illustration__tire.svg
new file mode 100644
index 000000000000..9107c88eb3e2
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__tire.svg
@@ -0,0 +1,37 @@
+
+
+
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/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
index b65c66c986ad..1f412665fc2f 100644
--- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
+++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
@@ -5,7 +5,8 @@ description: Details on requesting the Expensify Card as an employee
To start using the Expensify Card, do the following:
1. **Enable Expensify Cards:** An admin must first enable the cards. Then, an admin can assign you a card by setting a limit, which allows access to the card.
2. **Request the Card:**
- - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!” Use this task to message your admin team.
+ - 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.
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/integrations/travel-integrations/Egencia.md b/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md
index 178621a62d90..35d232d2df67 100644
--- a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md
+++ b/docs/articles/expensify-classic/integrations/travel-integrations/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/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/redirects.csv b/docs/redirects.csv
index 67ca238c1aed..1a60d52c1749 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -203,6 +203,9 @@ 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
diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg
index 32ed6ba30059..c08a5aae1b73 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..61b6eeb84537 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/Info.plist b/ios/NewExpensify/Info.plist
index 2ccce98e6a21..3e0b46a292a7 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 9.0.5
+ 9.0.6CFBundleSignature????CFBundleURLTypes
@@ -40,11 +40,13 @@
CFBundleVersion
- 9.0.5.12
+ 9.0.6.3FullStoryOrgIdo-1WN56P-na1
+ RecordOnStart
+ ITSAppUsesNonExemptEncryption
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 8248e7db0454..d13eca4d1cad 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 9.0.5
+ 9.0.6CFBundleSignature????CFBundleVersion
- 9.0.5.12
+ 9.0.6.3
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 87cdb420af38..5125a598997f 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.0.5
+ 9.0.6CFBundleVersion
- 9.0.5.12
+ 9.0.6.3NSExtensionNSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 29ab90c4b7db..50dfc65d07b2 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1871,7 +1871,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.91):
+ - RNLiveMarkdown (0.1.103):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1889,9 +1889,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNLiveMarkdown/common (= 0.1.91)
+ - RNLiveMarkdown/common (= 0.1.103)
- Yoga
- - RNLiveMarkdown/common (0.1.91):
+ - RNLiveMarkdown/common (0.1.103):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1935,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)
@@ -2614,10 +2614,10 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: 24fbb7370eefee2f325fb64cfe904b111ffcd81b
+ RNLiveMarkdown: f12157fc91b72e19705c9cc8c98034c4c1669d5a
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c
- RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216
+ RNPermissions: d2392b754e67bc14491f5b12588bef2864e783f3
RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37
RNReanimated: 323436b1a5364dca3b5f8b1a13458455e0de9efe
RNScreens: abd354e98519ed267600b7ee64fdcb8e060b1218
diff --git a/package-lock.json b/package-lock.json
index c401dfe77198..45813001d07c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "new.expensify",
- "version": "9.0.5-12",
+ "version": "9.0.6-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.5-12",
+ "version": "9.0.6-3",
"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.91",
+ "@expensify/react-native-live-markdown": "0.1.103",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -55,7 +55,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "2.0.26",
+ "expensify-common": "2.0.39",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -106,7 +106,7 @@
"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",
@@ -233,6 +233,7 @@
"memfs": "^4.6.0",
"onchange": "^7.1.0",
"patch-package": "^8.0.0",
+ "peggy": "^4.0.3",
"portfinder": "^1.0.28",
"prettier": "^2.8.8",
"pusher-js-mock": "^0.3.3",
@@ -3784,9 +3785,9 @@
}
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.91",
- "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.91.tgz",
- "integrity": "sha512-6uQTgwhpvLqQKdtNqSgh45sRuQRXzv/WwyhdvQNge6EYtulyGFqT82GIP+LIGW8Xnl73nzFZTuMKwWxFFR/Cow==",
+ "version": "0.1.103",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.103.tgz",
+ "integrity": "sha512-w9jQoxBE9LghfL8UdYbG+8A+CApmER/XMH8N7/bINn7w57+FnnBa5ckPWx6/UYX7OYsmYxSaHJLQkJEXYlDRZg==",
"workspaces": [
"parser",
"example",
@@ -7879,6 +7880,33 @@
"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",
@@ -25974,9 +26002,9 @@
}
},
"node_modules/expensify-common": {
- "version": "2.0.26",
- "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.26.tgz",
- "integrity": "sha512-3GORs2xfx78SoKLDh4lXpk4Bx61sAVNnlo23VB803zs7qZz8/Oq3neKedtEJuRAmUps0C1Y5y9xZE8nrPO31nQ==",
+ "version": "2.0.39",
+ "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.39.tgz",
+ "integrity": "sha512-HyW7MiS8+ZWO2xye5TSsiKJfIsaGl0M2RlI+txJNF9GWeroA6kaXybTY1Ppq+cS+a7+MU/KMQh7GNnIMrvkf+w==",
"dependencies": {
"awesome-phonenumber": "^5.4.0",
"classnames": "2.5.0",
@@ -35858,6 +35886,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,
@@ -37435,8 +37489,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",
@@ -40226,6 +40281,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",
diff --git a/package.json b/package.json
index 5420a3e886ef..1cd32c974031 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.5-12",
+ "version": "9.0.6-3",
"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.",
@@ -61,13 +61,14 @@
"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/",
- "react-compiler-healthcheck": "react-compiler-healthcheck --verbose"
+ "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.91",
+ "@expensify/react-native-live-markdown": "0.1.103",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -109,7 +110,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "2.0.26",
+ "expensify-common": "2.0.39",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -160,7 +161,7 @@
"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",
@@ -287,6 +288,7 @@
"memfs": "^4.6.0",
"onchange": "^7.1.0",
"patch-package": "^8.0.0",
+ "peggy": "^4.0.3",
"portfinder": "^1.0.28",
"prettier": "^2.8.8",
"pusher-js-mock": "^0.3.3",
diff --git a/patches/@expensify+react-native-live-markdown+0.1.91.patch b/patches/@expensify+react-native-live-markdown+0.1.91.patch
deleted file mode 100644
index c77e46accae3..000000000000
--- a/patches/@expensify+react-native-live-markdown+0.1.91.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts b/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts
-index 1cda659..ba5c3c3 100644
---- a/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts
-+++ b/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts
-@@ -66,7 +66,7 @@ function setCursorPosition(target: HTMLElement, start: number, end: number | nul
- // 3. Caret at the end of whole input, when pressing enter
- // 4. All other placements
- if (prevChar === '\n' && prevTextLength !== undefined && prevTextLength < textCharacters.length) {
-- if (nextChar !== '\n') {
-+ if (nextChar && nextChar !== '\n' && i !== n - 1) {
- range.setStart(textNodes[i + 1] as Node, 0);
- } else if (i !== textNodes.length - 1) {
- range.setStart(textNodes[i] as Node, 1);
diff --git a/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch b/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch
index f8e171008e14..bf6decac0450 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;
}
@@ -2609,32 +2619,6 @@ index 6982220..b515270 100644
+ readonly eventEmitter: NativeEventEmitter;
};
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
---- 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"
- },
- "dependencies": {},
-@@ -121,5 +122,13 @@
- "yarn eslint --fix",
- "git add"
- ]
-+ },
-+ "codegenConfig": {
-+ "name": "RNCNetInfoSpec",
-+ "type": "modules",
-+ "jsSrcsDir": "src/internal",
-+ "android": {
-+ "javaPackageName": "com.reactnativecommunity.netinfo"
-+ }
- }
- }
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
@@ -2971,95 +2955,95 @@ index 878f7ba..0000000
--- a/node_modules/@react-native-community/netinfo/windows/.npmignore
+++ /dev/null
@@ -1,92 +0,0 @@
--*AppPackages*
--*BundleArtifacts*
--
--#OS junk files
--[Tt]humbs.db
--*.DS_Store
--
--#Visual Studio files
--*.[Oo]bj
--*.user
--*.aps
--*.pch
--*.vspscc
--*.vssscc
--*_i.c
--*_p.c
--*.ncb
--*.suo
--*.tlb
--*.tlh
--*.bak
--*.[Cc]ache
--*.ilk
--*.log
--*.lib
--*.sbr
--*.sdf
--*.opensdf
--*.opendb
--*.unsuccessfulbuild
--ipch/
--[Oo]bj/
--[Bb]in
--[Dd]ebug*/
--[Rr]elease*/
--Ankh.NoLoad
--
--# Visual C++ cache files
--ipch/
--*.aps
--*.ncb
--*.opendb
--*.opensdf
--*.sdf
--*.cachefile
--*.VC.db
--*.VC.VC.opendb
--
--#MonoDevelop
--*.pidb
--*.userprefs
--
--#Tooling
--_ReSharper*/
--*.resharper
--[Tt]est[Rr]esult*
--*.sass-cache
--
--#Project files
--[Bb]uild/
--
--#Subversion files
--.svn
--
--# Office Temp Files
--~$*
--
--# vim Temp Files
--*~
--
--#NuGet
--packages/
--*.nupkg
--
--#ncrunch
--*ncrunch*
--*crunch*.local.xml
--
--# visual studio database projects
--*.dbmdl
--
--#Test files
--*.testsettings
--
--#Other files
--*.DotSettings
--.vs/
--*project.lock.json
--
--#Files generated by the VS build
--**/Generated Files/**
--
+-*AppPackages*
+-*BundleArtifacts*
+-
+-#OS junk files
+-[Tt]humbs.db
+-*.DS_Store
+-
+-#Visual Studio files
+-*.[Oo]bj
+-*.user
+-*.aps
+-*.pch
+-*.vspscc
+-*.vssscc
+-*_i.c
+-*_p.c
+-*.ncb
+-*.suo
+-*.tlb
+-*.tlh
+-*.bak
+-*.[Cc]ache
+-*.ilk
+-*.log
+-*.lib
+-*.sbr
+-*.sdf
+-*.opensdf
+-*.opendb
+-*.unsuccessfulbuild
+-ipch/
+-[Oo]bj/
+-[Bb]in
+-[Dd]ebug*/
+-[Rr]elease*/
+-Ankh.NoLoad
+-
+-# Visual C++ cache files
+-ipch/
+-*.aps
+-*.ncb
+-*.opendb
+-*.opensdf
+-*.sdf
+-*.cachefile
+-*.VC.db
+-*.VC.VC.opendb
+-
+-#MonoDevelop
+-*.pidb
+-*.userprefs
+-
+-#Tooling
+-_ReSharper*/
+-*.resharper
+-[Tt]est[Rr]esult*
+-*.sass-cache
+-
+-#Project files
+-[Bb]uild/
+-
+-#Subversion files
+-.svn
+-
+-# Office Temp Files
+-~$*
+-
+-# vim Temp Files
+-*~
+-
+-#NuGet
+-packages/
+-*.nupkg
+-
+-#ncrunch
+-*ncrunch*
+-*crunch*.local.xml
+-
+-# visual studio database projects
+-*.dbmdl
+-
+-#Test files
+-*.testsettings
+-
+-#Other files
+-*.DotSettings
+-.vs/
+-*project.lock.json
+-
+-#Files generated by the VS build
+-**/Generated Files/**
+-
diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh
index a4be88984561..9145629015ee 100755
--- a/scripts/applyPatches.sh
+++ b/scripts/applyPatches.sh
@@ -8,24 +8,17 @@ SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}")
source "$SCRIPTS_DIR/shellUtils.sh"
# Wrapper to run patch-package.
-# We use `script` to preserve colorization when the output of patch-package is piped to tee
-# and we provide /dev/null to discard the output rather than sending it to a file
-# `script` has different syntax on macOS vs linux, so that's why we need a wrapper function
function patchPackage {
OS="$(uname)"
- if [[ "$OS" == "Darwin" ]]; then
- # macOS
- script -q /dev/null npx patch-package --error-on-fail
- elif [[ "$OS" == "Linux" ]]; then
- # Ubuntu/Linux
- script -q -c "npx patch-package --error-on-fail" /dev/null
+ if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then
+ npx patch-package --error-on-fail
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
-# (we use `script -q /dev/null` to preserve colorization in the output)
TEMP_OUTPUT="$(mktemp)"
patchPackage 2>&1 | tee "$TEMP_OUTPUT"
EXIT_CODE=${PIPESTATUS[0]}
@@ -36,7 +29,7 @@ rm -f "$TEMP_OUTPUT"
echo "$OUTPUT" | grep -q "Warning:"
WARNING_FOUND=$?
-printf "\n";
+printf "\n"
# Determine the final exit code
if [ "$EXIT_CODE" -eq 0 ]; then
diff --git a/src/CONST.ts b/src/CONST.ts
index e56b19cee876..2dc0a0622349 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -335,6 +335,8 @@ const CONST = {
VERIFICATION_MAX_ATTEMPTS: 7,
STATE: {
VERIFYING: 'VERIFYING',
+ VALIDATING: 'VALIDATING',
+ SETUP: 'SETUP',
PENDING: 'PENDING',
OPEN: 'OPEN',
},
@@ -361,7 +363,6 @@ const CONST = {
DEFAULT_ROOMS: 'defaultRooms',
VIOLATIONS: 'violations',
DUPE_DETECTION: 'dupeDetection',
- REPORT_FIELDS: 'reportFields',
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission',
SPOTNANA_TRAVEL: 'spotnanaTravel',
@@ -668,6 +669,7 @@ const CONST = {
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',
@@ -840,6 +842,8 @@ const CONST = {
IOU: 'iou',
TASK: 'task',
INVOICE: 'invoice',
+ PAYCHECK: 'paycheck',
+ BILL: 'bill',
},
CHAT_TYPE: chatTypes,
WORKSPACE_CHAT_ROOMS: {
@@ -1123,8 +1127,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
@@ -1245,7 +1247,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,
@@ -2146,6 +2148,7 @@ const CONST = {
ACCESS_VARIANTS: {
PAID: 'paid',
ADMIN: 'admin',
+ CONTROL: 'control',
},
DEFAULT_MAX_EXPENSE_AGE: 90,
DEFAULT_MAX_EXPENSE_AMOUNT: 200000,
@@ -5207,6 +5210,39 @@ const CONST = {
APPROVE: 'approve',
PAY: 'pay',
},
+ 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: {
@@ -5244,6 +5280,7 @@ const CONST = {
},
},
+ MAX_LENGTH_256: 256,
WORKSPACE_CARDS_LIST_LABEL_TYPE: {
CURRENT_BALANCE: 'currentBalance',
REMAINING_LIMIT: 'remainingLimit',
@@ -5251,6 +5288,13 @@ const CONST = {
},
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',
+ },
+
UPGRADE_FEATURE_INTRO_MAPPING: [
{
id: 'reportFields',
@@ -5261,15 +5305,12 @@ const CONST = {
icon: 'Pencil',
},
],
+
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 f96c51961acc..6151f983e8d0 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -19,6 +19,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';
@@ -147,6 +148,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();
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index bd4b294a6d68..b06b05dac7e1 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -320,9 +320,6 @@ 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',
@@ -445,6 +442,9 @@ const ONYXKEYS = {
* 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_',
},
/** List of Form ids */
@@ -535,8 +535,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',
@@ -622,7 +622,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;
@@ -692,6 +692,7 @@ type OnyxCollectionValuesMapping = {
[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;
};
type OnyxValuesMapping = {
@@ -797,7 +798,6 @@ type OnyxValuesMapping = {
[ONYXKEYS.MAX_CANVAS_HEIGHT]: number;
[ONYXKEYS.MAX_CANVAS_WIDTH]: number;
[ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string;
- [ONYXKEYS.ONBOARDING_ERROR_MESSAGE]: string;
[ONYXKEYS.ONBOARDING_POLICY_ID]: string;
[ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string;
[ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index cffa9051e859..4e94bb125c3d 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -676,6 +676,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,
@@ -700,6 +709,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,
@@ -732,6 +749,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,
@@ -797,30 +818,30 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/reportFields/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new` as const,
},
- WORKSPACE_REPORT_FIELD_SETTINGS: {
- route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit',
- getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit` as const,
+ WORKSPACE_REPORT_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_FIELD_LIST_VALUES: {
- route: 'settings/workspaces/:policyID/reportField/listValues/:reportFieldID?',
- getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/listValues/${encodeURIComponent(reportFieldID ?? '')}` 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_FIELD_ADD_VALUE: {
- route: 'settings/workspaces/:policyID/reportField/addValue/:reportFieldID?',
- getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/addValue/${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_FIELD_VALUE_SETTINGS: {
- route: 'settings/workspaces/:policyID/reportField/:valueIndex/:reportFieldID?',
+ WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: {
+ route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?',
getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) =>
- `settings/workspaces/${policyID}/reportField/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const,
+ `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const,
},
- WORKSPACE_REPORT_FIELD_EDIT_VALUE: {
- route: 'settings/workspaces/:policyID/reportField/new/:valueIndex/edit',
- getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportField/new/${valueIndex}/edit` 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_FIELD_INITIAL_VALUE: {
- route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit/initialValue',
- getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const,
+ WORKSPACE_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',
@@ -830,6 +851,10 @@ const ROUTES = {
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,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 50bc9ed6b63b..79dd38a653b7 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -331,6 +331,8 @@ const SCREENS = {
SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account',
SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced',
SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account',
+ CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation',
+ RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings',
},
INITIAL: 'Workspace_Initial',
PROFILE: 'Workspace_Profile',
@@ -341,6 +343,7 @@ const SCREENS = {
RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit',
EXPENSIFY_CARD: 'Workspace_ExpensifyCard',
EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New',
+ EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount',
BILLS: 'Workspace_Bills',
INVOICES: 'Workspace_Invoices',
TRAVEL: 'Workspace_Travel',
@@ -354,7 +357,7 @@ const SCREENS = {
TAG_EDIT: 'Tag_Edit',
TAXES: 'Workspace_Taxes',
REPORT_FIELDS: 'Workspace_ReportFields',
- REPORT_FIELD_SETTINGS: 'Workspace_ReportField_Settings',
+ 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',
@@ -372,6 +375,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',
@@ -384,6 +388,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',
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 (
(
<>
& {
+ 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;
}
/**
@@ -35,7 +43,7 @@ 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();
@@ -44,11 +52,12 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu
width: 0,
left: 0,
bottom: 0,
+ cursorCoordinates: {x: 0, y: 0},
});
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;
@@ -73,51 +82,51 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu
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/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 4b3f0f70db24..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;
@@ -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/Composer/index.tsx b/src/components/Composer/index.tsx
index a41f983434d8..3889c8597843 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -116,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]);
/**
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 && (
{shouldShowSettlementButton && (
{isHoldMenuVisible && iouReport && requestType !== undefined && (
{
+ // 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/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx
index 48d9a2b4ae3a..02da657609ba 100644
--- a/src/components/Search/SearchListWithHeader.tsx
+++ b/src/components/Search/SearchListWithHeader.tsx
@@ -1,7 +1,12 @@
import type {ForwardedRef} from 'react';
-import React, {forwardRef, useEffect, useMemo, useState} from 'react';
+import React, {forwardRef, useCallback, useEffect, useMemo, useState} from 'react';
+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 SearchUtils from '@libs/SearchUtils';
import CONST from '@src/CONST';
import type {SearchDataTypes, SearchQuery} from '@src/types/onyx/SearchResults';
@@ -13,6 +18,8 @@ type SearchListWithHeaderProps = Omit void;
};
function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [string, SelectedTransactionInfo] {
@@ -33,7 +40,14 @@ function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListIt
};
}
-function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchType, ...props}: SearchListWithHeaderProps, ref: ForwardedRef) {
+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 [selectedItems, setSelectedItems] = useState({});
const clearSelectedItems = () => setSelectedItems({});
@@ -42,39 +56,72 @@ function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchT
clearSelectedItems();
}, [hash]);
- const toggleTransaction = (item: TransactionListItemType | ReportListItemType) => {
- if (SearchUtils.isTransactionListItemType(item)) {
- if (!item.keyForList) {
+ const toggleTransaction = useCallback(
+ (item: TransactionListItemType | ReportListItemType) => {
+ if (SearchUtils.isTransactionListItemType(item)) {
+ if (!item.keyForList) {
+ return;
+ }
+
+ setSelectedItems((prev) => {
+ if (prev[item.keyForList]?.isSelected) {
+ const {[item.keyForList]: omittedTransaction, ...transactions} = prev;
+ return transactions;
+ }
+ return {...prev, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, action: item.action}};
+ });
+
return;
}
- setSelectedItems((prev) => {
- if (prev[item.keyForList]?.isSelected) {
- const {[item.keyForList]: omittedTransaction, ...transactions} = prev;
- return transactions;
- }
- return {...prev, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, action: item.action}};
+ if (item.transactions.every((transaction) => selectedItems[transaction.keyForList]?.isSelected)) {
+ const reducedSelectedItems: SelectedTransactions = {...selectedItems};
+
+ item.transactions.forEach((transaction) => {
+ delete reducedSelectedItems[transaction.keyForList];
+ });
+
+ setSelectedItems(reducedSelectedItems);
+ return;
+ }
+
+ setSelectedItems({
+ ...selectedItems,
+ ...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)),
});
+ },
+ [selectedItems],
+ );
+ const openBottomModal = (item: TransactionListItemType | ReportListItemType | null) => {
+ if (!isSmallScreenWidth) {
return;
}
- if (item.transactions.every((transaction) => selectedItems[transaction.keyForList]?.isSelected)) {
- const reducedSelectedItems: SelectedTransactions = {...selectedItems};
+ setLongPressedItem(item);
+ setIsModalVisible(true);
+ };
- item.transactions.forEach((transaction) => {
- delete reducedSelectedItems[transaction.keyForList];
- });
+ const turnOnSelectionMode = useCallback(() => {
+ setIsMobileSelectionModeActive?.(true);
+ setIsModalVisible(false);
+
+ if (longPressedItem) {
+ toggleTransaction(longPressedItem);
+ }
+ }, [longPressedItem, setIsMobileSelectionModeActive, toggleTransaction]);
- setSelectedItems(reducedSelectedItems);
+ const closeBottomModal = useCallback(() => {
+ setIsModalVisible(false);
+ }, []);
+
+ useEffect(() => {
+ if (isMobileSelectionModeActive) {
return;
}
- setSelectedItems({
- ...selectedItems,
- ...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)),
- });
- };
+ setSelectedItems({});
+ }, [setSelectedItems, isMobileSelectionModeActive]);
const toggleAllTransactions = () => {
const areItemsOfReportType = searchType === CONST.SEARCH.DATA_TYPES.REPORT;
@@ -104,6 +151,8 @@ function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchT
clearSelectedItems={clearSelectedItems}
query={query}
hash={hash}
+ isMobileSelectionModeActive={isMobileSelectionModeActive}
+ setIsMobileSelectionModeActive={setIsMobileSelectionModeActive}
/>
// eslint-disable-next-line react/jsx-props-no-spreading
@@ -111,10 +160,24 @@ function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchT
sections={[{data: sortedSelectedData, isDisabled: false}]}
ListItem={ListItem}
onSelectRow={onSelectRow}
+ onLongPressRow={openBottomModal}
ref={ref}
onCheckboxPress={toggleTransaction}
onSelectAll={toggleAllTransactions}
+ isMobileSelectionModeActive={isMobileSelectionModeActive}
/>
+
+
+
+
>
);
}
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
index 8d42f9e6da36..b0f2acfb57d1 100644
--- a/src/components/Search/SearchPageHeader.tsx
+++ b/src/components/Search/SearchPageHeader.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback} from 'react';
+import React, {useMemo} from 'react';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -10,6 +10,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as SearchActions from '@libs/actions/Search';
+import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {SearchQuery} from '@src/types/onyx/SearchResults';
@@ -22,11 +23,13 @@ type SearchHeaderProps = {
selectedItems?: SelectedTransactions;
clearSelectedItems?: () => void;
hash: number;
+ isMobileSelectionModeActive?: boolean;
+ setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void;
};
type SearchHeaderOptionValue = DeepValueOf | undefined;
-function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}: SearchHeaderProps) {
+function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchHeaderProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
@@ -39,12 +42,13 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}:
finished: {icon: Illustrations.CheckmarkCircle, title: translate('common.finished')},
};
- const getHeaderButtons = useCallback(() => {
+ const selectedItemsKeys = Object.keys(selectedItems ?? []);
+
+ const headerButtonsOptions = useMemo(() => {
const options: Array> = [];
- const selectedItemsKeys = Object.keys(selectedItems ?? []);
if (selectedItemsKeys.length === 0) {
- return null;
+ return options;
}
const itemsToDelete = selectedItemsKeys.filter((id) => selectedItems[id].canDelete);
@@ -56,6 +60,9 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}:
value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE,
onSelected: () => {
clearSelectedItems?.();
+ if (isMobileSelectionModeActive) {
+ setIsMobileSelectionModeActive?.(false);
+ }
SearchActions.deleteMoneyRequestOnSearch(hash, itemsToDelete);
},
});
@@ -70,6 +77,9 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}:
value: CONST.SEARCH.BULK_ACTION_TYPES.HOLD,
onSelected: () => {
clearSelectedItems?.();
+ if (isMobileSelectionModeActive) {
+ setIsMobileSelectionModeActive?.(false);
+ }
SearchActions.holdMoneyRequestOnSearch(hash, itemsToHold, '');
},
});
@@ -84,6 +94,9 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}:
value: CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD,
onSelected: () => {
clearSelectedItems?.();
+ if (isMobileSelectionModeActive) {
+ setIsMobileSelectionModeActive?.(false);
+ }
SearchActions.unholdMoneyRequestOnSearch(hash, itemsToUnhold);
},
});
@@ -107,21 +120,18 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}:
});
}
- return (
- null}
- shouldAlwaysShowDropdownMenu
- pressOnEnter
- buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
- customText={translate('workspace.common.selected', {selectedNumber: selectedItemsKeys.length})}
- options={options}
- isSplitButton={false}
- isDisabled={isOffline}
- />
- );
- }, [clearSelectedItems, hash, isOffline, selectedItems, styles.colorMuted, styles.fontWeightNormal, theme.icon, translate]);
+ return options;
+ }, [clearSelectedItems, hash, selectedItems, selectedItemsKeys, styles, theme, translate, isMobileSelectionModeActive, setIsMobileSelectionModeActive]);
if (isSmallScreenWidth) {
+ if (isMobileSelectionModeActive) {
+ return (
+
+ );
+ }
return null;
}
@@ -131,11 +141,23 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}:
icon={headerContent[query]?.icon}
shouldShowBackButton={false}
>
- {getHeaderButtons()}
+ {headerButtonsOptions.length > 0 && (
+ null}
+ shouldAlwaysShowDropdownMenu
+ pressOnEnter
+ buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
+ customText={translate('workspace.common.selected', {selectedNumber: selectedItemsKeys.length})}
+ options={headerButtonsOptions}
+ isSplitButton={false}
+ isDisabled={isOffline}
+ />
+ )}
);
}
SearchPageHeader.displayName = 'SearchPageHeader';
+export type {SearchHeaderOptionValue};
export default SearchPageHeader;
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index fc5c23d5c9ec..78992496f031 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -1,11 +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 TableListItemSkeleton from '@components/Skeletons/TableListItemSkeleton';
+import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -23,7 +24,6 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SearchResults from '@src/types/onyx/SearchResults';
import type {SearchDataTypes, SearchQuery} from '@src/types/onyx/SearchResults';
-import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import {useSearchContext} from './SearchContext';
import SearchListWithHeader from './SearchListWithHeader';
import SearchPageHeader from './SearchPageHeader';
@@ -34,6 +34,8 @@ type SearchProps = {
policyIDs?: string;
sortBy?: SearchColumnType;
sortOrder?: SortOrder;
+ isMobileSelectionModeActive?: boolean;
+ setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void;
};
const sortableSearchTabs: SearchQuery[] = [CONST.SEARCH.TAB.ALL];
@@ -41,15 +43,17 @@ const transactionItemMobileHeight = 100;
const reportItemTransactionHeight = 52;
const listItemPadding = 12; // this is equivalent to 'mb3' on every transaction/report list item
const searchHeaderHeight = 54;
-
-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 (SearchUtils.isTransactionListItemType(item)) {
@@ -70,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) {
@@ -90,9 +101,8 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [hash, isOffline]);
- const isLoadingItems = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || searchResults?.data === undefined;
+ const isLoadingItems = !isOffline && searchResults?.data === undefined;
const isLoadingMoreItems = !isLoadingItems && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
- const shouldShowEmptyState = !isLoadingItems && SearchUtils.isSearchResultsEmpty(searchResults);
if (isLoadingItems) {
return (
@@ -101,12 +111,14 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
query={query}
hash={hash}
/>
-
+
>
);
}
- if (shouldShowEmptyState) {
+ const shouldShowEmptyState = searchResults && SearchUtils.isSearchResultsEmpty(searchResults);
+
+ if (shouldShowEmptyState ?? !searchResults) {
return (
<>
+ !isLargeScreenWidth ? null : (
+
+ )
}
- canSelectMultiple={isLargeScreenWidth}
+ 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.
@@ -197,7 +213,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
updateCellsBatchingPeriod={200}
ListItem={ListItem}
onSelectRow={openReport}
- getItemHeight={getItemHeight}
+ getItemHeight={getItemHeightMemoized}
shouldDebounceRowSelect
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
listHeaderWrapperStyle={[styles.ph8, styles.pv3, styles.pb5]}
@@ -205,9 +221,11 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
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
index cff74fe08a0a..2238b0f49855 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -26,4 +26,21 @@ type SearchContext = {
setSelectedTransactionIds: (selectedTransactionIds: string[]) => void;
};
-export type {SelectedTransactionInfo, SelectedTransactions, SearchColumnType, SortOrder, SearchContext};
+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/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 99330478c75f..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
@@ -92,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 8b6ba790e6b0..eb2e66ad9a78 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -93,6 +93,8 @@ function BaseSelectionList(
updateCellsBatchingPeriod = 50,
removeClippedSubviews = true,
shouldDelayFocus = true,
+ onLongPressRow,
+ isMobileSelectionModeActive,
}: BaseSelectionListProps,
ref: ForwardedRef,
) {
@@ -447,6 +449,8 @@ function BaseSelectionList(
isDisabled={isDisabled}
showTooltip={showTooltip}
canSelectMultiple={canSelectMultiple}
+ onLongPressRow={onLongPressRow}
+ isMobileSelectionModeActive={isMobileSelectionModeActive}
onSelectRow={() => selectRow(item)}
onCheckboxPress={handleOnCheckboxPress()}
onDismissError={() => onDismissError?.(item)}
diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx
index ad77070c1b99..7888a8b26114 100644
--- a/src/components/SelectionList/Search/ActionCell.tsx
+++ b/src/components/SelectionList/Search/ActionCell.tsx
@@ -31,9 +31,19 @@ type ActionCellProps = {
isLargeScreenWidth?: boolean;
isSelected?: boolean;
goToItem: () => void;
+ isChildListItem?: boolean;
+ parentAction?: string;
};
-function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isLargeScreenWidth = true, isSelected = false, goToItem}: ActionCellProps) {
+function ActionCell({
+ action = CONST.SEARCH.ACTION_TYPES.VIEW,
+ transactionID,
+ isLargeScreenWidth = true,
+ isSelected = false,
+ goToItem,
+ isChildListItem = false,
+ parentAction = '',
+}: ActionCellProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
@@ -53,13 +63,11 @@ function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isL
}
}, [action, currentSearchHash, transactionID]);
- if (!isLargeScreenWidth) {
- return null;
- }
-
const text = translate(actionTranslationsMap[action]);
- if (action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE) {
+ const 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 (
+ ) : null;
+ }
+
+ if (action === CONST.SEARCH.ACTION_TYPES.REVIEW) {
return (
);
}
-
return (
void;
+ canSelectMultiple?: boolean;
+ isSelected?: boolean;
+ isDisabled?: boolean | null;
+ isDisabledCheckbox?: boolean;
+ handleCheckboxPress?: () => void;
};
-function ExpenseItemHeaderNarrow({participantFrom, participantFromDisplayName, participantTo, participantToDisplayName, action, transactionID, onButtonPress}: ExpenseItemHeaderNarrowProps) {
+function ExpenseItemHeaderNarrow({
+ participantFrom,
+ participantFromDisplayName,
+ participantTo,
+ participantToDisplayName,
+ onButtonPress,
+ action,
+ canSelectMultiple,
+ isDisabledCheckbox,
+ isSelected,
+ isDisabled,
+ handleCheckboxPress,
+ text,
+ transactionID,
+}: ExpenseItemHeaderNarrowProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const theme = useTheme();
@@ -28,6 +50,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 29c7bc2ca60c..e6358698d414 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -5,7 +5,6 @@ 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';
@@ -59,12 +58,12 @@ function ReportListItem({
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();
@@ -72,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);
@@ -110,6 +109,7 @@ function ReportListItem({
onSelectRow={() => openReportInRHP(transactionItem)}
onDismissError={onDismissError}
onFocus={onFocus}
+ onLongPressRow={onLongPressRow}
shouldSyncFocus={shouldSyncFocus}
/>
);
@@ -126,6 +126,7 @@ function ReportListItem({
showTooltip={showTooltip}
canSelectMultiple={canSelectMultiple}
onSelectRow={onSelectRow}
+ onLongPressRow={onLongPressRow}
onDismissError={onDismissError}
errors={item.errors}
pendingAction={item.pendingAction}
@@ -145,7 +146,7 @@ function ReportListItem({
onButtonPress={handleOnButtonPress}
/>
)}
-
+
{canSelectMultiple && (
@@ -155,12 +156,11 @@ function ReportListItem({
containerStyle={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!item.isSelected, !!item.isDisabled)]}
disabled={!!isDisabled || item.isDisabledCheckbox}
accessibilityLabel={item.text ?? ''}
- style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled]}
+ style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, !isLargeScreenWidth && styles.mr3]}
/>
)}
{reportItem?.reportName}
- {`${reportItem.transactions.length} ${translate('search.groupedExpenses')}`}
@@ -172,22 +172,18 @@ function ReportListItem({
{isLargeScreenWidth && (
- <>
- {/** We add an empty view with type style to align the total with the table header */}
-
-
-
-
- >
+
+
+
)}
-
{reportItem.transactions.map((transaction) => (
{
@@ -200,6 +196,7 @@ function ReportListItem({
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 6db308831baa..a10552ca9ad8 100644
--- a/src/components/SelectionList/Search/TransactionListItem.tsx
+++ b/src/components/SelectionList/Search/TransactionListItem.tsx
@@ -15,6 +15,7 @@ function TransactionListItem({
onCheckboxPress,
onDismissError,
onFocus,
+ onLongPressRow,
shouldSyncFocus,
}: TransactionListItemProps) {
const transactionItem = item as unknown as TransactionListItemType;
@@ -46,6 +47,7 @@ function TransactionListItem({
pendingAction={item.pendingAction}
keyForList={item.keyForList}
onFocus={onFocus}
+ onLongPressRow={onLongPressRow}
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
>
@@ -59,6 +61,7 @@ function TransactionListItem({
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 f9ca70536e4b..a4d567d5f8ab 100644
--- a/src/components/SelectionList/Search/TransactionListItemRow.tsx
+++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx
@@ -4,6 +4,7 @@ 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';
@@ -52,6 +53,8 @@ type TransactionListItemRowProps = {
isDisabled: boolean;
canSelectMultiple: boolean;
isButtonSelected?: boolean;
+ parentAction?: string;
+ shouldShowTransactionCheckbox?: boolean;
};
const getTypeIcon = (type?: SearchTransactionType) => {
@@ -158,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 (
{showItemHeaderOnNarrowLayout && (
)}
-
+
+ {canSelectMultiple && shouldShowTransactionCheckbox && (
+
+
+ {item.isSelected && (
+
+ )}
+
+
+ )}
+
+
+
-
-
-
diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx
index 235cff294f8f..4bf1715e0434 100644
--- a/src/components/SelectionList/SearchTableHeader.tsx
+++ b/src/components/SelectionList/SearchTableHeader.tsx
@@ -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',
@@ -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',
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index d40a6f0fa225..bfab13e895cc 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -60,6 +60,12 @@ type CommonListItemProps = {
/** 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 = {
@@ -465,6 +471,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/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/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx
index 0c7e603a4aa2..5e563ea99763 100644
--- a/src/components/TaskHeaderActionButton.tsx
+++ b/src/components/TaskHeaderActionButton.tsx
@@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
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 ONYXKEYS from '@src/ONYXKEYS';
@@ -36,7 +37,17 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps)
isDisabled={!Task.canModifyTask(report, session?.accountID ?? -1)}
medium
text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')}
- onPress={Session.checkIfActionIsAllowed(() => (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/hooks/useActiveBottomTabRoute.ts b/src/hooks/useActiveBottomTabRoute.ts
new file mode 100644
index 000000000000..434cca0cd815
--- /dev/null
+++ b/src/hooks/useActiveBottomTabRoute.ts
@@ -0,0 +1,8 @@
+import {useContext} from 'react';
+import ActiveBottomTabRouteContext from '@libs/Navigation/AppNavigator/Navigators/ActiveBottomTabRouteContext';
+
+function useActiveBottomTabRoute() {
+ return useContext(ActiveBottomTabRouteContext);
+}
+
+export default useActiveBottomTabRoute;
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/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts
index 5888f96d1c15..022d6178877d 100644
--- a/src/hooks/useHtmlPaste/index.ts
+++ b/src/hooks/useHtmlPaste/index.ts
@@ -1,5 +1,6 @@
import {useNavigation} from '@react-navigation/native';
import {useCallback, useEffect} from 'react';
+import type {ClipboardEvent as PasteEvent} from 'react';
import Parser from '@libs/Parser';
import type UseHtmlPaste from './types';
@@ -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 {
@@ -142,18 +145,18 @@ 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-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts
new file mode 100644
index 000000000000..b806c0dea95a
--- /dev/null
+++ b/src/hooks/usePaginatedReportActions.ts
@@ -0,0 +1,39 @@
+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';
+
+/**
+ * Get the longest continuous chunk of reportActions including the linked reportAction. If not linking to a specific action, returns the continuous chunk of newest reportActions.
+ */
+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}`, {
+ canEvict: false,
+ selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
+ });
+ const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`);
+
+ const reportActions = useMemo(() => {
+ if (!sortedAllReportActions?.length) {
+ return [];
+ }
+ return PaginationUtils.getContinuousChain(sortedAllReportActions, reportActionPages ?? [], (reportAction) => reportAction.reportActionID, reportActionID);
+ }, [reportActionID, reportActionPages, sortedAllReportActions]);
+
+ const linkedAction = useMemo(
+ () => sortedAllReportActions?.find((reportAction) => String(reportAction.reportActionID) === String(reportActionID)),
+ [reportActionID, sortedAllReportActions],
+ );
+
+ return {
+ reportActions,
+ linkedAction,
+ };
+}
+
+export default usePaginatedReportActions;
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/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 2e18cdf76520..49795aa3b253 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -135,6 +135,7 @@ export default {
yes: 'Yes',
no: 'No',
ok: 'OK',
+ notNow: 'Not now',
learnMore: 'Learn more',
buttonConfirm: 'Got it',
name: 'Name',
@@ -521,6 +522,7 @@ export default {
replyInThread: 'Reply in thread',
joinThread: 'Join thread',
leaveThread: 'Leave thread',
+ copyOnyxData: 'Copy Onyx data',
flagAsOffensive: 'Flag as offensive',
menu: 'Menu',
},
@@ -605,7 +607,7 @@ export default {
saveTheWorld: 'Save the world',
},
allSettingsScreen: {
- subscriptions: 'Subscriptions',
+ subscription: 'Subscription',
cardsAndDomains: 'Cards & Domains',
},
tabSelector: {
@@ -625,6 +627,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',
@@ -641,7 +647,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',
@@ -1153,9 +1159,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.',
@@ -1463,7 +1469,6 @@ 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',
@@ -1619,7 +1624,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.',
@@ -1685,7 +1690,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',
@@ -2638,6 +2643,12 @@ export default {
'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',
},
categories: {
deleteCategories: 'Delete categories',
@@ -2666,6 +2677,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: {
@@ -2803,6 +2818,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.',
@@ -3126,6 +3143,14 @@ export default {
defaultVendor: 'Default vendor',
autoSync: 'Auto-sync',
reimbursedReports: 'Sync reimbursed reports',
+ cardReconciliation: 'Card reconciliation',
+ reconciliationAccount: 'Reconciliation account',
+ 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.`,
+ },
},
bills: {
manageYourBills: 'Manage your bills',
@@ -3168,7 +3193,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',
@@ -3182,7 +3207,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: {
@@ -3216,7 +3241,7 @@ export default {
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.',
},
@@ -3411,7 +3436,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?',
@@ -3437,6 +3462,7 @@ export default {
screenShareRequest: 'Expensify is inviting you to a screen share',
},
search: {
+ selectMultiple: 'Select multiple',
resultsAreLimited: 'Search results are limited.',
searchResults: {
emptyResults: {
@@ -3902,7 +3928,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: {
@@ -3972,6 +3998,10 @@ export default {
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',
@@ -4051,7 +4081,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.',
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 882167db02dc..04effd01812e 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -125,6 +125,7 @@ export default {
yes: 'Sí',
no: 'No',
ok: 'OK',
+ notNow: 'Ahora no',
learnMore: 'Más información',
buttonConfirm: 'Ok, entendido',
name: 'Nombre',
@@ -372,8 +373,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',
@@ -513,6 +514,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ú',
},
@@ -598,7 +600,7 @@ export default {
saveTheWorld: 'Salvar el mundo',
},
allSettingsScreen: {
- subscriptions: 'Suscripciones',
+ subscription: 'Suscripcion',
cardsAndDomains: 'Tarjetas y Dominios',
},
tabSelector: {
@@ -617,7 +619,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',
@@ -634,7 +642,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',
@@ -899,13 +907,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.',
@@ -1124,7 +1132,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.',
},
},
@@ -1147,7 +1155,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.',
},
},
@@ -1158,9 +1166,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.',
@@ -1375,7 +1383,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 groupo de ${displayName}`,
+ defaultReportName: ({displayName}: {displayName: string}) => `Chat de grupo de ${displayName}`,
},
languagePage: {
language: 'Idioma',
@@ -1471,7 +1479,6 @@ 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',
@@ -1539,7 +1546,7 @@ export default {
localTime: 'Hora local',
},
newChatPage: {
- startGroup: 'Grupo de inicio',
+ startGroup: 'Crear grupo',
addToGroup: 'Añadir al grupo',
},
yearPickerPage: {
@@ -1645,7 +1652,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: {
@@ -1691,7 +1698,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.',
@@ -1713,7 +1720,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',
@@ -2684,6 +2691,12 @@ export default {
'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',
},
categories: {
deleteCategories: 'Eliminar categorías',
@@ -2712,6 +2725,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 GL',
+ updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código GL. Inténtelo nuevamente.',
},
moreFeatures: {
spendSection: {
@@ -2847,8 +2864,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 GL',
+ updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código GL. Por favor, inténtelo nuevamente.',
},
taxes: {
subtitle: 'Añade nombres, tasas y establezca valores por defecto para los impuestos.',
@@ -3112,6 +3131,14 @@ export default {
defaultVendor: 'Proveedor predeterminado',
autoSync: 'Autosincronización',
reimbursedReports: 'Sincronizar informes reembolsados',
+ cardReconciliation: 'Conciliación de tarjetas',
+ reconciliationAccount: 'Cuenta de conciliación',
+ 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',
@@ -3215,7 +3242,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',
@@ -3230,7 +3257,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...',
@@ -3263,7 +3290,7 @@ export default {
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.',
},
@@ -3460,8 +3487,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?',
},
@@ -3486,6 +3513,7 @@ export default {
screenShareRequest: 'Expensify te está invitando a compartir la pantalla',
},
search: {
+ selectMultiple: 'Seleccionar múltiples',
resultsAreLimited: 'Los resultados de búsqueda están limitados.',
searchResults: {
emptyResults: {
@@ -4415,7 +4443,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: {
@@ -4487,6 +4515,10 @@ export default {
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',
@@ -4566,7 +4598,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.',
},
diff --git a/src/libs/API/parameters/PolicyReportFieldsReplace.ts b/src/libs/API/parameters/DeletePolicyReportField.ts
similarity index 66%
rename from src/libs/API/parameters/PolicyReportFieldsReplace.ts
rename to src/libs/API/parameters/DeletePolicyReportField.ts
index c6d1834f0789..d79e9b07249e 100644
--- a/src/libs/API/parameters/PolicyReportFieldsReplace.ts
+++ b/src/libs/API/parameters/DeletePolicyReportField.ts
@@ -1,4 +1,4 @@
-type PolicyReportFieldsReplace = {
+type DeletePolicyReportField = {
policyID: string;
/**
* Stringified JSON object with type of following structure:
@@ -7,4 +7,4 @@ type PolicyReportFieldsReplace = {
reportFields: string;
};
-export default PolicyReportFieldsReplace;
+export default DeletePolicyReportField;
diff --git a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts b/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts
deleted file mode 100644
index f790ada3aad9..000000000000
--- a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-type UpdateFrequentlyUsedEmojisParams = {value: string};
-
-export default UpdateFrequentlyUsedEmojisParams;
diff --git a/src/libs/API/parameters/UpdateNetSuiteCustomersJobsParams.ts b/src/libs/API/parameters/UpdateNetSuiteCustomersJobsParams.ts
new file mode 100644
index 000000000000..c29b503a9acf
--- /dev/null
+++ b/src/libs/API/parameters/UpdateNetSuiteCustomersJobsParams.ts
@@ -0,0 +1,10 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type UpdateNetSuiteCustomersJobsParams = {
+ policyID: string;
+ customersMapping: ValueOf;
+ jobsMapping: ValueOf;
+};
+
+export default UpdateNetSuiteCustomersJobsParams;
diff --git a/src/libs/API/parameters/UpdatePolicyCategoryGLCodeParams.ts b/src/libs/API/parameters/UpdatePolicyCategoryGLCodeParams.ts
new file mode 100644
index 000000000000..f5e4d4ab7eca
--- /dev/null
+++ b/src/libs/API/parameters/UpdatePolicyCategoryGLCodeParams.ts
@@ -0,0 +1,7 @@
+type UpdatePolicyCategoryGLCodeParams = {
+ policyID: string;
+ categoryName: string;
+ glCode: string;
+};
+
+export default UpdatePolicyCategoryGLCodeParams;
diff --git a/src/libs/API/parameters/UpdatePolicyCategoryPayrollCodeParams.ts b/src/libs/API/parameters/UpdatePolicyCategoryPayrollCodeParams.ts
new file mode 100644
index 000000000000..8751832f7cad
--- /dev/null
+++ b/src/libs/API/parameters/UpdatePolicyCategoryPayrollCodeParams.ts
@@ -0,0 +1,7 @@
+type UpdatePolicyCategoryPayrollCodeParams = {
+ policyID: string;
+ categoryName: string;
+ payrollCode: string;
+};
+
+export default UpdatePolicyCategoryPayrollCodeParams;
diff --git a/src/libs/API/parameters/UpdatePolicyTagGLCodeParams.ts b/src/libs/API/parameters/UpdatePolicyTagGLCodeParams.ts
new file mode 100644
index 000000000000..f720864a8f68
--- /dev/null
+++ b/src/libs/API/parameters/UpdatePolicyTagGLCodeParams.ts
@@ -0,0 +1,9 @@
+type UpdatePolicyTagGLCodeParams = {
+ policyID: string;
+ tagListName: string;
+ tagListIndex: number;
+ tagName: string;
+ glCode: string;
+};
+
+export default UpdatePolicyTagGLCodeParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 539854ba7842..204fa01ed14c 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -64,7 +64,6 @@ export type {default as UpdateAutomaticTimezoneParams} from './UpdateAutomaticTi
export type {default as UpdateChatPriorityModeParams} from './UpdateChatPriorityModeParams';
export type {default as UpdateDateOfBirthParams} from './UpdateDateOfBirthParams';
export type {default as UpdateDisplayNameParams} from './UpdateDisplayNameParams';
-export type {default as UpdateFrequentlyUsedEmojisParams} from './UpdateFrequentlyUsedEmojisParams';
export type {default as UpdateGroupChatNameParams} from './UpdateGroupChatNameParams';
export type {default as UpdateGroupChatMemberRolesParams} from './UpdateGroupChatMemberRolesParams';
export type {default as UpdateHomeAddressParams} from './UpdateHomeAddressParams';
@@ -170,6 +169,8 @@ export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspace
export type {default as RenameWorkspaceCategoriesParams} from './RenameWorkspaceCategoriesParams';
export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams';
export type {default as DeleteWorkspaceCategoriesParams} from './DeleteWorkspaceCategoriesParams';
+export type {default as UpdatePolicyCategoryPayrollCodeParams} from './UpdatePolicyCategoryPayrollCodeParams';
+export type {default as UpdatePolicyCategoryGLCodeParams} from './UpdatePolicyCategoryGLCodeParams';
export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams';
export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams';
export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams';
@@ -211,6 +212,7 @@ export type {default as DeletePolicyTaxesParams} from './DeletePolicyTaxesParams
export type {default as UpdatePolicyTaxValueParams} from './UpdatePolicyTaxValueParams';
export type {default as RenamePolicyTagsParams} from './RenamePolicyTagsParams';
export type {default as DeletePolicyTagsParams} from './DeletePolicyTagsParams';
+export type {default as UpdatePolicyTagGLCodeParams} from './UpdatePolicyTagGLCodeParams';
export type {default as AddSubscriptionPaymentCardParams} from './AddSubscriptionPaymentCardParams';
export type {default as SetPolicyCustomTaxNameParams} from './SetPolicyCustomTaxNameParams';
export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicyForeignCurrencyDefaultParams';
@@ -242,7 +244,7 @@ export type {default as DeleteMoneyRequestOnSearchParams} from './DeleteMoneyReq
export type {default as HoldMoneyRequestOnSearchParams} from './HoldMoneyRequestOnSearchParams';
export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams';
export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams';
-export type {default as PolicyReportFieldsReplace} from './PolicyReportFieldsReplace';
+export type {default as DeletePolicyReportField} from './DeletePolicyReportField';
export type {default as ConnectPolicyToNetSuiteParams} from './ConnectPolicyToNetSuiteParams';
export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspaceReportFieldParams';
export type {default as UpdateWorkspaceReportFieldInitialValueParams} from './UpdateWorkspaceReportFieldInitialValueParams';
@@ -254,4 +256,5 @@ export type {default as RequestExpensifyCardLimitIncreaseParams} from './Request
export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams';
export type {default as UpdateNetSuiteCustomFormIDParams} from './UpdateNetSuiteCustomFormIDParams';
export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIntacctGenericTypeParams';
+export type {default as UpdateNetSuiteCustomersJobsParams} from './UpdateNetSuiteCustomersJobsParams';
export type {default as CopyExistingPolicyConnectionParams} from './CopyExistingPolicyConnectionParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 609c19497da3..4ecddbdb7406 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -57,7 +57,6 @@ const WRITE_COMMANDS = {
VALIDATE_LOGIN: 'ValidateLogin',
VALIDATE_SECONDARY_LOGIN: 'ValidateSecondaryLogin',
UPDATE_PREFERRED_EMOJI_SKIN_TONE: 'UpdatePreferredEmojiSkinTone',
- UPDATE_FREQUENTLY_USED_EMOJIS: 'UpdateFrequentlyUsedEmojis',
UPDATE_CHAT_PRIORITY_MODE: 'UpdateChatPriorityMode',
SET_CONTACT_METHOD_AS_DEFAULT: 'SetContactMethodAsDefault',
UPDATE_THEME: 'UpdateTheme',
@@ -131,12 +130,15 @@ const WRITE_COMMANDS = {
CREATE_POLICY_TAG: 'CreatePolicyTag',
RENAME_POLICY_TAG: 'RenamePolicyTag',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
+ UPDATE_POLICY_CATEGORY_PAYROLL_CODE: 'UpdatePolicyCategoryPayrollCode',
+ UPDATE_POLICY_CATEGORY_GL_CODE: 'UpdatePolicyCategoryGLCode',
DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories',
DELETE_POLICY_REPORT_FIELD: 'DeletePolicyReportField',
SET_POLICY_TAGS_REQUIRED: 'SetPolicyTagsRequired',
SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag',
RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist',
DELETE_POLICY_TAGS: 'DeletePolicyTags',
+ UPDATE_POLICY_TAG_GL_CODE: 'UpdatePolicyTagGLCode',
CREATE_TASK: 'CreateTask',
CANCEL_TASK: 'CancelTask',
EDIT_TASK_ASSIGNEE: 'EditTaskAssignee',
@@ -250,6 +252,7 @@ const WRITE_COMMANDS = {
UPDATE_NETSUITE_LOCATIONS_MAPPING: 'UpdateNetSuiteLocationsMapping',
UPDATE_NETSUITE_CUSTOMERS_MAPPING: 'UpdateNetSuiteCustomersMapping',
UPDATE_NETSUITE_JOBS_MAPPING: 'UpdateNetSuiteJobsMapping',
+ UPDATE_NETSUITE_CUSTOMERS_JOBS_MAPPING: 'UpdateNetSuiteCustomersJobsMapping',
UPDATE_NETSUITE_EXPORTER: 'UpdateNetSuiteExporter',
UPDATE_NETSUITE_EXPORT_DATE: 'UpdateNetSuiteExportDate',
UPDATE_NETSUITE_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'UpdateNetSuiteReimbursableExpensesExportDestination',
@@ -353,7 +356,6 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.VALIDATE_LOGIN]: Parameters.ValidateLoginParams;
[WRITE_COMMANDS.VALIDATE_SECONDARY_LOGIN]: Parameters.ValidateSecondaryLoginParams;
[WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE]: Parameters.UpdatePreferredEmojiSkinToneParams;
- [WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS]: Parameters.UpdateFrequentlyUsedEmojisParams;
[WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE]: Parameters.UpdateChatPriorityModeParams;
[WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: Parameters.SetContactMethodAsDefaultParams;
[WRITE_COMMANDS.UPDATE_THEME]: Parameters.UpdateThemeParams;
@@ -428,12 +430,15 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams;
- [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.PolicyReportFieldsReplace;
+ [WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE]: Parameters.UpdatePolicyCategoryPayrollCodeParams;
+ [WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE]: Parameters.UpdatePolicyCategoryGLCodeParams;
+ [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.DeletePolicyReportField;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
[WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED]: Parameters.SetPolicyTagsRequired;
[WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglistParams;
[WRITE_COMMANDS.CREATE_POLICY_TAG]: Parameters.CreatePolicyTagsParams;
[WRITE_COMMANDS.RENAME_POLICY_TAG]: Parameters.RenamePolicyTagsParams;
+ [WRITE_COMMANDS.UPDATE_POLICY_TAG_GL_CODE]: Parameters.UpdatePolicyTagGLCodeParams;
[WRITE_COMMANDS.SET_POLICY_TAGS_ENABLED]: Parameters.SetPolicyTagsEnabled;
[WRITE_COMMANDS.DELETE_POLICY_TAGS]: Parameters.DeletePolicyTagsParams;
[WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams;
@@ -582,6 +587,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_NETSUITE_LOCATIONS_MAPPING]: Parameters.UpdateNetSuiteGenericTypeParams<'mapping', ValueOf>;
[WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOMERS_MAPPING]: Parameters.UpdateNetSuiteGenericTypeParams<'mapping', ValueOf>;
[WRITE_COMMANDS.UPDATE_NETSUITE_JOBS_MAPPING]: Parameters.UpdateNetSuiteGenericTypeParams<'mapping', ValueOf>;
+ [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOMERS_JOBS_MAPPING]: Parameters.UpdateNetSuiteCustomersJobsParams;
[WRITE_COMMANDS.UPDATE_NETSUITE_EXPORTER]: Parameters.UpdateNetSuiteGenericTypeParams<'email', string>;
[WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_DATE]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>;
[WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>;
diff --git a/src/libs/Browser/index.ts b/src/libs/Browser/index.ts
index 98ad449c3dd0..aeec4f4def4a 100644
--- a/src/libs/Browser/index.ts
+++ b/src/libs/Browser/index.ts
@@ -1,4 +1,4 @@
-import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types';
+import type {GetBrowser, IsChromeIOS, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types';
const getBrowser: GetBrowser = () => '';
@@ -10,8 +10,10 @@ const isMobileChrome: IsMobileChrome = () => false;
const isMobileWebKit: IsMobileWebKit = () => false;
+const isChromeIOS: IsChromeIOS = () => false;
+
const isSafari: IsSafari = () => false;
const openRouteInDesktopApp: OpenRouteInDesktopApp = () => {};
-export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, openRouteInDesktopApp};
+export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, isChromeIOS, openRouteInDesktopApp};
diff --git a/src/libs/Browser/index.website.ts b/src/libs/Browser/index.website.ts
index a83fa1cac70e..b89190dc7f78 100644
--- a/src/libs/Browser/index.website.ts
+++ b/src/libs/Browser/index.website.ts
@@ -1,7 +1,7 @@
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
-import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types';
+import type {GetBrowser, IsChromeIOS, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types';
/**
* Fetch browser name from UA string
@@ -66,6 +66,14 @@ const isMobileWebKit: IsMobileWebKit = () => {
return /iP(ad|od|hone)/i.test(userAgent) && /WebKit/i.test(userAgent);
};
+/**
+ * Checks if the requesting user agent is a Chrome browser on an iOS mobile device.
+ */
+const isChromeIOS: IsChromeIOS = () => {
+ const userAgent = navigator.userAgent;
+ return /iP(ad|od|hone)/i.test(userAgent) && /CriOS/i.test(userAgent);
+};
+
const isSafari: IsSafari = () => getBrowser() === 'safari' || isMobileSafari();
/**
@@ -109,4 +117,4 @@ const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '',
}
};
-export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, openRouteInDesktopApp};
+export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, isChromeIOS, openRouteInDesktopApp};
diff --git a/src/libs/Browser/types.ts b/src/libs/Browser/types.ts
index 25f305953c87..cb242d3729aa 100644
--- a/src/libs/Browser/types.ts
+++ b/src/libs/Browser/types.ts
@@ -8,8 +8,10 @@ type IsMobileChrome = () => boolean;
type IsMobileWebKit = () => boolean;
+type IsChromeIOS = () => boolean;
+
type IsSafari = () => boolean;
type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string) => void;
-export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp};
+export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, IsChromeIOS, OpenRouteInDesktopApp};
diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts
index 512eebce78e2..130aad270b92 100644
--- a/src/libs/Clipboard/index.ts
+++ b/src/libs/Clipboard/index.ts
@@ -115,10 +115,14 @@ const setHtml: SetHtml = (html: string, text: string) => {
// See https://webkit.org/blog/10855/async-clipboard-api/ for more details.
setHTMLSync(html, text);
} else {
+ const htmlNonClosingTags = html
+ .replace(//gi, '')
+ .replace(//gi, '');
+
navigator.clipboard.write([
new ClipboardItem({
/* eslint-disable @typescript-eslint/naming-convention */
- 'text/html': new Blob([html], {type: 'text/html'}),
+ 'text/html': new Blob([htmlNonClosingTags], {type: 'text/html'}),
'text/plain': new Blob([text], {type: 'text/plain'}),
}),
]);
diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts
index 862b0ae5e928..c3b80797d750 100644
--- a/src/libs/CurrencyUtils.ts
+++ b/src/libs/CurrencyUtils.ts
@@ -125,9 +125,11 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR
style: 'currency',
currency: currencyWithFallback,
- // We are forcing the number of decimals because we override the default number of decimals in the backend for RSD
+ // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies
// See: https://github.com/Expensify/PHP-Libs/pull/834
- minimumFractionDigits: currency === 'RSD' ? getCurrencyDecimals(currency) : undefined,
+ minimumFractionDigits: getCurrencyDecimals(currency),
+ // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places.
+ maximumFractionDigits: 2,
});
}
@@ -175,9 +177,11 @@ function convertToDisplayStringWithoutCurrency(amountInCents: number, currency:
style: 'currency',
currency,
- // We are forcing the number of decimals because we override the default number of decimals in the backend for RSD
+ // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies
// See: https://github.com/Expensify/PHP-Libs/pull/834
- minimumFractionDigits: currency === 'RSD' ? getCurrencyDecimals(currency) : undefined,
+ minimumFractionDigits: getCurrencyDecimals(currency),
+ // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places.
+ maximumFractionDigits: 2,
})
.filter((x) => x.type !== 'currency')
.filter((x) => x.type !== 'literal' || x.value.trim().length !== 0)
diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts
index 6fb5725addfc..007a892c048e 100644
--- a/src/libs/EmojiUtils.ts
+++ b/src/libs/EmojiUtils.ts
@@ -1,4 +1,3 @@
-import {getUnixTime} from 'date-fns';
import {Str} from 'expensify-common';
import memoize from 'lodash/memoize';
import Onyx from 'react-native-onyx';
@@ -235,37 +234,6 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL
return addSpacesToEmojiCategories(mergedEmojis);
}
-/**
- * Get the updated frequently used emojis list by usage
- */
-function getFrequentlyUsedEmojis(newEmoji: Emoji | Emoji[]): FrequentlyUsedEmoji[] {
- let frequentEmojiList = [...frequentlyUsedEmojis];
-
- const maxFrequentEmojiCount = CONST.EMOJI_FREQUENT_ROW_COUNT * CONST.EMOJI_NUM_PER_ROW - 1;
-
- const currentTimestamp = getUnixTime(new Date());
- (Array.isArray(newEmoji) ? [...newEmoji] : [newEmoji]).forEach((emoji) => {
- let currentEmojiCount = 1;
- const emojiIndex = frequentEmojiList.findIndex((e) => e.code === emoji.code);
- if (emojiIndex >= 0) {
- currentEmojiCount = frequentEmojiList[emojiIndex].count + 1;
- frequentEmojiList.splice(emojiIndex, 1);
- }
-
- const updatedEmoji = {...Emojis.emojiCodeTableWithSkinTones[emoji.code], count: currentEmojiCount, lastUpdatedAt: currentTimestamp};
-
- // We want to make sure the current emoji is added to the list
- // Hence, we take one less than the current frequent used emojis
- frequentEmojiList = frequentEmojiList.slice(0, maxFrequentEmojiCount);
- frequentEmojiList.push(updatedEmoji);
-
- // Sort the list by count and lastUpdatedAt in descending order
- frequentEmojiList.sort((a, b) => b.count - a.count || b.lastUpdatedAt - a.lastUpdatedAt);
- });
-
- return frequentEmojiList;
-}
-
/**
* Given an emoji item object, return an emoji code based on its type.
*/
@@ -601,7 +569,6 @@ export {
getLocalizedEmojiName,
getHeaderEmojis,
mergeEmojisWithFrequentlyUsedEmojis,
- getFrequentlyUsedEmojis,
containsOnlyEmojis,
replaceEmojis,
suggestEmojis,
diff --git a/src/libs/Environment/Environment.ts b/src/libs/Environment/Environment.ts
index 1f5a391d3b13..c343788bed05 100644
--- a/src/libs/Environment/Environment.ts
+++ b/src/libs/Environment/Environment.ts
@@ -38,6 +38,13 @@ function isDevelopment(): boolean {
return (Config?.ENVIRONMENT ?? CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.DEV;
}
+/**
+ * Are we running the app in staging?
+ */
+function isStaging(): boolean {
+ return (Config?.ENVIRONMENT ?? CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.STAGING;
+}
+
/**
* Are we running the app in production?
*/
@@ -76,4 +83,4 @@ function getSpotnanaEnvironmentTMCID(): Promise {
return getEnvironment().then((environment) => SPOTNANA_ENVIRONMENT_TMC_ID[environment]);
}
-export {getEnvironment, isInternalTestBuild, isDevelopment, isProduction, getEnvironmentURL, getOldDotEnvironmentURL, getTravelDotEnvironmentURL, getSpotnanaEnvironmentTMCID};
+export {getEnvironment, isInternalTestBuild, isDevelopment, isStaging, isProduction, getEnvironmentURL, getOldDotEnvironmentURL, getTravelDotEnvironmentURL, getSpotnanaEnvironmentTMCID};
diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts
index 4a7551beca77..1adb607bcfca 100644
--- a/src/libs/Fullstory/index.native.ts
+++ b/src/libs/Fullstory/index.native.ts
@@ -1,6 +1,9 @@
import FullStory, {FSPage} from '@fullstory/react-native';
import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import CONST from '@src/CONST';
import * as Environment from '@src/libs/Environment/Environment';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {UserMetadata} from '@src/types/onyx';
/**
@@ -8,6 +11,21 @@ import type {UserMetadata} from '@src/types/onyx';
* Proxy function calls to React-Native lib
* */
const FS = {
+ /**
+ * Initializes FullStory
+ */
+ init: () => {
+ Environment.getEnvironment().then((envName: string) => {
+ // We only want to start fullstory if the app is running in production
+ if (envName !== CONST.ENVIRONMENT.PRODUCTION) {
+ return;
+ }
+ FullStory.restart();
+ const [session] = useOnyx(ONYXKEYS.USER_METADATA);
+ FS.fsIdentify(session);
+ });
+ },
+
/**
* Sets the identity as anonymous using the FullStory library.
*/
diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts
index a9c75ad838e9..8419bccabb90 100644
--- a/src/libs/Fullstory/index.ts
+++ b/src/libs/Fullstory/index.ts
@@ -92,6 +92,11 @@ const FS = {
});
}
},
+
+ /**
+ * Init function, created so we're consistent with the native file
+ */
+ init: () => {},
};
export default FS;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index cbf7ef82b401..1defbc70464d 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -237,6 +237,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/members/WorkspaceOwnerChangeErrorPage').default,
[SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../../pages/workspace/categories/CreateCategoryPage').default,
[SCREENS.WORKSPACE.CATEGORY_EDIT]: () => require('../../../../pages/workspace/categories/EditCategoryPage').default,
+ [SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE]: () => require('../../../../pages/workspace/categories/CategoryPayrollCodePage').default,
+ [SCREENS.WORKSPACE.CATEGORY_GL_CODE]: () => require('../../../../pages/workspace/categories/CategoryGLCodePage').default,
[SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../../pages/workspace/distanceRates/CreateDistanceRatePage').default,
[SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default,
[SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRateDetailsPage').default,
@@ -250,6 +252,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/tags/WorkspaceEditTagsPage').default,
[SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../../pages/workspace/tags/WorkspaceCreateTagPage').default,
[SCREENS.WORKSPACE.TAG_EDIT]: () => require('../../../../pages/workspace/tags/EditTagPage').default,
+ [SCREENS.WORKSPACE.TAG_GL_CODE]: () => require('../../../../pages/workspace/tags/TagGLCodePage').default,
[SCREENS.WORKSPACE.TAXES_SETTINGS]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsPage').default,
[SCREENS.WORKSPACE.TAXES_SETTINGS_CUSTOM_TAX_NAME]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName').default,
[SCREENS.WORKSPACE.TAXES_SETTINGS_FOREIGN_CURRENCY_DEFAULT]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency').default,
@@ -376,7 +379,6 @@ const SettingsModalStackNavigator = createModalStackNavigator
require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteApprovalAccountSelectPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_CUSTOM_FORM_ID]: () => require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage').default,
-
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: () => require('../../../../pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS]: () =>
require('../../../../pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage').default,
@@ -397,6 +399,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: () =>
require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: () => require('../../../../pages/workspace/accounting/reconciliation/CardReconciliationPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: () =>
+ require('../../../../pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default,
[SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../../pages/workspace/taxes/WorkspaceEditTaxPage').default,
@@ -404,18 +409,19 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/ValuePage').default,
[SCREENS.WORKSPACE.TAX_CREATE]: () => require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default,
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts').default,
[SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default,
[SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default,
[SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: () => require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default,
[SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard').default,
[SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/CreateReportFieldPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldSettingsPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reportFields/ReportFieldListValuesPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldAddListValuePage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldValueSettingsPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldInitialValuePage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldEditValuePage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/CreateReportFieldsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldsSettingsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reportFields/ReportFieldsListValuesPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsAddListValuePage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldsValueSettingsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsInitialValuePage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsEditValuePage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctImportPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING]: () =>
require('../../../../pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage').default,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/ActiveBottomTabRouteContext.ts b/src/libs/Navigation/AppNavigator/Navigators/ActiveBottomTabRouteContext.ts
new file mode 100644
index 000000000000..ce55da8e4bde
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/ActiveBottomTabRouteContext.ts
@@ -0,0 +1,6 @@
+import React from 'react';
+import type {BottomTabScreensParamList, NavigationPartialRoute} from '@libs/Navigation/types';
+
+const ActiveBottomTabRouteContext = React.createContext | undefined>(undefined);
+
+export default ActiveBottomTabRouteContext;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/ActiveRouteContext.ts b/src/libs/Navigation/AppNavigator/Navigators/ActiveRouteContext.ts
deleted file mode 100644
index c319aeca3e04..000000000000
--- a/src/libs/Navigation/AppNavigator/Navigators/ActiveRouteContext.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react';
-import type {AuthScreensParamList, NavigationPartialRoute} from '@libs/Navigation/types';
-
-const ActiveRouteContext = React.createContext | undefined>(undefined);
-
-export default ActiveRouteContext;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
index 46212f3bc41f..372c1ce478cc 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
@@ -2,13 +2,15 @@ import {useNavigationState} from '@react-navigation/native';
import type {StackNavigationOptions} from '@react-navigation/stack';
import React from 'react';
import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator';
+import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
-import type {BottomTabNavigatorParamList, CentralPaneName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types';
+import type {BottomTabNavigatorParamList, BottomTabScreensParamList, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types';
+import {isBottomTabName} from '@libs/NavigationUtils';
import SidebarScreen from '@pages/home/sidebar/SidebarScreen';
import SearchPageBottomTab from '@pages/Search/SearchPageBottomTab';
import SCREENS from '@src/SCREENS';
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
-import ActiveRouteContext from './ActiveRouteContext';
+import ActiveBottomTabRouteContext from './ActiveBottomTabRouteContext';
const loadInitialSettingsPage = () => require('../../../../pages/settings/InitialSettingsPage').default;
const Tab = createCustomBottomTabNavigator();
@@ -19,10 +21,22 @@ const screenOptions: StackNavigationOptions = {
};
function BottomTabNavigator() {
- const activeRoute = useNavigationState | undefined>(getTopmostCentralPaneRoute);
+ const activeRoute = useNavigationState | undefined>((state) => {
+ if (!state) {
+ return undefined;
+ }
+ let route: NavigationPartialRoute | undefined;
+ for (const selector of [getTopmostBottomTabRoute, getTopmostCentralPaneRoute]) {
+ const selectedRoute = selector(state);
+ if (isBottomTabName(selectedRoute?.name)) {
+ route = selectedRoute as NavigationPartialRoute;
+ }
+ }
+ return route;
+ });
return (
-
+
-
+
);
}
diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx
index 61adcd77da76..29a2205b2e37 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx
@@ -4,11 +4,9 @@ import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import NoDropZone from '@components/DragAndDrop/NoDropZone';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
-import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useOnboardingLayout from '@hooks/useOnboardingLayout';
import useThemeStyles from '@hooks/useThemeStyles';
-import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import OnboardingModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/OnboardingModalNavigatorScreenOptions';
import Navigation from '@libs/Navigation/Navigation';
import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types';
@@ -28,11 +26,15 @@ function OnboardingModalNavigator() {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useOnboardingLayout();
const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
- selector: hasCompletedGuidedSetupFlowSelector,
+ selector: (onboarding) => {
+ // onboarding is an array for old accounts and accounts created from olddot
+ if (Array.isArray(onboarding)) {
+ return true;
+ }
+ return onboarding?.hasCompletedGuidedSetupFlow;
+ },
});
- useDisableModalDismissOnEscape();
-
useEffect(() => {
if (!hasCompletedGuidedSetupFlow) {
return;
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx
index 2e1c4c012156..556365b473c3 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx
@@ -3,10 +3,12 @@ import React, {useCallback, useEffect} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
+import type {TupleToUnion} from 'type-fest';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import {PressableWithFeedback} from '@components/Pressable';
import Tooltip from '@components/Tooltip';
+import useActiveBottomTabRoute from '@hooks/useActiveBottomTabRoute';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
@@ -15,11 +17,9 @@ import * as Session from '@libs/actions/Session';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
-import linkingConfig from '@libs/Navigation/linkingConfig';
-import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
-import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
+import Navigation from '@libs/Navigation/Navigation';
import type {RootStackParamList, State} from '@libs/Navigation/types';
-import {isCentralPaneName} from '@libs/NavigationUtils';
+import {isCentralPaneName, isHomeTabName, isSearchTabName, isSettingTabName} from '@libs/NavigationUtils';
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar';
import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton';
@@ -42,6 +42,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
const styles = useThemeStyles();
const {translate} = useLocalize();
const navigation = useNavigation();
+ const HOME_SCREENS = [SCREENS.HOME, SCREENS.REPORT];
const {activeWorkspaceID: contextActiveWorkspaceID} = useActiveWorkspace();
const activeWorkspaceID = sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID) ?? contextActiveWorkspaceID;
@@ -55,12 +56,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
return;
}
- Welcome.isOnboardingFlowCompleted({
- onNotCompleted: () => {
- const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config);
- navigationRef.resetRoot(adaptedState);
- },
- });
+ Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_ROOT)});
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isLoadingApp]);
@@ -76,6 +72,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
return topmostBottomTabRoute?.name ?? SCREENS.HOME;
});
+ const activeBottomTabRoute = useActiveBottomTabRoute();
const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID);
const navigateToChats = useCallback(() => {
@@ -99,7 +96,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
) ? theme.iconMenu : theme.icon}
width={variables.iconBottomBar}
height={variables.iconBottomBar}
/>
@@ -112,7 +109,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
{
- if (currentTabName === SCREENS.SEARCH.BOTTOM_TAB || currentTabName === SCREENS.SEARCH.CENTRAL_PANE) {
+ if (isSearchTabName(activeBottomTabRoute?.name)) {
return;
}
interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL)));
@@ -125,14 +122,14 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
-
+
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts
index 5b3cefb63a2d..a1768df5e0d6 100644
--- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts
+++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts
@@ -1,16 +1,13 @@
-import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native';
-import {findFocusedRoute, getPathFromState, StackRouter} from '@react-navigation/native';
+import type {RouterConfigOptions, StackNavigationState} from '@react-navigation/native';
+import {getPathFromState, StackRouter} from '@react-navigation/native';
import type {ParamListBase} from '@react-navigation/routers';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
-import * as Localize from '@libs/Localize';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import linkingConfig from '@libs/Navigation/linkingConfig';
import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
-import {isCentralPaneName, isOnboardingFlowName} from '@libs/NavigationUtils';
-import * as Welcome from '@userActions/Welcome';
-import CONST from '@src/CONST';
+import {isCentralPaneName} from '@libs/NavigationUtils';
import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import type {ResponsiveStackNavigatorRouterOptions} from './types';
@@ -100,23 +97,6 @@ function compareAndAdaptState(state: StackNavigationState) {
}
}
-function shouldPreventReset(state: StackNavigationState, action: CommonActions.Action | StackActionType) {
- if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) {
- return false;
- }
- const currentFocusedRoute = findFocusedRoute(state);
- const targetFocusedRoute = findFocusedRoute(action?.payload);
-
- // We want to prevent the user from navigating back to a non-onboarding screen if they are currently on an onboarding screen
- if (isOnboardingFlowName(currentFocusedRoute?.name) && !isOnboardingFlowName(targetFocusedRoute?.name)) {
- Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
- // We reset the URL as the browser sets it in a way that doesn't match the navigation state
- // eslint-disable-next-line no-restricted-globals
- history.replaceState({}, '', getPathFromState(state, linkingConfig.config));
- return true;
- }
-}
-
function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) {
const stackRouter = StackRouter(options);
@@ -127,12 +107,6 @@ function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) {
const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList});
return state;
},
- getStateForAction(state: StackNavigationState, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) {
- if (shouldPreventReset(state, action)) {
- return state;
- }
- return stackRouter.getStateForAction(state, action, configOptions);
- },
};
}
diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx
index a225831b56ff..63792be4f79f 100644
--- a/src/libs/Navigation/NavigationRoot.tsx
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -1,7 +1,6 @@
import type {NavigationState} from '@react-navigation/native';
import {DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native';
import React, {useContext, useEffect, useMemo, useRef} from 'react';
-import {useOnyx} from 'react-native-onyx';
import HybridAppMiddleware from '@components/HybridAppMiddleware';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
@@ -9,14 +8,11 @@ import useCurrentReportID from '@hooks/useCurrentReportID';
import useTheme from '@hooks/useTheme';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {FSPage} from '@libs/Fullstory';
-import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import Log from '@libs/Log';
import {getPathFromURL} from '@libs/Url';
import {updateLastVisitedPath} from '@userActions/App';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
-import ROUTES from '@src/ROUTES';
import AppNavigator from './AppNavigator';
import getPolicyIDFromState from './getPolicyIDFromState';
import linkingConfig from './linkingConfig';
@@ -81,37 +77,25 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
const {isSmallScreenWidth} = useWindowDimensions();
const {setActiveWorkspaceID} = useActiveWorkspace();
- const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
- selector: hasCompletedGuidedSetupFlowSelector,
- });
+ const initialState = useMemo(
+ () => {
+ if (!lastVisitedPath) {
+ return undefined;
+ }
- const initialState = useMemo(() => {
- // If the user haven't completed the flow, we want to always redirect them to the onboarding flow.
- if (!hasCompletedGuidedSetupFlow) {
- const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config);
- return adaptedState;
- }
-
- // If there is no lastVisitedPath, we can do early return. We won't modify the default behavior.
- if (!lastVisitedPath) {
- return undefined;
- }
-
- const path = initialUrl ? getPathFromURL(initialUrl) : null;
+ const path = initialUrl ? getPathFromURL(initialUrl) : null;
- // If the user opens the root of app "/" it will be parsed to empty string "".
- // If the path is defined and different that empty string we don't want to modify the default behavior.
- if (path) {
- return;
- }
-
- // Otherwise we want to redirect the user to the last visited path.
- const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config);
- return adaptedState;
+ // For non-nullable paths we don't want to set initial state
+ if (path) {
+ return;
+ }
- // The initialState value is relevant only on the first render.
+ const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config);
+ return adaptedState;
+ },
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, []);
+ [],
+ );
// https://reactnavigation.org/docs/themes
const navigationTheme = useMemo(
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index acd12afd345f..1426305b8555 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -113,6 +113,8 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT,
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED,
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT,
+ SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION,
+ SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS,
],
[SCREENS.WORKSPACE.TAXES]: [
SCREENS.WORKSPACE.TAXES_SETTINGS,
@@ -132,8 +134,16 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.TAG_SETTINGS,
SCREENS.WORKSPACE.TAG_EDIT,
SCREENS.WORKSPACE.TAG_LIST_VIEW,
+ SCREENS.WORKSPACE.TAG_GL_CODE,
+ ],
+ [SCREENS.WORKSPACE.CATEGORIES]: [
+ SCREENS.WORKSPACE.CATEGORY_CREATE,
+ SCREENS.WORKSPACE.CATEGORY_SETTINGS,
+ SCREENS.WORKSPACE.CATEGORIES_SETTINGS,
+ SCREENS.WORKSPACE.CATEGORY_EDIT,
+ SCREENS.WORKSPACE.CATEGORY_GL_CODE,
+ SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE,
],
- [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS, SCREENS.WORKSPACE.CATEGORY_EDIT],
[SCREENS.WORKSPACE.DISTANCE_RATES]: [
SCREENS.WORKSPACE.CREATE_DISTANCE_RATE,
SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS,
@@ -144,14 +154,14 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
],
[SCREENS.WORKSPACE.REPORT_FIELDS]: [
SCREENS.WORKSPACE.REPORT_FIELDS_CREATE,
- SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS,
+ SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS,
SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES,
SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE,
SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS,
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE,
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
],
- [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW],
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT],
};
export default FULL_SCREEN_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index d5550a694c51..f9baa822fb89 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -448,6 +448,8 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.route},
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: {path: ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {path: ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
},
@@ -463,6 +465,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.route,
},
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
+ path: ROUTES.WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT.route,
+ },
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
path: ROUTES.WORKSPACE_RATE_AND_UNIT.route,
},
@@ -520,6 +525,18 @@ const config: LinkingOptions['config'] = {
categoryName: (categoryName: string) => decodeURIComponent(categoryName),
},
},
+ [SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE]: {
+ path: ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.route,
+ parse: {
+ categoryName: (categoryName: string) => decodeURIComponent(categoryName),
+ },
+ },
+ [SCREENS.WORKSPACE.CATEGORY_GL_CODE]: {
+ path: ROUTES.WORKSPACE_CATEGORY_GL_CODE.route,
+ parse: {
+ categoryName: (categoryName: string) => decodeURIComponent(categoryName),
+ },
+ },
[SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: {
path: ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.route,
},
@@ -557,6 +574,13 @@ const config: LinkingOptions['config'] = {
tagName: (tagName: string) => decodeURIComponent(tagName),
},
},
+ [SCREENS.WORKSPACE.TAG_GL_CODE]: {
+ path: ROUTES.WORKSPACE_TAG_GL_CODE.route,
+ parse: {
+ orderWeight: Number,
+ tagName: (tagName: string) => decodeURIComponent(tagName),
+ },
+ },
[SCREENS.WORKSPACE.TAG_SETTINGS]: {
path: ROUTES.WORKSPACE_TAG_SETTINGS.route,
parse: {
@@ -586,34 +610,34 @@ const config: LinkingOptions['config'] = {
path: ROUTES.WORKSPACE_CREATE_REPORT_FIELD.route,
},
[SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.route,
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
},
[SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_ADD_VALUE.route,
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_ADD_VALUE.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
},
[SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_VALUE_SETTINGS.route,
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
},
[SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_EDIT_VALUE.route,
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_EDIT_VALUE.route,
},
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: {
- path: ROUTES.WORKSPACE_REPORT_FIELD_SETTINGS.route,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: {
+ path: ROUTES.WORKSPACE_REPORT_FIELDS_SETTINGS.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
},
[SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: {
- path: ROUTES.WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE.route,
+ path: ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.route,
parse: {
reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID),
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 5b7098b78a26..08c484b5f72e 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -79,6 +79,12 @@ type CentralPaneScreensParamList = {
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined;
};
+type SearchNavigatorParamList = {
+ [SCREENS.SEARCH.BOTTOM_TAB]: undefined;
+ [SCREENS.SEARCH.CENTRAL_PANE]: undefined;
+ [SCREENS.SEARCH.REPORT_RHP]: undefined;
+};
+
type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.SHARE_CODE]: undefined;
[SCREENS.SETTINGS.PROFILE.ROOT]: undefined;
@@ -201,6 +207,14 @@ type SettingsNavigatorParamList = {
categoryName: string;
backTo?: Routes;
};
+ [SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE]: {
+ policyID: string;
+ categoryName: string;
+ };
+ [SCREENS.WORKSPACE.CATEGORY_GL_CODE]: {
+ policyID: string;
+ categoryName: string;
+ };
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: {
policyID: string;
categoryName: string;
@@ -255,6 +269,11 @@ type SettingsNavigatorParamList = {
orderWeight: number;
tagName: string;
};
+ [SCREENS.WORKSPACE.TAG_GL_CODE]: {
+ policyID: string;
+ orderWeight: number;
+ tagName: string;
+ };
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined;
[SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: {
canChangeSize: 0 | 1;
@@ -294,7 +313,7 @@ type SettingsNavigatorParamList = {
policyID: string;
valueIndex: number;
};
- [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: {
+ [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: {
policyID: string;
reportFieldID: string;
};
@@ -591,6 +610,14 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: {
+ policyID: string;
+ connection: ValueOf;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {
+ policyID: string;
+ connection: ValueOf;
+ };
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
@@ -626,6 +653,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
+ policyID: string;
+ };
} & ReimbursementAccountNavigatorParamList;
type NewChatNavigatorParamList = {
@@ -1109,6 +1139,8 @@ type ExplanationModalNavigatorParamList = {
[SCREENS.EXPLANATION_MODAL.ROOT]: undefined;
};
+type BottomTabScreensParamList = {[SCREENS.HOME]: undefined; [SCREENS.REPORT]: undefined} & SearchNavigatorParamList & SettingsNavigatorParamList;
+
type BottomTabNavigatorParamList = {
[SCREENS.HOME]: {policyID?: string};
[SCREENS.SEARCH.BOTTOM_TAB]: {
@@ -1207,12 +1239,12 @@ type RootStackParamList = PublicScreensParamList & AuthScreensParamList & LeftMo
type BottomTabName = keyof BottomTabNavigatorParamList;
+type BottomTabScreenName = keyof BottomTabScreensParamList;
+
type FullScreenName = keyof FullScreenNavigatorParamList;
type CentralPaneName = keyof CentralPaneScreensParamList;
-type OnboardingFlowName = keyof OnboardingModalNavigatorParamList;
-
type SwitchPolicyIDParams = {
policyID?: string;
route?: Routes;
@@ -1226,6 +1258,8 @@ export type {
CentralPaneName,
BackToParams,
BottomTabName,
+ BottomTabScreenName,
+ BottomTabScreensParamList,
BottomTabNavigatorParamList,
DetailsNavigatorParamList,
EditRequestNavigatorParamList,
@@ -1243,7 +1277,6 @@ export type {
NewChatNavigatorParamList,
NewTaskNavigatorParamList,
OnboardingModalNavigatorParamList,
- OnboardingFlowName,
ParticipantsNavigatorParamList,
PrivateNotesNavigatorParamList,
ProfileNavigatorParamList,
@@ -1257,6 +1290,7 @@ export type {
RoomInviteNavigatorParamList,
RoomMembersNavigatorParamList,
RootStackParamList,
+ SearchNavigatorParamList,
SettingsNavigatorParamList,
SignInNavigatorParamList,
FeatureTrainingNavigatorParamList,
diff --git a/src/libs/NavigationUtils.ts b/src/libs/NavigationUtils.ts
index aa26268977a2..34e9df954688 100644
--- a/src/libs/NavigationUtils.ts
+++ b/src/libs/NavigationUtils.ts
@@ -1,7 +1,9 @@
import cloneDeep from 'lodash/cloneDeep';
+import type {TupleToUnion} from 'type-fest';
+import {flattenObject} from '@src/languages/translations';
import SCREENS from '@src/SCREENS';
import getTopmostBottomTabRoute from './Navigation/getTopmostBottomTabRoute';
-import type {CentralPaneName, OnboardingFlowName, RootStackParamList, State} from './Navigation/types';
+import type {CentralPaneName, RootStackParamList, State} from './Navigation/types';
const CENTRAL_PANE_SCREEN_NAMES = new Set([
SCREENS.SETTINGS.WORKSPACES,
@@ -17,8 +19,6 @@ const CENTRAL_PANE_SCREEN_NAMES = new Set([
SCREENS.REPORT,
]);
-const ONBOARDING_SCREEN_NAMES = new Set([SCREENS.ONBOARDING.PERSONAL_DETAILS, SCREENS.ONBOARDING.PURPOSE, SCREENS.ONBOARDING.WORK, SCREENS.ONBOARDING_MODAL.ONBOARDING]);
-
function isCentralPaneName(screen: string | undefined): screen is CentralPaneName {
if (!screen) {
return false;
@@ -27,14 +27,6 @@ function isCentralPaneName(screen: string | undefined): screen is CentralPaneNam
return CENTRAL_PANE_SCREEN_NAMES.has(screen as CentralPaneName);
}
-function isOnboardingFlowName(screen: string | undefined): screen is OnboardingFlowName {
- if (!screen) {
- return false;
- }
-
- return ONBOARDING_SCREEN_NAMES.has(screen as OnboardingFlowName);
-}
-
const removePolicyIDParamFromState = (state: State) => {
const stateCopy = cloneDeep(state);
const bottomTabRoute = getTopmostBottomTabRoute(stateCopy);
@@ -44,4 +36,43 @@ const removePolicyIDParamFromState = (state: State) => {
return stateCopy;
};
-export {isCentralPaneName, removePolicyIDParamFromState, isOnboardingFlowName};
+const SETTINGS_SCREENS = Object.values(flattenObject(SCREENS.SETTINGS));
+const SEARCH_SCREENS = Object.values(flattenObject(SCREENS.SEARCH));
+const HOME_SCREENS = [SCREENS.HOME, SCREENS.REPORT];
+const BOTTOM_TAB_SCREEN_NAMES = new Set([...SETTINGS_SCREENS, ...SEARCH_SCREENS, ...HOME_SCREENS]);
+
+const SETTINGS_TAB_SCREEN_NAMES = new Set(SETTINGS_SCREENS);
+
+const SEARCH_TAB_SCREEN_NAMES = new Set(SEARCH_SCREENS);
+
+const HOME_SCREEN_NAMES = new Set(HOME_SCREENS);
+
+function isBottomTabName(screen: TupleToUnion | undefined) {
+ if (!screen) {
+ return false;
+ }
+ return BOTTOM_TAB_SCREEN_NAMES.has(screen);
+}
+
+function isSettingTabName(screen: TupleToUnion | undefined) {
+ if (!screen) {
+ return false;
+ }
+ return SETTINGS_TAB_SCREEN_NAMES.has(screen);
+}
+
+function isSearchTabName(screen: TupleToUnion | undefined) {
+ if (!screen) {
+ return false;
+ }
+ return SEARCH_TAB_SCREEN_NAMES.has(screen);
+}
+
+function isHomeTabName(screen: TupleToUnion | undefined) {
+ if (!screen) {
+ return false;
+ }
+ return HOME_SCREEN_NAMES.has(screen);
+}
+
+export {isCentralPaneName, isBottomTabName, isSearchTabName, isSettingTabName, isHomeTabName, removePolicyIDParamFromState};
diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts
index acdb982c729a..bc1b82b21bc4 100644
--- a/src/libs/NetworkConnection.ts
+++ b/src/libs/NetworkConnection.ts
@@ -11,7 +11,6 @@ import AppStateMonitor from './AppStateMonitor';
import Log from './Log';
let isOffline = false;
-let hasPendingNetworkCheck = false;
type NetworkStatus = ValueOf;
type ResponseJSON = {
@@ -191,13 +190,8 @@ function clearReconnectionCallbacks() {
* Refresh NetInfo state.
*/
function recheckNetworkConnection() {
- if (hasPendingNetworkCheck) {
- return;
- }
-
Log.info('[NetworkConnection] recheck NetInfo');
- hasPendingNetworkCheck = true;
- NetInfo.refresh().finally(() => (hasPendingNetworkCheck = false));
+ NetInfo.refresh();
}
export default {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 330d9d6ef61d..7337ab4dc359 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -657,10 +657,11 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV
* Get the last message text from the report directly or from other sources for special cases.
*/
function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails: Partial | null, policy?: OnyxEntry): string {
- const lastReportAction = visibleReportActionItems[report?.reportID ?? '-1'] ?? null;
+ const reportID = report?.reportID ?? '-1';
+ const lastReportAction = visibleReportActionItems[reportID] ?? null;
// some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action
- const lastOriginalReportAction = lastReportActions[report?.reportID ?? '-1'] ?? null;
+ const lastOriginalReportAction = lastReportActions[reportID] ?? null;
let lastMessageTextFromReport = '';
if (ReportUtils.isArchivedRoom(report)) {
@@ -720,8 +721,14 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastReportAction).text);
} else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) {
lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction);
- } else if (ReportActionUtils.isApprovedOrSubmittedReportAction(lastReportAction)) {
+ } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
+ lastMessageTextFromReport = ReportUtils.getIOUSubmittedMessage(reportID);
+ } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
+ lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(reportID);
+ } else if (ReportActionUtils.isActionableAddPaymentCard(lastReportAction)) {
lastMessageTextFromReport = ReportActionUtils.getReportActionMessageText(lastReportAction);
+ } else if (lastReportAction?.actionName && ReportActionUtils.isOldDotReportAction(lastReportAction)) {
+ lastMessageTextFromReport = ReportActionUtils.getMessageOfOldDotReportAction(lastReportAction);
}
return lastMessageTextFromReport || (report?.lastMessageText ?? '');
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index faea5965fee4..23aabcf3c6d0 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -15,10 +15,6 @@ function canUseDefaultRooms(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas);
}
-function canUseReportFields(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas);
-}
-
function canUseViolations(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas);
}
@@ -73,7 +69,6 @@ export default {
canUseLinkPreviews,
canUseViolations,
canUseDupeDetection,
- canUseReportFields,
canUseP2PDistanceRequests,
canUseWorkflowsDelayedSubmission,
canUseSpotnanaTravel,
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index f13274a5c4cf..a51388cae5b6 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -318,6 +318,10 @@ function isPaidGroupPolicy(policy: OnyxEntry): boolean {
return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE;
}
+function isControlPolicy(policy: OnyxEntry): boolean {
+ return policy?.type === CONST.POLICY.TYPE.CORPORATE;
+}
+
function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry, isDistanceRequest: boolean): boolean {
const distanceUnit = getCustomUnit(policy);
const customUnitID = distanceUnit?.customUnitID ?? 0;
@@ -812,6 +816,7 @@ export {
getIntegrationLastSuccessfulDate,
getCurrentConnectionName,
getCustomersOrJobsLabelNetSuite,
+ isControlPolicy,
isNetSuiteCustomSegmentRecord,
getNameFromNetSuiteCustomField,
isNetSuiteCustomFieldPropertyEditable,
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 3f8acd0e06fe..01ca1c46b1fa 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -260,6 +260,18 @@ function isRoomChangeLogAction(reportAction: OnyxEntry): reportAct
return isActionOfType(reportAction, ...Object.values(CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG));
}
+function isInviteOrRemovedAction(
+ reportAction: OnyxInputOrEntry,
+): reportAction is ReportAction> {
+ return isActionOfType(
+ reportAction,
+ CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.INVITE_TO_ROOM,
+ CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.REMOVE_FROM_ROOM,
+ CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INVITE_TO_ROOM,
+ CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_FROM_ROOM,
+ );
+}
+
/**
* Returns whether the comment is a thread parent message/the first message in a thread
*/
@@ -1396,6 +1408,14 @@ function getTrackExpenseActionableWhisper(transactionID: string, chatReportID: s
return Object.values(chatReportActions).find((action: ReportAction) => isActionableTrackExpense(action) && getOriginalMessage(action)?.transactionID === transactionID);
}
+/**
+ * Checks if a given report action corresponds to a add payment card action.
+ * @param reportAction
+ */
+function isActionableAddPaymentCard(reportAction: OnyxEntry): reportAction is ReportAction {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_ADD_PAYMENT_CARD;
+}
+
export {
extractLinksFromMessageHtml,
getDismissedViolationMessageText,
@@ -1474,6 +1494,7 @@ export {
isClosedAction,
isRenamedAction,
isRoomChangeLogAction,
+ isInviteOrRemovedAction,
isChronosOOOListAction,
isAddCommentAction,
isPolicyChangeLogAction,
@@ -1481,6 +1502,7 @@ export {
isTripPreview,
getIOUActionForReportID,
getFilteredForOneTransactionView,
+ isActionableAddPaymentCard,
};
export type {LastVisibleMessage};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index fb3bebd75274..8f1e3c4edc35 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -40,6 +40,7 @@ import type {
UserWallet,
} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
+import type Onboarding from '@src/types/onyx/Onboarding';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {Status} from '@src/types/onyx/PersonalDetails';
@@ -52,6 +53,7 @@ import AccountUtils from './AccountUtils';
import * as IOU from './actions/IOU';
import * as PolicyActions from './actions/Policy/Policy';
import * as store from './actions/ReimbursementAccount/store';
+import * as SessionUtils from './actions/Session';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import {hasValidDraftComment} from './DraftCommentUtils';
@@ -81,8 +83,6 @@ import * as UserUtils from './UserUtils';
type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18;
-type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string};
-
type SpendBreakdown = {
nonReimbursableSpend: number;
reimbursableSpend: number;
@@ -437,6 +437,7 @@ type OptionData = {
shouldShowAmountInput?: boolean;
amountInputProps?: MoneyRequestAmountInputProps;
tabIndex?: 0 | -1;
+ isConciergeChat?: boolean;
} & Report;
type OnyxDataTaskAssigneeChat = {
@@ -571,6 +572,12 @@ Onyx.connect({
},
});
+let onboarding: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.NVP_ONBOARDING,
+ callback: (value) => (onboarding = value),
+});
+
function getCurrentUserAvatar(): AvatarSource | undefined {
return currentUserPersonalDetails?.avatar;
}
@@ -607,6 +614,13 @@ function isDraftReport(reportID: string | undefined): boolean {
return !!draftReport;
}
+/**
+ * Returns the report
+ */
+function getReport(reportID: string): OnyxEntry {
+ return ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+}
+
/**
* Returns the parentReport if the given report is a thread
*/
@@ -1065,7 +1079,7 @@ function isSystemChat(report: OnyxEntry): boolean {
* Only returns true if this is our main 1:1 DM report with Concierge.
*/
function isConciergeChatReport(report: OnyxInputOrEntry): boolean {
- const participantAccountIDs = Object.keys(report?.participants ?? {});
+ const participantAccountIDs = Object.keys(report?.participants ?? {}).filter((accountID) => Number(accountID) !== currentUserAccountID);
return participantAccountIDs.length === 1 && Number(participantAccountIDs[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report);
}
@@ -1254,7 +1268,7 @@ function isClosedExpenseReportWithNoExpenses(report: OnyxEntry): boolean
*/
function isArchivedRoom(report: OnyxInputOrEntry, reportNameValuePairs?: OnyxInputOrEntry): boolean {
if (reportNameValuePairs) {
- return reportNameValuePairs.isArchived;
+ return reportNameValuePairs.private_isArchived;
}
return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED;
@@ -1471,10 +1485,10 @@ function isOneTransactionReport(reportID: string): boolean {
/**
* Checks if a report is a transaction thread associated with a report that has only one transaction
*/
-function isOneTransactionThread(reportID: string, parentReportID: string): boolean {
+function isOneTransactionThread(reportID: string, parentReportID: string, threadParentReportAction: OnyxEntry): boolean {
const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]);
const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportID, parentReportActions);
- return reportID === transactionThreadReportID;
+ return reportID === transactionThreadReportID && !ReportActionsUtils.isSentMoneyReportAction(threadParentReportAction);
}
/**
@@ -1616,37 +1630,6 @@ function canDeleteReportAction(reportAction: OnyxInputOrEntry, rep
return isActionOwner || isAdmin;
}
-/**
- * Get welcome message based on room type
- */
-function getRoomWelcomeMessage(report: OnyxEntry): WelcomeMessage {
- const welcomeMessage: WelcomeMessage = {showReportName: true};
- const workspaceName = getPolicyName(report);
-
- if (isArchivedRoom(report)) {
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartOne');
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartTwo');
- } else if (isDomainRoom(report)) {
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report?.reportName ?? ''});
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo');
- } else if (isAdminRoom(report)) {
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName});
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo');
- } else if (isAnnounceRoom(report)) {
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName});
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName});
- } else if (isInvoiceRoom(report)) {
- welcomeMessage.showReportName = false;
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom');
- } else {
- // Message for user created rooms or other room types.
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartOne');
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartTwo');
- }
-
- return welcomeMessage;
-}
-
/**
* Returns true if Concierge is one of the chat participants (1:1 as well as group chats)
*/
@@ -2478,13 +2461,6 @@ function isHoldCreator(transaction: OnyxEntry, reportID: string): b
return isActionCreator(holdReportAction);
}
-/**
- * Check if report fields are available to use in a report
- */
-function reportFieldsEnabled(report: Report) {
- return Permissions.canUseReportFields(allBetas ?? []) && isPaidGroupPolicyExpenseReport(report);
-}
-
/**
* Given a report field, check if the field can be edited or not.
* For title fields, its considered disabled if `deletable` prop is `true` (https://github.com/Expensify/App/issues/35043#issuecomment-1911275433)
@@ -2586,7 +2562,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry
const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID ?? '-1');
const titleReportField = getFormulaTypeReportField(reportFields ?? {});
- if (titleReportField && report?.reportName && reportFieldsEnabled(report)) {
+ if (titleReportField && report?.reportName && isPaidGroupPolicyExpenseReport(report)) {
return report.reportName;
}
@@ -2829,7 +2805,9 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry)
const canHoldOrUnholdRequest = !isRequestSettled && !isApproved && !isDeletedParentAction;
const canHoldRequest = canHoldOrUnholdRequest && !isOnHold && (isRequestHoldCreator || (!isRequestIOU && canModifyStatus)) && !isScanning && !!transaction?.reimbursable;
- const canUnholdRequest = !!(canHoldOrUnholdRequest && isOnHold && (isRequestHoldCreator || (!isRequestIOU && canModifyStatus))) && !!transaction?.reimbursable;
+ const canUnholdRequest =
+ !!(canHoldOrUnholdRequest && isOnHold && !TransactionUtils.isDuplicate(transaction.transactionID, true) && (isRequestHoldCreator || (!isRequestIOU && canModifyStatus))) &&
+ !!transaction?.reimbursable;
return {canHoldRequest, canUnholdRequest};
}
@@ -3995,18 +3973,26 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa
}
const titleReportField = getTitleReportField(getReportFieldsByPolicyID(policyID) ?? {});
- if (!!titleReportField && reportFieldsEnabled(expenseReport)) {
+ if (!!titleReportField && isPaidGroupPolicyExpenseReport(expenseReport)) {
expenseReport.reportName = populateOptimisticReportFormula(titleReportField.defaultValue, expenseReport, policy);
}
return expenseReport;
}
-function getIOUSubmittedMessage(reportID: string) {
+function getFormattedAmount(reportID: string) {
const report = getReportOrDraftReport(reportID);
const linkedReport = isChatThread(report) ? getParentReport(report) : report;
const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(linkedReport?.total ?? 0), linkedReport?.currency);
- return Localize.translateLocal('iou.submittedAmount', {formattedAmount});
+ return formattedAmount;
+}
+
+function getIOUSubmittedMessage(reportID: string) {
+ return Localize.translateLocal('iou.submittedAmount', {formattedAmount: getFormattedAmount(reportID)});
+}
+
+function getIOUApprovedMessage(reportID: string) {
+ return Localize.translateLocal('iou.approvedAmount', {amount: getFormattedAmount(reportID)});
}
/**
@@ -5092,7 +5078,7 @@ function buildOptimisticTaskReport(
title?: string,
description?: string,
policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE,
- notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
+ notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
): OptimisticTaskReport {
const participants: Participants = {
[ownerAccountID]: {
@@ -5417,6 +5403,8 @@ function shouldReportBeInOptionList({
// This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy.
// Optionally exclude reports that do not belong to currently active workspace
+ const parentReportAction = ReportActionsUtils.getParentReportAction(report);
+
if (
!report?.reportID ||
!report?.type ||
@@ -5447,7 +5435,11 @@ function shouldReportBeInOptionList({
}
// If this is a transaction thread associated with a report that only has one transaction, omit it
- if (isOneTransactionThread(report.reportID, report.parentReportID ?? '-1')) {
+ if (isOneTransactionThread(report.reportID, report.parentReportID ?? '-1', parentReportAction)) {
+ return false;
+ }
+
+ if (report?.type === CONST.REPORT.TYPE.PAYCHECK || report?.type === CONST.REPORT.TYPE.BILL) {
return false;
}
@@ -5516,8 +5508,6 @@ function shouldReportBeInOptionList({
return false;
}
- const parentReportAction = ReportActionsUtils.getParentReportAction(report);
-
// Hide chat threads where the parent message is pending removal
if (
!isEmptyObject(parentReportAction) &&
@@ -6922,6 +6912,10 @@ function canJoinChat(report: OnyxInputOrEntry, parentReportAction: OnyxI
* Whether the user can leave a report
*/
function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boolean {
+ if (isPublicRoom(report) && SessionUtils.isAnonymousUser()) {
+ return false;
+ }
+
if (report?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) {
return false;
}
@@ -7060,10 +7054,18 @@ function shouldShowMerchantColumn(transactions: Transaction[]) {
}
/**
- * Whether the report is a system chat or concierge chat, depending on the user's account ID (used for A/B testing purposes).
+ * Whether the report is a system chat or concierge chat, depending on the onboarding report ID or fallbacking
+ * to the user's account ID (used for A/B testing purposes).
*/
-function isChatUsedForOnboarding(report: OnyxEntry): boolean {
- return AccountUtils.isAccountIDOddNumber(currentUserAccountID ?? -1) ? isSystemChat(report) : isConciergeChatReport(report);
+function isChatUsedForOnboarding(optionOrReport: OnyxEntry | OptionData): boolean {
+ // onboarding can be an array for old accounts and accounts created from olddot
+ if (!Array.isArray(onboarding) && onboarding?.chatReportID === optionOrReport?.reportID) {
+ return true;
+ }
+
+ return AccountUtils.isAccountIDOddNumber(currentUserAccountID ?? -1)
+ ? isSystemChat(optionOrReport)
+ : (optionOrReport as OptionData).isConciergeChat ?? isConciergeChatReport(optionOrReport);
}
/**
@@ -7167,6 +7169,7 @@ export {
getGroupChatName,
getIOUReportActionDisplayMessage,
getIOUReportActionMessage,
+ getIOUApprovedMessage,
getIOUSubmittedMessage,
getIcons,
getIconsForParticipants,
@@ -7202,7 +7205,6 @@ export {
getReportPreviewMessage,
getReportRecipientAccountIDs,
getRoom,
- getRoomWelcomeMessage,
getRootParentReport,
getRouteFromLink,
getSystemChat,
@@ -7275,6 +7277,7 @@ export {
isIOUReport,
isIOUReportUsingReport,
isJoinRequestInAdminRoom,
+ isDomainRoom,
isMoneyRequest,
isMoneyRequestReport,
isMoneyRequestReportPendingDeletion,
@@ -7325,7 +7328,6 @@ export {
navigateBackAfterDeleteTransaction,
parseReportRouteParams,
parseReportActionHtmlToText,
- reportFieldsEnabled,
requiresAttentionFromCurrentUser,
shouldAutoFocusOnKeyPress,
shouldCreateNewMoneyRequestReport,
@@ -7357,6 +7359,7 @@ export {
findPolicyExpenseChatByPolicyID,
hasOnlyNonReimbursableTransactions,
getMostRecentlyVisitedReport,
+ getReport,
};
export type {
diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js
new file mode 100644
index 000000000000..28143edb40f7
--- /dev/null
+++ b/src/libs/SearchParser/searchParser.js
@@ -0,0 +1,1161 @@
+// @generated by Peggy 4.0.3.
+//
+// https://peggyjs.org/
+
+
+function peg$subclass(child, parent) {
+ function C() { this.constructor = child; }
+ C.prototype = parent.prototype;
+ child.prototype = new C();
+}
+
+function peg$SyntaxError(message, expected, found, location) {
+ var self = Error.call(this, message);
+ // istanbul ignore next Check is a necessary evil to support older environments
+ if (Object.setPrototypeOf) {
+ Object.setPrototypeOf(self, peg$SyntaxError.prototype);
+ }
+ self.expected = expected;
+ self.found = found;
+ self.location = location;
+ self.name = "SyntaxError";
+ return self;
+}
+
+peg$subclass(peg$SyntaxError, Error);
+
+function peg$padEnd(str, targetLength, padString) {
+ padString = padString || " ";
+ if (str.length > targetLength) { return str; }
+ targetLength -= str.length;
+ padString += padString.repeat(targetLength);
+ return str + padString.slice(0, targetLength);
+}
+
+peg$SyntaxError.prototype.format = function(sources) {
+ var str = "Error: " + this.message;
+ if (this.location) {
+ var src = null;
+ var k;
+ for (k = 0; k < sources.length; k++) {
+ if (sources[k].source === this.location.source) {
+ src = sources[k].text.split(/\r\n|\n|\r/g);
+ break;
+ }
+ }
+ var s = this.location.start;
+ var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
+ ? this.location.source.offset(s)
+ : s;
+ var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
+ if (src) {
+ var e = this.location.end;
+ var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
+ var line = src[s.line - 1];
+ var last = s.line === e.line ? e.column : line.length + 1;
+ var hatLen = (last - s.column) || 1;
+ str += "\n --> " + loc + "\n"
+ + filler + " |\n"
+ + offset_s.line + " | " + line + "\n"
+ + filler + " | " + peg$padEnd("", s.column - 1, ' ')
+ + peg$padEnd("", hatLen, "^");
+ } else {
+ str += "\n at " + loc;
+ }
+ }
+ return str;
+};
+
+peg$SyntaxError.buildMessage = function(expected, found) {
+ var DESCRIBE_EXPECTATION_FNS = {
+ literal: function(expectation) {
+ return "\"" + literalEscape(expectation.text) + "\"";
+ },
+
+ class: function(expectation) {
+ var escapedParts = expectation.parts.map(function(part) {
+ return Array.isArray(part)
+ ? classEscape(part[0]) + "-" + classEscape(part[1])
+ : classEscape(part);
+ });
+
+ return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]";
+ },
+
+ any: function() {
+ return "any character";
+ },
+
+ end: function() {
+ return "end of input";
+ },
+
+ other: function(expectation) {
+ return expectation.description;
+ }
+ };
+
+ function hex(ch) {
+ return ch.charCodeAt(0).toString(16).toUpperCase();
+ }
+
+ function literalEscape(s) {
+ return s
+ .replace(/\\/g, "\\\\")
+ .replace(/"/g, "\\\"")
+ .replace(/\0/g, "\\0")
+ .replace(/\t/g, "\\t")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); })
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); });
+ }
+
+ function classEscape(s) {
+ return s
+ .replace(/\\/g, "\\\\")
+ .replace(/\]/g, "\\]")
+ .replace(/\^/g, "\\^")
+ .replace(/-/g, "\\-")
+ .replace(/\0/g, "\\0")
+ .replace(/\t/g, "\\t")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); })
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); });
+ }
+
+ function describeExpectation(expectation) {
+ return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
+ }
+
+ function describeExpected(expected) {
+ var descriptions = expected.map(describeExpectation);
+ var i, j;
+
+ descriptions.sort();
+
+ if (descriptions.length > 0) {
+ for (i = 1, j = 1; i < descriptions.length; i++) {
+ if (descriptions[i - 1] !== descriptions[i]) {
+ descriptions[j] = descriptions[i];
+ j++;
+ }
+ }
+ descriptions.length = j;
+ }
+
+ switch (descriptions.length) {
+ case 1:
+ return descriptions[0];
+
+ case 2:
+ return descriptions[0] + " or " + descriptions[1];
+
+ default:
+ return descriptions.slice(0, -1).join(", ")
+ + ", or "
+ + descriptions[descriptions.length - 1];
+ }
+ }
+
+ function describeFound(found) {
+ return found ? "\"" + literalEscape(found) + "\"" : "end of input";
+ }
+
+ return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
+};
+
+function peg$parse(input, options) {
+ options = options !== undefined ? options : {};
+
+ var peg$FAILED = {};
+ var peg$source = options.grammarSource;
+
+ var peg$startRuleFunctions = { query: peg$parsequery };
+ var peg$startRuleFunction = peg$parsequery;
+
+ var peg$c0 = "!=";
+ var peg$c1 = ">";
+ var peg$c2 = ">=";
+ var peg$c3 = "<";
+ var peg$c4 = "<=";
+ var peg$c5 = "type";
+ var peg$c6 = "status";
+ var peg$c7 = "date";
+ var peg$c8 = "amount";
+ var peg$c9 = "expenseType";
+ var peg$c10 = "in";
+ var peg$c11 = "currency";
+ var peg$c12 = "merchant";
+ var peg$c13 = "description";
+ var peg$c14 = "from";
+ var peg$c15 = "to";
+ var peg$c16 = "category";
+ var peg$c17 = "tag";
+ var peg$c18 = "taxRate";
+ var peg$c19 = "card";
+ var peg$c20 = "reportID";
+ var peg$c21 = "keyword";
+ var peg$c22 = "sortBy";
+ var peg$c23 = "sortOrder";
+ var peg$c24 = "offset";
+ var peg$c25 = "\"";
+
+ var peg$r0 = /^[:=]/;
+ var peg$r1 = /^[^"\r\n]/;
+ var peg$r2 = /^[A-Za-z0-9_@.\/#&+\-\\',]/;
+ var peg$r3 = /^[ \t\r\n]/;
+
+ var peg$e0 = peg$classExpectation([":", "="], false, false);
+ var peg$e1 = peg$literalExpectation("!=", false);
+ var peg$e2 = peg$literalExpectation(">", false);
+ var peg$e3 = peg$literalExpectation(">=", false);
+ var peg$e4 = peg$literalExpectation("<", false);
+ var peg$e5 = peg$literalExpectation("<=", false);
+ var peg$e6 = peg$literalExpectation("type", false);
+ var peg$e7 = peg$literalExpectation("status", false);
+ var peg$e8 = peg$literalExpectation("date", false);
+ var peg$e9 = peg$literalExpectation("amount", false);
+ var peg$e10 = peg$literalExpectation("expenseType", false);
+ var peg$e11 = peg$literalExpectation("in", false);
+ var peg$e12 = peg$literalExpectation("currency", false);
+ var peg$e13 = peg$literalExpectation("merchant", false);
+ var peg$e14 = peg$literalExpectation("description", false);
+ var peg$e15 = peg$literalExpectation("from", false);
+ var peg$e16 = peg$literalExpectation("to", false);
+ var peg$e17 = peg$literalExpectation("category", false);
+ var peg$e18 = peg$literalExpectation("tag", false);
+ var peg$e19 = peg$literalExpectation("taxRate", false);
+ var peg$e20 = peg$literalExpectation("card", false);
+ var peg$e21 = peg$literalExpectation("reportID", false);
+ var peg$e22 = peg$literalExpectation("keyword", false);
+ var peg$e23 = peg$literalExpectation("sortBy", false);
+ var peg$e24 = peg$literalExpectation("sortOrder", false);
+ var peg$e25 = peg$literalExpectation("offset", false);
+ var peg$e26 = peg$literalExpectation("\"", false);
+ var peg$e27 = peg$classExpectation(["\"", "\r", "\n"], true, false);
+ var peg$e28 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ","], false, false);
+ var peg$e29 = peg$otherExpectation("whitespace");
+ var peg$e30 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false);
+
+ var peg$f0 = function(filters) { return applyDefaults(filters); };
+ var peg$f1 = function(head, tail) {
+ const allFilters = [head, ...tail.map(([_, filter]) => filter)].filter(filter => filter !== null);
+ if (!allFilters.length) {
+ return null;
+ }
+ return allFilters.reduce((result, filter) => buildFilter("and", result, filter));
+ };
+ var peg$f2 = function(field, op, value) {
+ if (isDefaultField(field)) {
+ updateDefaultValues(field, value.trim());
+ return null;
+ }
+
+ if (!field && !op) {
+ return buildFilter('eq', 'keyword', value.trim());
+ }
+
+ const values = value.split(',');
+ const operatorValue = op ?? 'eq';
+
+ return values.slice(1).reduce((acc, val) => buildFilter('or', acc, buildFilter(operatorValue, field, val.trim())), buildFilter(operatorValue, field, values[0]));
+ };
+ var peg$f3 = function() { return "eq"; };
+ var peg$f4 = function() { return "neq"; };
+ var peg$f5 = function() { return "gt"; };
+ var peg$f6 = function() { return "gte"; };
+ var peg$f7 = function() { return "lt"; };
+ var peg$f8 = function() { return "lte"; };
+ var peg$f9 = function() { return "type"; };
+ var peg$f10 = function() { return "status"; };
+ var peg$f11 = function() { return "date"; };
+ var peg$f12 = function() { return "amount"; };
+ var peg$f13 = function() { return "expenseType"; };
+ var peg$f14 = function() { return "in"; };
+ var peg$f15 = function() { return "currency"; };
+ var peg$f16 = function() { return "merchant"; };
+ var peg$f17 = function() { return "description"; };
+ var peg$f18 = function() { return "from"; };
+ var peg$f19 = function() { return "to"; };
+ var peg$f20 = function() { return "category"; };
+ var peg$f21 = function() { return "tag"; };
+ var peg$f22 = function() { return "taxRate"; };
+ var peg$f23 = function() { return "card"; };
+ var peg$f24 = function() { return "reportID"; };
+ var peg$f25 = function() { return "keyword"; };
+ var peg$f26 = function() { return "sortBy"; };
+ var peg$f27 = function() { return "sortOrder"; };
+ var peg$f28 = function() { return "offset"; };
+ var peg$f29 = function(parts) { return parts.join(''); };
+ var peg$f30 = function(chars) { return chars.join(''); };
+ var peg$f31 = function(chars) { return chars.join(''); };
+ var peg$f32 = function() { return "and"; };
+ var peg$currPos = options.peg$currPos | 0;
+ var peg$savedPos = peg$currPos;
+ var peg$posDetailsCache = [{ line: 1, column: 1 }];
+ var peg$maxFailPos = peg$currPos;
+ var peg$maxFailExpected = options.peg$maxFailExpected || [];
+ var peg$silentFails = options.peg$silentFails | 0;
+
+ var peg$result;
+
+ if (options.startRule) {
+ if (!(options.startRule in peg$startRuleFunctions)) {
+ throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
+ }
+
+ peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
+ }
+
+ function text() {
+ return input.substring(peg$savedPos, peg$currPos);
+ }
+
+ function offset() {
+ return peg$savedPos;
+ }
+
+ function range() {
+ return {
+ source: peg$source,
+ start: peg$savedPos,
+ end: peg$currPos
+ };
+ }
+
+ function location() {
+ return peg$computeLocation(peg$savedPos, peg$currPos);
+ }
+
+ function expected(description, location) {
+ location = location !== undefined
+ ? location
+ : peg$computeLocation(peg$savedPos, peg$currPos);
+
+ throw peg$buildStructuredError(
+ [peg$otherExpectation(description)],
+ input.substring(peg$savedPos, peg$currPos),
+ location
+ );
+ }
+
+ function error(message, location) {
+ location = location !== undefined
+ ? location
+ : peg$computeLocation(peg$savedPos, peg$currPos);
+
+ throw peg$buildSimpleError(message, location);
+ }
+
+ function peg$literalExpectation(text, ignoreCase) {
+ return { type: "literal", text: text, ignoreCase: ignoreCase };
+ }
+
+ function peg$classExpectation(parts, inverted, ignoreCase) {
+ return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase };
+ }
+
+ function peg$anyExpectation() {
+ return { type: "any" };
+ }
+
+ function peg$endExpectation() {
+ return { type: "end" };
+ }
+
+ function peg$otherExpectation(description) {
+ return { type: "other", description: description };
+ }
+
+ function peg$computePosDetails(pos) {
+ var details = peg$posDetailsCache[pos];
+ var p;
+
+ if (details) {
+ return details;
+ } else {
+ if (pos >= peg$posDetailsCache.length) {
+ p = peg$posDetailsCache.length - 1;
+ } else {
+ p = pos;
+ while (!peg$posDetailsCache[--p]) {}
+ }
+
+ details = peg$posDetailsCache[p];
+ details = {
+ line: details.line,
+ column: details.column
+ };
+
+ while (p < pos) {
+ if (input.charCodeAt(p) === 10) {
+ details.line++;
+ details.column = 1;
+ } else {
+ details.column++;
+ }
+
+ p++;
+ }
+
+ peg$posDetailsCache[pos] = details;
+
+ return details;
+ }
+ }
+
+ function peg$computeLocation(startPos, endPos, offset) {
+ var startPosDetails = peg$computePosDetails(startPos);
+ var endPosDetails = peg$computePosDetails(endPos);
+
+ var res = {
+ source: peg$source,
+ start: {
+ offset: startPos,
+ line: startPosDetails.line,
+ column: startPosDetails.column
+ },
+ end: {
+ offset: endPos,
+ line: endPosDetails.line,
+ column: endPosDetails.column
+ }
+ };
+ if (offset && peg$source && (typeof peg$source.offset === "function")) {
+ res.start = peg$source.offset(res.start);
+ res.end = peg$source.offset(res.end);
+ }
+ return res;
+ }
+
+ function peg$fail(expected) {
+ if (peg$currPos < peg$maxFailPos) { return; }
+
+ if (peg$currPos > peg$maxFailPos) {
+ peg$maxFailPos = peg$currPos;
+ peg$maxFailExpected = [];
+ }
+
+ peg$maxFailExpected.push(expected);
+ }
+
+ function peg$buildSimpleError(message, location) {
+ return new peg$SyntaxError(message, null, null, location);
+ }
+
+ function peg$buildStructuredError(expected, found, location) {
+ return new peg$SyntaxError(
+ peg$SyntaxError.buildMessage(expected, found),
+ expected,
+ found,
+ location
+ );
+ }
+
+ function peg$parsequery() {
+ var s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ s1 = peg$parse_();
+ s2 = peg$parsefilterList();
+ if (s2 === peg$FAILED) {
+ s2 = null;
+ }
+ s3 = peg$parse_();
+ peg$savedPos = s0;
+ s0 = peg$f0(s2);
+
+ return s0;
+ }
+
+ function peg$parsefilterList() {
+ var s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ s1 = peg$parsefilter();
+ if (s1 !== peg$FAILED) {
+ s2 = [];
+ s3 = peg$currPos;
+ s4 = peg$parselogicalAnd();
+ s5 = peg$parsefilter();
+ if (s5 !== peg$FAILED) {
+ s4 = [s4, s5];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ while (s3 !== peg$FAILED) {
+ s2.push(s3);
+ s3 = peg$currPos;
+ s4 = peg$parselogicalAnd();
+ s5 = peg$parsefilter();
+ if (s5 !== peg$FAILED) {
+ s4 = [s4, s5];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ }
+ peg$savedPos = s0;
+ s0 = peg$f1(s1, s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parsefilter() {
+ var s0, s1, s2, s3, s4, s5, s6;
+
+ s0 = peg$currPos;
+ s1 = peg$parse_();
+ s2 = peg$parsekey();
+ if (s2 === peg$FAILED) {
+ s2 = null;
+ }
+ s3 = peg$parse_();
+ s4 = peg$parseoperator();
+ if (s4 === peg$FAILED) {
+ s4 = null;
+ }
+ s5 = peg$parse_();
+ s6 = peg$parseidentifier();
+ if (s6 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f2(s2, s4, s6);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseoperator() {
+ var s0, s1;
+
+ s0 = peg$currPos;
+ s1 = input.charAt(peg$currPos);
+ if (peg$r0.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f3();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 2) === peg$c0) {
+ s1 = peg$c0;
+ peg$currPos += 2;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e1); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f4();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 62) {
+ s1 = peg$c1;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e2); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f5();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 2) === peg$c2) {
+ s1 = peg$c2;
+ peg$currPos += 2;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e3); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f6();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 60) {
+ s1 = peg$c3;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f7();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 2) === peg$c4) {
+ s1 = peg$c4;
+ peg$currPos += 2;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f8();
+ }
+ s0 = s1;
+ }
+ }
+ }
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parsekey() {
+ var s0, s1;
+
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 4) === peg$c5) {
+ s1 = peg$c5;
+ peg$currPos += 4;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f9();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 6) === peg$c6) {
+ s1 = peg$c6;
+ peg$currPos += 6;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e7); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f10();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 4) === peg$c7) {
+ s1 = peg$c7;
+ peg$currPos += 4;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e8); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f11();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 6) === peg$c8) {
+ s1 = peg$c8;
+ peg$currPos += 6;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e9); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f12();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 11) === peg$c9) {
+ s1 = peg$c9;
+ peg$currPos += 11;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e10); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f13();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 2) === peg$c10) {
+ s1 = peg$c10;
+ peg$currPos += 2;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e11); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f14();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 8) === peg$c11) {
+ s1 = peg$c11;
+ peg$currPos += 8;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e12); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f15();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 8) === peg$c12) {
+ s1 = peg$c12;
+ peg$currPos += 8;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e13); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f16();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 11) === peg$c13) {
+ s1 = peg$c13;
+ peg$currPos += 11;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e14); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f17();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 4) === peg$c14) {
+ s1 = peg$c14;
+ peg$currPos += 4;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f18();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 2) === peg$c15) {
+ s1 = peg$c15;
+ peg$currPos += 2;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e16); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f19();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 8) === peg$c16) {
+ s1 = peg$c16;
+ peg$currPos += 8;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e17); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f20();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 3) === peg$c17) {
+ s1 = peg$c17;
+ peg$currPos += 3;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e18); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f21();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 7) === peg$c18) {
+ s1 = peg$c18;
+ peg$currPos += 7;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e19); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f22();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 4) === peg$c19) {
+ s1 = peg$c19;
+ peg$currPos += 4;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e20); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f23();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 8) === peg$c20) {
+ s1 = peg$c20;
+ peg$currPos += 8;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e21); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f24();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 7) === peg$c21) {
+ s1 = peg$c21;
+ peg$currPos += 7;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e22); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f25();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 6) === peg$c22) {
+ s1 = peg$c22;
+ peg$currPos += 6;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e23); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f26();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 9) === peg$c23) {
+ s1 = peg$c23;
+ peg$currPos += 9;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e24); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f27();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 6) === peg$c24) {
+ s1 = peg$c24;
+ peg$currPos += 6;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e25); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f28();
+ }
+ s0 = s1;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseidentifier() {
+ var s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = peg$parsequotedString();
+ if (s2 === peg$FAILED) {
+ s2 = peg$parsealphanumeric();
+ }
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = peg$parsequotedString();
+ if (s2 === peg$FAILED) {
+ s2 = peg$parsealphanumeric();
+ }
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f29(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parsequotedString() {
+ var s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 34) {
+ s1 = peg$c25;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e26); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = [];
+ s3 = input.charAt(peg$currPos);
+ if (peg$r1.test(s3)) {
+ peg$currPos++;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e27); }
+ }
+ while (s3 !== peg$FAILED) {
+ s2.push(s3);
+ s3 = input.charAt(peg$currPos);
+ if (peg$r1.test(s3)) {
+ peg$currPos++;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e27); }
+ }
+ }
+ if (input.charCodeAt(peg$currPos) === 34) {
+ s3 = peg$c25;
+ peg$currPos++;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e26); }
+ }
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f30(s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parsealphanumeric() {
+ var s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = input.charAt(peg$currPos);
+ if (peg$r2.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e28); }
+ }
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = input.charAt(peg$currPos);
+ if (peg$r2.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e28); }
+ }
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f31(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parselogicalAnd() {
+ var s0, s1;
+
+ s0 = peg$currPos;
+ s1 = peg$parse_();
+ peg$savedPos = s0;
+ s1 = peg$f32();
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parse_() {
+ var s0, s1;
+
+ peg$silentFails++;
+ s0 = [];
+ s1 = input.charAt(peg$currPos);
+ if (peg$r3.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e30); }
+ }
+ while (s1 !== peg$FAILED) {
+ s0.push(s1);
+ s1 = input.charAt(peg$currPos);
+ if (peg$r3.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e30); }
+ }
+ }
+ peg$silentFails--;
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e29); }
+
+ return s0;
+ }
+
+
+ const defaultValues = {
+ "type": "expense",
+ "status": "all",
+ "sortBy": "date",
+ "sortOrder": "desc",
+ "offset": 0
+ };
+
+ function buildFilter(operator, left, right) {
+ return { operator, left, right };
+ }
+
+ function applyDefaults(filters) {
+ return {
+ ...defaultValues,
+ filters
+ };
+ }
+
+ function updateDefaultValues(field, value) {
+ defaultValues[field] = value;
+ }
+
+ function isDefaultField(field) {
+ return defaultValues.hasOwnProperty(field);
+ }
+
+ peg$result = peg$startRuleFunction();
+
+ if (options.peg$library) {
+ return /** @type {any} */ ({
+ peg$result,
+ peg$currPos,
+ peg$FAILED,
+ peg$maxFailExpected,
+ peg$maxFailPos
+ });
+ }
+ if (peg$result !== peg$FAILED && peg$currPos === input.length) {
+ return peg$result;
+ } else {
+ if (peg$result !== peg$FAILED && peg$currPos < input.length) {
+ peg$fail(peg$endExpectation());
+ }
+
+ throw peg$buildStructuredError(
+ peg$maxFailExpected,
+ peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
+ peg$maxFailPos < input.length
+ ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
+ : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
+ );
+ }
+}
+
+const peg$allowedStartRules = [
+ "query"
+];
+
+export {
+ peg$allowedStartRules as StartRules,
+ peg$SyntaxError as SyntaxError,
+ peg$parse as parse
+};
diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy
new file mode 100644
index 000000000000..0957239b6acb
--- /dev/null
+++ b/src/libs/SearchParser/searchParser.peggy
@@ -0,0 +1,122 @@
+// This files defines the grammar that's used by [Peggy](https://peggyjs.org/) to generate the searchParser.js file.
+// The searchParser is setup to parse our custom search syntax and output an AST with the filters.
+//
+// Here's a general grammar structure:
+//
+// start: entry point for the parser. It calls the query rule and return its value.
+// query: rule to process the values returned by the filterList rule. Takes filters as an argument and returns the final AST output.
+// filterList: rule to process the array of filters returned by the filter rule. It takes head and tail as arguments, filters it for null values and builds the AST.
+// filter: rule to build the filter object. It takes field, operator and value as input and returns {operator, left: field, right: value} or null if the left value is a defaultValues
+// operator: rule to match pre-defined search syntax operators, e.g. !=, >, etc
+// key: rule to match pre-defined search syntax fields, e.g. amount, merchant, etc
+// identifier: composite rule to match patterns defined by the quotedString and alphanumeric rules
+// quotedString: rule to match a quoted string pattern, e.g. "this is a quoted string"
+// alphanumeric: rule to match unquoted alphanumeric characters, e.g. a-z, 0-9, _, @, etc
+// logicalAnd: rule to match whitespace and return it as a logical 'and' operator
+// whitespace: rule to match whitespaces
+
+{
+ const defaultValues = {
+ "type": "expense",
+ "status": "all",
+ "sortBy": "date",
+ "sortOrder": "desc",
+ "offset": 0
+ };
+
+ function buildFilter(operator, left, right) {
+ return { operator, left, right };
+ }
+
+ function applyDefaults(filters) {
+ return {
+ ...defaultValues,
+ filters
+ };
+ }
+
+ function updateDefaultValues(field, value) {
+ defaultValues[field] = value;
+ }
+
+ function isDefaultField(field) {
+ return defaultValues.hasOwnProperty(field);
+ }
+}
+
+query
+ = _ filters:filterList? _ { return applyDefaults(filters); }
+
+filterList
+ = head:filter tail:(logicalAnd filter)* {
+ const allFilters = [head, ...tail.map(([_, filter]) => filter)].filter(filter => filter !== null);
+ if (!allFilters.length) {
+ return null;
+ }
+ return allFilters.reduce((result, filter) => buildFilter("and", result, filter));
+ }
+
+filter
+ = _ field:key? _ op:operator? _ value:identifier {
+ if (isDefaultField(field)) {
+ updateDefaultValues(field, value.trim());
+ return null;
+ }
+
+ if (!field && !op) {
+ return buildFilter('eq', 'keyword', value.trim());
+ }
+
+ const values = value.split(',');
+ const operatorValue = op ?? 'eq';
+
+ return values.slice(1).reduce((acc, val) => buildFilter('or', acc, buildFilter(operatorValue, field, val.trim())), buildFilter(operatorValue, field, values[0]));
+ }
+
+operator
+ = (":" / "=") { return "eq"; }
+ / "!=" { return "neq"; }
+ / ">" { return "gt"; }
+ / ">=" { return "gte"; }
+ / "<" { return "lt"; }
+ / "<=" { return "lte"; }
+
+key
+ = "type" { return "type"; }
+ / "status" { return "status"; }
+ / "date" { return "date"; }
+ / "amount" { return "amount"; }
+ / "expenseType" { return "expenseType"; }
+ / "in" { return "in"; }
+ / "currency" { return "currency"; }
+ / "merchant" { return "merchant"; }
+ / "description" { return "description"; }
+ / "from" { return "from"; }
+ / "to" { return "to"; }
+ / "category" { return "category"; }
+ / "tag" { return "tag"; }
+ / "taxRate" { return "taxRate"; }
+ / "card" { return "card"; }
+ / "reportID" { return "reportID"; }
+ / "keyword" { return "keyword"; }
+ / "sortBy" { return "sortBy"; }
+ / "sortOrder" { return "sortOrder"; }
+ / "offset" { return "offset"; }
+
+identifier
+ = parts:(quotedString / alphanumeric)+ { return parts.join(''); }
+
+quotedString
+ = '"' chars:[^"\r\n]* '"' { return chars.join(''); }
+
+alphanumeric
+ = chars:[A-Za-z0-9_@./#&+\-\\',]+ { return chars.join(''); }
+
+logicalAnd
+ = _ { return "and"; }
+
+_ "whitespace"
+ = [ \t\r\n]*
+
+start
+ = query
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index 91d742f44e62..4ec3fa9fb314 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -1,4 +1,5 @@
-import type {SearchColumnType, SortOrder} from '@components/Search/types';
+import type {ValueOf} from 'type-fest';
+import type {AllFieldKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SortOrder} from '@components/Search/types';
import ReportListItem from '@components/SelectionList/Search/ReportListItem';
import TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
import type {ListItem, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
@@ -11,6 +12,7 @@ import DateUtils from './DateUtils';
import getTopmostCentralPaneRoute from './Navigation/getTopmostCentralPaneRoute';
import navigationRef from './Navigation/navigationRef';
import type {AuthScreensParamList, RootStackParamList, State} from './Navigation/types';
+import * as searchParser from './SearchParser/searchParser';
import * as TransactionUtils from './TransactionUtils';
import * as UserUtils from './UserUtils';
@@ -22,7 +24,7 @@ const columnNamesToSortingProperty = {
[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: 'formattedMerchant' as const,
[CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: 'formattedTotal' as const,
[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: 'category' as const,
- [CONST.SEARCH.TABLE_COLUMNS.TYPE]: 'type' as const,
+ [CONST.SEARCH.TABLE_COLUMNS.TYPE]: 'transactionType' as const,
[CONST.SEARCH.TABLE_COLUMNS.ACTION]: 'action' as const,
[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: 'comment' as const,
[CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: null,
@@ -158,15 +160,20 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
const reportIDToTransactions: Record = {};
for (const key in data) {
if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) {
- const value = {...data[key]};
- const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${value.reportID}`;
+ const reportItem = {...data[key]};
+ const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`;
const transactions = reportIDToTransactions[reportKey]?.transactions ?? [];
+ const isExpenseReport = reportItem.type === CONST.REPORT.TYPE.EXPENSE;
+
+ const to = isExpenseReport
+ ? (data[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] as SearchAccountDetails)
+ : (data.personalDetailsList?.[reportItem.managerID] as SearchAccountDetails);
reportIDToTransactions[reportKey] = {
- ...value,
- keyForList: value.reportID,
- from: data.personalDetailsList?.[value.accountID],
- to: data.personalDetailsList?.[value.managerID],
+ ...reportItem,
+ keyForList: reportItem.reportID,
+ from: data.personalDetailsList?.[reportItem.accountID],
+ to,
transactions,
};
} else if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
@@ -301,7 +308,94 @@ function isSearchResultsEmpty(searchResults: SearchResults) {
return !Object.keys(searchResults?.data).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION));
}
+function getQueryHashFromString(query: string): number {
+ return UserUtils.hashText(query, 2 ** 32);
+}
+
+type JSONQuery = {
+ input: string;
+ hash: number;
+ type: string;
+ status: string;
+ sortBy: string;
+ sortOrder: string;
+ offset: number;
+ filters: ASTNode;
+};
+
+function buildJSONQuery(query: string) {
+ try {
+ // Add the full input and hash to the results
+ const result = searchParser.parse(query) as JSONQuery;
+ result.input = query;
+ result.hash = getQueryHashFromString(query);
+ return result;
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+function getFilters(query: string, fields: Array>) {
+ let jsonQuery;
+ try {
+ jsonQuery = searchParser.parse(query) as JSONQuery;
+ } catch (e) {
+ console.error(e);
+ return;
+ }
+
+ const filters = {} as QueryFilters;
+
+ fields.forEach((field) => {
+ const rootFieldKey = field as ValueOf;
+ if (jsonQuery[rootFieldKey] === undefined) {
+ return;
+ }
+
+ filters[field] = {
+ operator: 'eq',
+ value: jsonQuery[rootFieldKey],
+ };
+ });
+
+ function traverse(node: ASTNode) {
+ if (!node.operator) {
+ return;
+ }
+
+ if (typeof node?.left === 'object') {
+ traverse(node.left);
+ }
+
+ if (typeof node?.right === 'object') {
+ traverse(node.right);
+ }
+
+ const nodeKey = node.left as ValueOf;
+ if (!fields.includes(nodeKey)) {
+ return;
+ }
+
+ if (!filters[nodeKey]) {
+ filters[nodeKey] = [];
+ }
+
+ const filterArray = filters[nodeKey] as QueryFilter[];
+ filterArray.push({
+ operator: node.operator,
+ value: node.right as string | number,
+ });
+ }
+
+ if (jsonQuery.filters) {
+ traverse(jsonQuery.filters);
+ }
+
+ return filters;
+}
+
export {
+ buildJSONQuery,
getListItem,
getQueryHash,
getSections,
@@ -313,4 +407,5 @@ export {
isReportListItemType,
isTransactionListItemType,
isSearchResultsEmpty,
+ getFilters,
};
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 80081c8f89c7..1007ba455fa6 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -6,6 +6,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PersonalDetails, PersonalDetailsList, ReportActions, TransactionViolation} from '@src/types/onyx';
import type Beta from '@src/types/onyx/Beta';
+import type {OriginalMessageChangeLog} from '@src/types/onyx/OriginalMessage';
import type Policy from '@src/types/onyx/Policy';
import type PriorityMode from '@src/types/onyx/PriorityMode';
import type Report from '@src/types/onyx/Report';
@@ -17,12 +18,29 @@ import localeCompare from './LocaleCompare';
import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
import * as OptionsListUtils from './OptionsListUtils';
+import Parser from './Parser';
+import Permissions from './Permissions';
import * as PolicyUtils from './PolicyUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
import * as ReportUtils from './ReportUtils';
import * as TaskUtils from './TaskUtils';
+type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string; phrase3?: string; messageText?: string; messageHtml?: string};
+
+let allBetas: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.BETAS,
+ callback: (value) => (allBetas = value),
+});
+
const visibleReportActionItems: ReportActions = {};
+let allPersonalDetails: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ callback: (value) => {
+ allPersonalDetails = value ?? {};
+ },
+});
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
callback: (actions, key) => {
@@ -88,13 +106,23 @@ function getOrderedReportIDs(
return;
}
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {};
+ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1');
const doesReportHaveViolations = OptionsListUtils.shouldShowViolations(report, betas ?? [], transactionViolations);
const isHidden = report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
const isFocused = report.reportID === currentReportId;
const allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) ?? {};
+ const transactionReportActions = ReportActionsUtils.getAllReportActions(report.reportID);
+ const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, transactionReportActions, undefined);
+ let doesTransactionThreadReportHasViolations = false;
+ if (oneTransactionThreadReportID) {
+ const transactionReport = ReportUtils.getReport(oneTransactionThreadReportID);
+ doesTransactionThreadReportHasViolations = !!transactionReport && OptionsListUtils.shouldShowViolations(transactionReport, betas ?? [], transactionViolations);
+ }
const hasErrorsOtherThanFailedReceipt =
- doesReportHaveViolations || Object.values(allReportErrors).some((error) => error?.[0] !== Localize.translateLocal('iou.error.genericSmartscanFailureMessage'));
- if (ReportUtils.isOneTransactionThread(report.reportID, report.parentReportID ?? '0')) {
+ doesTransactionThreadReportHasViolations ||
+ doesReportHaveViolations ||
+ Object.values(allReportErrors).some((error) => error?.[0] !== Localize.translateLocal('iou.error.genericSmartscanFailureMessage'));
+ if (ReportUtils.isOneTransactionThread(report.reportID, report.parentReportID ?? '0', parentReportAction)) {
return;
}
if (hasErrorsOtherThanFailedReceipt) {
@@ -212,6 +240,7 @@ function getOptionData({
policy,
parentReportAction,
hasViolations,
+ transactionViolations,
}: {
report: OnyxEntry;
reportActions: OnyxEntry;
@@ -220,6 +249,7 @@ function getOptionData({
policy: OnyxEntry | undefined;
parentReportAction: OnyxEntry | undefined;
hasViolations: boolean;
+ transactionViolations?: OnyxCollection;
}): ReportUtils.OptionData | undefined {
// When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for
// this method to be called after the Onyx data has been cleared out. In that case, it's fine to do
@@ -258,6 +288,7 @@ function getOptionData({
isWaitingOnBankAccount: false,
isAllowedToComment: true,
isDeletedParentAction: false,
+ isConciergeChat: false,
};
const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report);
@@ -279,6 +310,21 @@ function getOptionData({
result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report);
result.pendingAction = report.pendingFields?.addWorkspaceRoom ?? report.pendingFields?.createChat;
result.brickRoadIndicator = hasErrors || hasViolations ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '';
+ const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, ReportActionsUtils.getAllReportActions(report.reportID));
+ if (oneTransactionThreadReportID) {
+ const oneTransactionThreadReport = ReportUtils.getReport(oneTransactionThreadReportID);
+
+ if (
+ Permissions.canUseViolations(allBetas) &&
+ ReportUtils.shouldDisplayTransactionThreadViolations(
+ oneTransactionThreadReport,
+ transactionViolations,
+ ReportActionsUtils.getAllReportActions(report.reportID)[oneTransactionThreadReport?.parentReportActionID ?? '-1'],
+ )
+ ) {
+ result.brickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
+ }
+ }
result.ownerAccountID = report.ownerAccountID;
result.managerID = report.managerID;
result.reportID = report.reportID;
@@ -303,6 +349,7 @@ function getOptionData({
result.tooltipText = ReportUtils.getReportParticipantsTitle(visibleParticipantAccountIDs);
result.hasOutstandingChildTask = report.hasOutstandingChildTask;
result.hasParentAccess = report.hasParentAccess;
+ result.isConciergeChat = ReportUtils.isConciergeChatReport(report);
const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || ReportUtils.isExpenseReport(report);
const subtitle = ReportUtils.getChatRoomSubtitle(report);
@@ -353,7 +400,7 @@ function getOptionData({
result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName});
} else if (ReportActionsUtils.isTaskAction(lastAction)) {
result.alternateText = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastAction).text);
- } else if (ReportActionsUtils.isRoomChangeLogAction(lastAction)) {
+ } else if (ReportActionsUtils.isInviteOrRemovedAction(lastAction)) {
const lastActionOriginalMessage = lastAction?.actionName ? ReportActionsUtils.getOriginalMessage(lastAction) : null;
const targetAccountIDs = lastActionOriginalMessage?.targetAccountIDs ?? [];
const targetAccountIDsLength = targetAccountIDs.length !== 0 ? targetAccountIDs.length : report.lastMessageHtml?.match(/]*><\/mention-user>/g)?.length ?? 0;
@@ -372,11 +419,9 @@ function getOptionData({
: ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`;
result.alternateText += `${preposition} ${roomName}`;
}
- if (lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) {
- result.alternateText = `${lastActorDisplayName} ${Localize.translate(preferredLocale, 'roomChangeLog.updateRoomDescription')} ${
- lastActionOriginalMessage?.description
- }`.trim();
- }
+ } else if (lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) {
+ const lastActionOriginalMessage = lastAction?.actionName ? (ReportActionsUtils.getOriginalMessage(lastAction) as OriginalMessageChangeLog | undefined) : null;
+ result.alternateText = `${lastActorDisplayName} ${Localize.translate(preferredLocale, 'roomChangeLog.updateRoomDescription')} ${lastActionOriginalMessage?.description}`.trim();
} else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.LEAVE_POLICY) {
result.alternateText = Localize.translateLocal('workspace.invite.leftWorkspace');
} else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
@@ -386,38 +431,12 @@ function getOptionData({
} else {
result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : ReportActionsUtils.getLastVisibleMessage(report.reportID, {}, lastAction)?.lastMessageText;
if (!result.alternateText) {
- result.alternateText = Localize.translate(preferredLocale, 'report.noActivityYet');
+ result.alternateText = ReportUtils.formatReportLastMessageText(getWelcomeMessage(report, policy).messageText ?? Localize.translateLocal('report.noActivityYet'));
}
}
} else {
if (!lastMessageText) {
- if (ReportUtils.isSystemChat(report)) {
- lastMessageText = Localize.translate(preferredLocale, 'reportActionsView.beginningOfChatHistorySystemDM');
- } else if (ReportUtils.isSelfDM(report)) {
- lastMessageText = Localize.translate(preferredLocale, 'reportActionsView.beginningOfChatHistorySelfDM');
- } else {
- // Here we get the beginning of chat history message and append the display name for each user, adding pronouns if there are any.
- // We also add a fullstop after the final name, the word "and" before the final name and commas between all previous names.
- lastMessageText =
- Localize.translate(preferredLocale, 'reportActionsView.beginningOfChatHistory') +
- displayNamesWithTooltips
- .map(({displayName, pronouns}, index) => {
- const formattedText = !pronouns ? displayName : `${displayName} (${pronouns})`;
-
- if (index === displayNamesWithTooltips.length - 1) {
- return `${formattedText}.`;
- }
- if (index === displayNamesWithTooltips.length - 2) {
- return `${formattedText} ${Localize.translate(preferredLocale, 'common.and')}`;
- }
- if (index < displayNamesWithTooltips.length - 2) {
- return `${formattedText},`;
- }
-
- return '';
- })
- .join(' ');
- }
+ lastMessageText = ReportUtils.formatReportLastMessageText(getWelcomeMessage(report, policy).messageText ?? Localize.translateLocal('report.noActivityYet'));
}
result.alternateText =
@@ -458,7 +477,111 @@ function getOptionData({
return result;
}
+function getWelcomeMessage(report: OnyxEntry, policy: OnyxEntry): WelcomeMessage {
+ const welcomeMessage: WelcomeMessage = {showReportName: true};
+ if (ReportUtils.isChatThread(report) || ReportUtils.isTaskReport(report)) {
+ return welcomeMessage;
+ }
+
+ if (ReportUtils.isChatRoom(report)) {
+ return getRoomWelcomeMessage(report);
+ }
+
+ if (ReportUtils.isPolicyExpenseChat(report)) {
+ if (policy?.description) {
+ welcomeMessage.messageHtml = policy.description;
+ welcomeMessage.messageText = Parser.htmlToText(welcomeMessage.messageHtml);
+ } else {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartOne');
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo');
+ welcomeMessage.phrase3 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree');
+ welcomeMessage.messageText = `${welcomeMessage.phrase1} ${ReportUtils.getDisplayNameForParticipant(report?.ownerAccountID)} ${welcomeMessage.phrase2} ${ReportUtils.getPolicyName(
+ report,
+ )} ${welcomeMessage.phrase3}`;
+ }
+ return welcomeMessage;
+ }
+
+ if (ReportUtils.isSelfDM(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistorySelfDM');
+ welcomeMessage.messageText = welcomeMessage.phrase1;
+ return welcomeMessage;
+ }
+
+ if (ReportUtils.isSystemChat(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistorySystemDM');
+ welcomeMessage.messageText = welcomeMessage.phrase1;
+ return welcomeMessage;
+ }
+
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistory');
+ const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report);
+ const isMultipleParticipant = participantAccountIDs.length > 1;
+ const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(
+ OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, allPersonalDetails),
+ isMultipleParticipant,
+ );
+ const displayNamesWithTooltipsText = displayNamesWithTooltips
+ .map(({displayName, pronouns}, index) => {
+ const formattedText = !pronouns ? displayName : `${displayName} (${pronouns})`;
+
+ if (index === displayNamesWithTooltips.length - 1) {
+ return `${formattedText}.`;
+ }
+ if (index === displayNamesWithTooltips.length - 2) {
+ return `${formattedText} ${Localize.translateLocal('common.and')}`;
+ }
+ if (index < displayNamesWithTooltips.length - 2) {
+ return `${formattedText},`;
+ }
+
+ return '';
+ })
+ .join(' ');
+
+ welcomeMessage.messageText = `${welcomeMessage.phrase1} ${displayNamesWithTooltipsText}`;
+ return welcomeMessage;
+}
+
+/**
+ * Get welcome message based on room type
+ */
+function getRoomWelcomeMessage(report: OnyxEntry): WelcomeMessage {
+ const welcomeMessage: WelcomeMessage = {showReportName: true};
+ const workspaceName = ReportUtils.getPolicyName(report);
+
+ if (report?.description) {
+ welcomeMessage.messageHtml = report.description;
+ welcomeMessage.messageText = Parser.htmlToText(welcomeMessage.messageHtml);
+ return welcomeMessage;
+ }
+ if (ReportUtils.isArchivedRoom(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartOne');
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartTwo');
+ } else if (ReportUtils.isDomainRoom(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report?.reportName ?? ''});
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo');
+ } else if (ReportUtils.isAdminRoom(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName});
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo');
+ } else if (ReportUtils.isAnnounceRoom(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName});
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName});
+ } else if (ReportUtils.isInvoiceRoom(report)) {
+ welcomeMessage.showReportName = false;
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom');
+ } else {
+ // Message for user created rooms or other room types.
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartOne');
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartTwo');
+ }
+ welcomeMessage.messageText = `${welcomeMessage.phrase1} ${welcomeMessage.showReportName ? ReportUtils.getReportName(report) : ''} ${welcomeMessage.phrase2 ?? ''}`;
+
+ return welcomeMessage;
+}
+
export default {
getOptionData,
getOrderedReportIDs,
+ getWelcomeMessage,
};
diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts
index bd0bd10cd83e..06745a49217b 100644
--- a/src/libs/TaskUtils.ts
+++ b/src/libs/TaskUtils.ts
@@ -1,12 +1,21 @@
import type {OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type {Message} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
import * as Localize from './Localize';
+import Navigation from './Navigation/Navigation';
import {getReportActionHtml, getReportActionText} from './ReportActionsUtils';
import * as ReportConnection from './ReportConnection';
+/**
+ * Check if the active route belongs to task edit flow.
+ */
+function isActiveTaskEditRoute(reportID: string): boolean {
+ return [ROUTES.TASK_TITLE, ROUTES.TASK_ASSIGNEE, ROUTES.REPORT_DESCRIPTION].map((route) => route.getRoute(reportID)).some(Navigation.isActiveRoute);
+}
+
/**
* Given the Task reportAction name, return the appropriate message to be displayed and copied to clipboard.
*/
@@ -42,4 +51,4 @@ function getTaskCreatedMessage(reportAction: OnyxEntry) {
return taskTitle ? Localize.translateLocal('task.messages.created', {title: taskTitle}) : '';
}
-export {getTaskReportActionMessage, getTaskTitle, getTaskCreatedMessage};
+export {isActiveTaskEditRoute, getTaskReportActionMessage, getTaskTitle, getTaskCreatedMessage};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index b9a8a05ba046..65f958cad8e8 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -322,21 +322,38 @@ function getReportPreviewAction(chatReportID: string, iouReportID: string): Onyx
* @param policy
* @param isFromGlobalCreate
* @param iouRequestType one of manual/scan/distance
- * @param skipConfirmation if true, skip confirmation step
*/
function initMoneyRequest(reportID: string, policy: OnyxEntry, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) {
// Generate a brand new transactionID
const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID;
+ const currency = policy?.outputCurrency ?? currentUserPersonalDetails?.localCurrencyCode ?? CONST.CURRENCY.USD;
// Disabling this line since currentDate can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const created = currentDate || format(new Date(), 'yyyy-MM-dd');
+
+ const currentTransaction = allTransactionDrafts?.[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${newTransactionID}`];
+
+ // in case we have to re-init money request, but the IOU request type is the same with the old draft transaction,
+ // we should keep most of the existing data by using the ONYX MERGE operation
+ if (currentTransaction?.iouRequestType === iouRequestType) {
+ // so, we just need to update the reportID, isFromGlobalCreate, created, currency
+ Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${newTransactionID}`, {
+ reportID,
+ isFromGlobalCreate,
+ created,
+ currency,
+ transactionID: newTransactionID,
+ });
+ return;
+ }
+
const comment: Comment = {};
// Add initial empty waypoints when starting a distance expense
if (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) {
comment.waypoints = {
- waypoint0: {},
- waypoint1: {},
+ waypoint0: {keyForList: 'start_waypoint'},
+ waypoint1: {keyForList: 'stop_waypoint'},
};
if (!isFromGlobalCreate) {
const customUnitRateID = DistanceRequestUtils.getCustomUnitRateID(reportID);
@@ -350,7 +367,7 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry,
amount: 0,
comment,
created,
- currency: policy?.outputCurrency ?? currentUserPersonalDetails?.localCurrencyCode ?? CONST.CURRENCY.USD,
+ currency,
iouRequestType,
reportID,
transactionID: newTransactionID,
diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts
index 5870d642d8cd..8e2fff3868ae 100644
--- a/src/libs/actions/PersonalDetails.ts
+++ b/src/libs/actions/PersonalDetails.ts
@@ -25,7 +25,6 @@ import ROUTES from '@src/ROUTES';
import type {DateOfBirthForm} from '@src/types/form';
import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx';
import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails';
-import * as Session from './Session';
let currentUserEmail = '';
let currentUserAccountID = -1;
@@ -191,10 +190,6 @@ function updateAddress(street: string, street2: string, city: string, state: str
* selected timezone if set to automatically update.
*/
function updateAutomaticTimezone(timezone: Timezone) {
- if (Session.isAnonymousUser()) {
- return;
- }
-
if (!currentUserAccountID) {
return;
}
diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts
index 4f32eb0b81ba..ae8ed41f22c5 100644
--- a/src/libs/actions/Policy/Category.ts
+++ b/src/libs/actions/Policy/Category.ts
@@ -2,7 +2,7 @@ import lodashUnion from 'lodash/union';
import type {NullishDeep, OnyxCollection, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
-import type {EnablePolicyCategoriesParams, OpenPolicyCategoriesPageParams, SetPolicyDistanceRatesDefaultCategoryParams} from '@libs/API/parameters';
+import type {EnablePolicyCategoriesParams, OpenPolicyCategoriesPageParams, SetPolicyDistanceRatesDefaultCategoryParams, UpdatePolicyCategoryGLCodeParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
@@ -331,6 +331,142 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string
API.write(WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY, parameters, onyxData);
}
+function setPolicyCategoryPayrollCode(policyID: string, categoryName: string, payrollCode: string) {
+ const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {};
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': payrollCode,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ pendingAction: null,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': null,
+ },
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': payrollCode,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updatePayrollCodeFailureMessage'),
+ pendingAction: null,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': null,
+ },
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ categoryName,
+ payrollCode,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE, parameters, onyxData);
+}
+
+function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode: string) {
+ const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {};
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GL Code': CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GL Code': glCode,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ pendingAction: null,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GL Code': null,
+ },
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GL Code': glCode,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updateGLCodeFailureMessage'),
+ pendingAction: null,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GL Code': null,
+ },
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters: UpdatePolicyCategoryGLCodeParams = {
+ policyID,
+ categoryName,
+ glCode,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE, parameters, onyxData);
+}
+
function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) {
const onyxData: OnyxData = {
optimisticData: [
@@ -616,8 +752,10 @@ export {
buildOptimisticPolicyRecentlyUsedCategories,
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
+ setPolicyCategoryPayrollCode,
createPolicyCategory,
renamePolicyCategory,
+ setPolicyCategoryGLCode,
clearCategoryErrors,
enablePolicyCategories,
setPolicyDistanceRatesDefaultCategory,
diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts
index bccb08c47c18..27b67c9fe686 100644
--- a/src/libs/actions/Policy/ReportField.ts
+++ b/src/libs/actions/Policy/ReportField.ts
@@ -5,8 +5,8 @@ import * as API from '@libs/API';
import type {
CreateWorkspaceReportFieldListValueParams,
CreateWorkspaceReportFieldParams,
+ DeletePolicyReportField,
EnableWorkspaceReportFieldListValueParams,
- PolicyReportFieldsReplace,
RemoveWorkspaceReportFieldListValueParams,
UpdateWorkspaceReportFieldInitialValueParams,
} from '@libs/API/parameters';
@@ -260,7 +260,7 @@ function deleteReportFields(policyID: string, reportFieldsToUpdate: string[]) {
],
};
- const parameters: PolicyReportFieldsReplace = {
+ const parameters: DeletePolicyReportField = {
policyID,
reportFields: JSON.stringify(Object.values(updatedReportFields)),
};
diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts
index 2558969be2f3..daefaed07f68 100644
--- a/src/libs/actions/Policy/Tag.ts
+++ b/src/libs/actions/Policy/Tag.ts
@@ -1,7 +1,15 @@
-import type {NullishDeep, OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
-import type {EnablePolicyTagsParams, OpenPolicyTagsPageParams, RenamePolicyTaglistParams, RenamePolicyTagsParams, SetPolicyTagsEnabled, SetPolicyTagsRequired} from '@libs/API/parameters';
+import type {
+ EnablePolicyTagsParams,
+ OpenPolicyTagsPageParams,
+ RenamePolicyTaglistParams,
+ RenamePolicyTagsParams,
+ SetPolicyTagsEnabled,
+ SetPolicyTagsRequired,
+ UpdatePolicyTagGLCodeParams,
+} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
@@ -624,6 +632,9 @@ function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: stri
}
function setPolicyRequiresTag(policyID: string, requiresTag: boolean) {
+ const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {};
+ const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags);
+
const onyxData: OnyxData = {
optimisticData: [
{
@@ -667,6 +678,26 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) {
],
};
+ if (isMultiLevelTags) {
+ const getUpdatedTagsData = (required: boolean): OnyxUpdate => ({
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ ...Object.keys(policyTags).reduce((acc, key) => {
+ acc[key] = {
+ ...acc[key],
+ required,
+ };
+ return acc;
+ }, {}),
+ },
+ });
+
+ onyxData.optimisticData?.push(getUpdatedTagsData(requiresTag));
+ onyxData.failureData?.push(getUpdatedTagsData(!requiresTag));
+ onyxData.successData?.push(getUpdatedTagsData(requiresTag));
+ }
+
const parameters = {
policyID,
requiresTag,
@@ -733,6 +764,81 @@ function setPolicyTagsRequired(policyID: string, requiresTag: boolean, tagListIn
API.write(WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED, parameters, onyxData);
}
+function setPolicyTagGLCode(policyID: string, tagName: string, tagListIndex: number, glCode: string) {
+ const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[tagListIndex];
+ const policyTagToUpdate = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`]?.[tagListName]?.tags?.[tagName] ?? {};
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ [tagListName]: {
+ tags: {
+ [tagName]: {
+ ...policyTagToUpdate,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GL Code': CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GL Code': glCode,
+ },
+ },
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ [tagListName]: {
+ tags: {
+ [tagName]: {
+ errors: null,
+ pendingAction: null,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GL Code': null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ [tagListName]: {
+ tags: {
+ [tagName]: {
+ ...policyTagToUpdate,
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.updateGLCodeFailureMessage'),
+ },
+ },
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters: UpdatePolicyTagGLCodeParams = {
+ policyID,
+ tagName,
+ tagListName,
+ tagListIndex,
+ glCode,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_POLICY_TAG_GL_CODE, parameters, onyxData);
+}
+
export {
buildOptimisticPolicyRecentlyUsedTags,
setPolicyRequiresTag,
@@ -747,6 +853,7 @@ export {
renamePolicyTag,
renamePolicyTaglist,
setWorkspaceTagEnabled,
+ setPolicyTagGLCode,
};
export type {NewCustomUnit};
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 3060f53f12c3..026ce45146d3 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -1,4 +1,3 @@
-import {findFocusedRoute} from '@react-navigation/native';
import {format as timezoneFormat, utcToZonedTime} from 'date-fns-tz';
import {Str} from 'expensify-common';
import isEmpty from 'lodash/isEmpty';
@@ -56,13 +55,11 @@ import {prepareDraftComment} from '@libs/DraftCommentUtils';
import * as EmojiUtils from '@libs/EmojiUtils';
import * as Environment from '@libs/Environment/Environment';
import * as ErrorUtils from '@libs/ErrorUtils';
-import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import isPublicScreenRoute from '@libs/isPublicScreenRoute';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import {registerPaginationConfig} from '@libs/Middleware/Pagination';
-import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
-import {isOnboardingFlowName} from '@libs/NavigationUtils';
+import Navigation from '@libs/Navigation/Navigation';
import type {NetworkStatus} from '@libs/NetworkConnection';
import LocalNotification from '@libs/Notification/LocalNotification';
import Parser from '@libs/Parser';
@@ -2544,47 +2541,28 @@ function openReportFromDeepLink(url: string) {
// Navigate to the report after sign-in/sign-up.
InteractionManager.runAfterInteractions(() => {
Session.waitForUserSignIn().then(() => {
- Onyx.connect({
- key: ONYXKEYS.NVP_ONBOARDING,
- callback: (onboarding) => {
- Navigation.waitForProtectedRoutes().then(() => {
- if (route && Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(route)) {
- Session.signOutAndRedirectToSignIn(true);
- return;
- }
-
- // We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
- // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
- // which is already called when AuthScreens mounts.
- if (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) {
- return;
- }
-
- if (shouldSkipDeepLinkNavigation(route)) {
- return;
- }
-
- const state = navigationRef.getRootState();
- const currentFocusedRoute = findFocusedRoute(state);
- const hasCompletedGuidedSetupFlow = hasCompletedGuidedSetupFlowSelector(onboarding);
-
- // We need skip deeplinking if the user hasn't completed the guided setup flow.
- if (!hasCompletedGuidedSetupFlow) {
- return;
- }
-
- if (isOnboardingFlowName(currentFocusedRoute?.name)) {
- Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
- return;
- }
-
- if (isAuthenticated) {
- return;
- }
-
- Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
- });
- },
+ Navigation.waitForProtectedRoutes().then(() => {
+ if (route && Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(route)) {
+ Session.signOutAndRedirectToSignIn(true);
+ return;
+ }
+
+ // We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
+ // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
+ // which is already called when AuthScreens mounts.
+ if (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) {
+ return;
+ }
+
+ if (shouldSkipDeepLinkNavigation(route)) {
+ return;
+ }
+
+ if (isAuthenticated) {
+ return;
+ }
+
+ Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
});
});
});
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index 0a7244bde1e5..964f5be0129b 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -126,7 +126,7 @@ function createTaskAndNavigate(
const currentTime = DateUtils.getDBTimeWithSkew();
const lastCommentText = ReportUtils.formatReportLastMessageText(ReportActionsUtils.getReportActionText(optimisticAddCommentReport.reportAction));
- const parentReport = getReport(parentReportID);
+ const parentReport = ReportUtils.getReport(parentReportID);
const optimisticParentReport = {
lastVisibleActionCreated: optimisticAddCommentReport.reportAction.created,
lastMessageText: lastCommentText,
@@ -906,13 +906,6 @@ function getParentReport(report: OnyxEntry): OnyxEntry {
- return ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
-}
-
/**
* Cancels a task by setting the report state to SUBMITTED and status to CLOSED
*/
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 7acc79485f0c..7b3b1abd04ef 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -13,7 +13,6 @@ import type {
SetContactMethodAsDefaultParams,
SetNameValuePairParams,
UpdateChatPriorityModeParams,
- UpdateFrequentlyUsedEmojisParams,
UpdateNewsletterSubscriptionParams,
UpdatePreferredEmojiSkinToneParams,
UpdateStatusParams,
@@ -37,7 +36,7 @@ import Visibility from '@libs/Visibility';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {BlockedFromConcierge, CustomStatusDraft, FrequentlyUsedEmoji, Policy} from '@src/types/onyx';
+import type {BlockedFromConcierge, CustomStatusDraft, Policy} from '@src/types/onyx';
import type Login from '@src/types/onyx/Login';
import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
@@ -655,23 +654,6 @@ function updatePreferredSkinTone(skinTone: number) {
API.write(WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE, parameters, {optimisticData});
}
-/**
- * Sync frequentlyUsedEmojis with Onyx and Server
- */
-function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) {
- const optimisticData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.SET,
- key: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
- value: frequentlyUsedEmojis,
- },
- ];
-
- const parameters: UpdateFrequentlyUsedEmojisParams = {value: JSON.stringify(frequentlyUsedEmojis)};
-
- API.write(WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS, parameters, {optimisticData});
-}
-
/**
* Sync user chat priority mode with Onyx and Server
* @param mode
@@ -1045,7 +1027,6 @@ export {
setShouldUseStagingServer,
setMuteAllSounds,
clearUserErrorMessage,
- updateFrequentlyUsedEmojis,
joinScreenShare,
clearScreenShareRequest,
generateStatementPDF,
diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts
index b592424cfcdf..0c3224ee37d9 100644
--- a/src/libs/actions/Welcome.ts
+++ b/src/libs/actions/Welcome.ts
@@ -11,8 +11,7 @@ import ROUTES from '@src/ROUTES';
import type Onboarding from '@src/types/onyx/Onboarding';
import type TryNewDot from '@src/types/onyx/TryNewDot';
-type OnboardingData = Onboarding | [] | undefined;
-
+let onboarding: Onboarding | [] | undefined;
let isLoadingReportData = true;
let tryNewDotData: TryNewDot | undefined;
@@ -31,8 +30,8 @@ let isServerDataReadyPromise = new Promise((resolve) => {
resolveIsReadyPromise = resolve;
});
-let resolveOnboardingFlowStatus: (value?: OnboardingData) => void;
-let isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
+let resolveOnboardingFlowStatus: (value?: Promise) => void | undefined;
+let isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
resolveOnboardingFlowStatus = resolve;
});
@@ -46,7 +45,7 @@ function onServerDataReady(): Promise {
}
function isOnboardingFlowCompleted({onCompleted, onNotCompleted}: HasCompletedOnboardingFlowProps) {
- isOnboardingFlowStatusKnownPromise.then((onboarding) => {
+ isOnboardingFlowStatusKnownPromise.then(() => {
if (Array.isArray(onboarding) || onboarding?.hasCompletedGuidedSetupFlow === undefined) {
return;
}
@@ -96,14 +95,30 @@ function handleHybridAppOnboarding() {
isOnboardingFlowCompleted({
onNotCompleted: () =>
setTimeout(() => {
- Navigation.navigate(ROUTES.EXPLANATION_MODAL_ROOT);
+ Navigation.navigate(ROUTES.ONBOARDING_ROOT);
}, variables.explanationModalDelay),
}),
});
}
/**
- * Check if report data are loaded
+ * Check that a few requests have completed so that the welcome action can proceed:
+ *
+ * - Whether we are a first time new expensify user
+ * - Whether we have loaded all policies the server knows about
+ * - Whether we have loaded all reports the server knows about
+ * Check if onboarding data is ready in order to check if the user has completed onboarding or not
+ */
+function checkOnboardingDataReady() {
+ if (onboarding === undefined) {
+ return;
+ }
+
+ resolveOnboardingFlowStatus?.();
+}
+
+/**
+ * Check if user dismissed modal and if report data are loaded
*/
function checkServerDataReady() {
if (isLoadingReportData) {
@@ -128,10 +143,6 @@ function setOnboardingPurposeSelected(value: OnboardingPurposeType) {
Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, value ?? null);
}
-function setOnboardingErrorMessage(value: string) {
- Onyx.set(ONYXKEYS.ONBOARDING_ERROR_MESSAGE, value ?? null);
-}
-
function setOnboardingAdminsChatReportID(adminsChatReportID?: string) {
Onyx.set(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, adminsChatReportID ?? null);
}
@@ -175,7 +186,9 @@ Onyx.connect({
return;
}
- resolveOnboardingFlowStatus(value);
+ onboarding = value;
+
+ checkOnboardingDataReady();
},
});
@@ -200,9 +213,10 @@ function resetAllChecks() {
isServerDataReadyPromise = new Promise((resolve) => {
resolveIsReadyPromise = resolve;
});
- isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
+ isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
resolveOnboardingFlowStatus = resolve;
});
+ onboarding = undefined;
isLoadingReportData = true;
}
@@ -215,5 +229,4 @@ export {
setOnboardingPolicyID,
completeHybridAppOnboarding,
handleHybridAppOnboarding,
- setOnboardingErrorMessage,
};
diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts
index adab82a5da8f..8be8d2cb077a 100644
--- a/src/libs/actions/connections/NetSuiteCommands.ts
+++ b/src/libs/actions/connections/NetSuiteCommands.ts
@@ -7,7 +7,7 @@ import {WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Connections, NetSuiteCustomFormID, NetSuiteCustomList, NetSuiteCustomSegment} from '@src/types/onyx/Policy';
+import type {Connections, NetSuiteCustomFormID, NetSuiteCustomList, NetSuiteCustomSegment, NetSuiteMappingValues} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
type SubsidiaryParam = {
@@ -402,6 +402,118 @@ function updateNetSuiteImportMapping void;
diff --git a/src/libs/hasCompletedGuidedSetupFlowSelector.ts b/src/libs/hasCompletedGuidedSetupFlowSelector.ts
deleted file mode 100644
index 83cde0a0be8c..000000000000
--- a/src/libs/hasCompletedGuidedSetupFlowSelector.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type {OnyxValue} from 'react-native-onyx';
-import type ONYXKEYS from '@src/ONYXKEYS';
-
-function hasCompletedGuidedSetupFlowSelector(onboarding: OnyxValue): boolean {
- // onboarding is an array for old accounts and accounts created from olddot
- if (Array.isArray(onboarding)) {
- return true;
- }
- return onboarding?.hasCompletedGuidedSetupFlow ?? false;
-}
-
-export default hasCompletedGuidedSetupFlowSelector;
diff --git a/src/libs/shouldRenderAppPaymentCard/index.native.ts b/src/libs/shouldRenderAppPaymentCard/index.native.ts
new file mode 100644
index 000000000000..74137a6f7cdc
--- /dev/null
+++ b/src/libs/shouldRenderAppPaymentCard/index.native.ts
@@ -0,0 +1,5 @@
+import type ShouldRenderAddPaymentCard from './types';
+
+const shouldRenderAddPaymentCard: ShouldRenderAddPaymentCard = () => false;
+
+export default shouldRenderAddPaymentCard;
diff --git a/src/libs/shouldRenderAppPaymentCard/index.ts b/src/libs/shouldRenderAppPaymentCard/index.ts
new file mode 100644
index 000000000000..9b2ee6082e02
--- /dev/null
+++ b/src/libs/shouldRenderAppPaymentCard/index.ts
@@ -0,0 +1,5 @@
+import type ShouldRenderAddPaymentCard from './types';
+
+const shouldRenderAddPaymentCard: ShouldRenderAddPaymentCard = () => true;
+
+export default shouldRenderAddPaymentCard;
diff --git a/src/libs/shouldRenderAppPaymentCard/types.ts b/src/libs/shouldRenderAppPaymentCard/types.ts
new file mode 100644
index 000000000000..80e934badab2
--- /dev/null
+++ b/src/libs/shouldRenderAppPaymentCard/types.ts
@@ -0,0 +1,3 @@
+type ShouldRenderAddPaymentCard = () => boolean;
+
+export default ShouldRenderAddPaymentCard;
diff --git a/src/libs/shouldShowSubscriptionsMenu/index.native.ts b/src/libs/shouldShowSubscriptionsMenu/index.native.ts
deleted file mode 100644
index c98302e9a87d..000000000000
--- a/src/libs/shouldShowSubscriptionsMenu/index.native.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type ShouldShowSubscriptionsMenu from './types';
-
-/**
- * Indicates whether the subscription menu should show in the all settings screen
- */
-const shouldShowSubscriptionsMenu: ShouldShowSubscriptionsMenu = false;
-
-export default shouldShowSubscriptionsMenu;
diff --git a/src/libs/shouldShowSubscriptionsMenu/index.ts b/src/libs/shouldShowSubscriptionsMenu/index.ts
deleted file mode 100644
index 2f2b7f17c2c5..000000000000
--- a/src/libs/shouldShowSubscriptionsMenu/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type ShouldShowSubscriptionsMenu from './types';
-
-/**
- * Indicates whether the subscription menu should show in the all settings screen
- */
-const shouldShowSubscriptionsMenu: ShouldShowSubscriptionsMenu = true;
-
-export default shouldShowSubscriptionsMenu;
diff --git a/src/libs/shouldShowSubscriptionsMenu/types.tsx b/src/libs/shouldShowSubscriptionsMenu/types.tsx
deleted file mode 100644
index e72b55234639..000000000000
--- a/src/libs/shouldShowSubscriptionsMenu/types.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-type ShouldShowSubscriptionsMenu = boolean;
-
-export default ShouldShowSubscriptionsMenu;
diff --git a/src/pages/EditReportFieldDate.tsx b/src/pages/EditReportFieldDate.tsx
index 38209ba1083b..06ba24f780ec 100644
--- a/src/pages/EditReportFieldDate.tsx
+++ b/src/pages/EditReportFieldDate.tsx
@@ -24,7 +24,7 @@ type EditReportFieldDatePageProps = {
isRequired: boolean;
/** Callback to fire when the Save button is pressed */
- onSubmit: (form: FormOnyxValues) => void;
+ onSubmit: (form: FormOnyxValues) => void;
};
function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldKey}: EditReportFieldDatePageProps) {
@@ -33,8 +33,8 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f
const inputRef = useRef(null);
const validate = useCallback(
- (value: FormOnyxValues) => {
- const errors: FormInputErrors = {};
+ (value: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
if (isRequired && value[fieldKey].trim() === '') {
errors[fieldKey] = translate('common.error.fieldRequired');
}
@@ -46,7 +46,7 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f
return (
) => {
+ const handleReportFieldChange = (form: FormOnyxValues