diff --git a/.eslintrc.js b/.eslintrc.js
index 5f450f3ae6c2..cfbfdcc8fe91 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -109,7 +109,6 @@ module.exports = {
'plugin:prettier/recommended',
],
plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler', 'lodash', 'deprecation'],
- ignorePatterns: ['lib/**'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: path.resolve(__dirname, './tsconfig.json'),
diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml
index 34a5c356356e..dc4de9ec31a7 100644
--- a/.github/workflows/cla.yml
+++ b/.github/workflows/cla.yml
@@ -4,7 +4,7 @@ on:
issue_comment:
types: [created]
pull_request_target:
- types: [opened, closed, synchronize]
+ types: [opened, synchronize]
jobs:
CLA:
diff --git a/.github/workflows/deployNewHelp.yml b/.github/workflows/deployNewHelp.yml
index 45a4ab7c3620..2d2f551482d2 100644
--- a/.github/workflows/deployNewHelp.yml
+++ b/.github/workflows/deployNewHelp.yml
@@ -55,7 +55,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
- node-version: '20.15.1'
+ node-version: '20.18.0'
# Wil install the _help/package.js
- name: Install Node.js Dependencies
diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml
index d8e706d467ba..c92ab83d1178 100644
--- a/.github/workflows/e2ePerformanceTests.yml
+++ b/.github/workflows/e2ePerformanceTests.yml
@@ -24,33 +24,29 @@ jobs:
runs-on: ubuntu-latest
name: Find the baseline and delta refs, and check for an existing build artifact for that commit
outputs:
- BASELINE_ARTIFACT_FOUND: ${{ steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND }}
- BASELINE_ARTIFACT_WORKFLOW_ID: ${{ steps.checkForExistingArtifact.outputs.ARTIFACT_WORKFLOW_ID }}
- BASELINE_VERSION: ${{ steps.getMostRecentRelease.outputs.VERSION }}
+ BASELINE_REF: ${{ steps.getBaselineRef.outputs.BASELINE_REF }}
DELTA_REF: ${{ steps.getDeltaRef.outputs.DELTA_REF }}
IS_PR_MERGED: ${{ steps.getPullRequestDetails.outputs.IS_MERGED }}
steps:
- uses: actions/checkout@v4
with:
- # The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify (we need a PAT to access the artifact API)
- token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }}
-
- - name: Get most recent release version
- id: getMostRecentRelease
- run: echo "VERSION=$(gh release list --limit 1 | awk '{ print $1 }')" >> "$GITHUB_OUTPUT"
- env:
- GITHUB_TOKEN: ${{ github.token }}
+ fetch-depth: 0 # Fetches the entire history
- - name: Check if there's an existing artifact for this baseline
- id: checkForExistingArtifact
- uses: ./.github/actions/javascript/getArtifactInfo
- with:
- GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }}
- ARTIFACT_NAME: baseline-${{ steps.getMostRecentRelease.outputs.VERSION }}android-artifact-apk
+ - name: Determine "baseline ref" (prev merge commit)
+ id: getBaselineRef
+ run: |
+ # Get the name of the current branch
+ current_branch=$(git rev-parse --abbrev-ref HEAD)
- - name: Skip build if there's already an existing artifact for the baseline
- if: ${{ fromJSON(steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND) }}
- run: echo 'APK for baseline ${{ steps.getMostRecentRelease.outputs.VERSION }} already exists, reusing existing build'
+ if [ "$current_branch" = "main" ]; then
+ # On the main branch, find the previous merge commit
+ previous_merge=$(git rev-list --merges HEAD~1 | head -n 1)
+ else
+ # On a feature branch, find the common ancestor of the current branch and main
+ previous_merge=$(git merge-base HEAD main)
+ fi
+ echo "$previous_merge"
+ echo "BASELINE_REF=$previous_merge" >> "$GITHUB_OUTPUT"
- name: Get pull request details
id: getPullRequestDetails
@@ -84,15 +80,14 @@ jobs:
fi
buildBaseline:
- name: Build apk from latest release as a baseline
+ name: Build apk from baseline
uses: ./.github/workflows/buildAndroid.yml
needs: prep
- if: ${{ !fromJSON(needs.prep.outputs.BASELINE_ARTIFACT_FOUND) }}
secrets: inherit
with:
type: e2e
- ref: ${{ needs.prep.outputs.BASELINE_VERSION }}
- artifact-prefix: baseline-${{ needs.prep.outputs.BASELINE_VERSION }}
+ ref: ${{ needs.prep.outputs.BASELINE_REF }}
+ artifact-prefix: baseline-${{ needs.prep.outputs.BASELINE_REF }}
buildDelta:
name: Build apk from delta ref
@@ -127,9 +122,6 @@ jobs:
with:
name: ${{ needs.buildBaseline.outputs.APK_ARTIFACT_NAME }}
path: zip
- # Set github-token only if the baseline was built in this workflow run:
- github-token: ${{ needs.prep.outputs.BASELINE_ARTIFACT_WORKFLOW_ID && github.token }}
- run-id: ${{ needs.prep.outputs.BASELINE_ARTIFACT_WORKFLOW_ID }}
# The downloaded artifact will be a file named "app-e2e-release.apk" so we have to rename it
- name: Rename baseline APK
diff --git a/.nvmrc b/.nvmrc
index b8e593f5210c..2a393af592b8 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20.15.1
+20.18.0
diff --git a/.prettierignore b/.prettierignore
index c4c88bd11d3e..b428978a1563 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -18,8 +18,6 @@ package-lock.json
*.markdown
# We need to modify the import here specifically, hence we disable prettier to get rid of the sorted imports
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/.well-known/apple-app-site-association b/.well-known/apple-app-site-association
index dce724440adf..54fdd5681ae4 100644
--- a/.well-known/apple-app-site-association
+++ b/.well-known/apple-app-site-association
@@ -2,7 +2,7 @@
"applinks": {
"details": [
{
- "appIDs": ["368M544MTT.com.chat.expensify.chat"],
+ "appIDs": ["368M544MTT.com.chat.expensify.chat", "452835FXHF.com.expensify.expensifylite"],
"components": [
{
"/": "/r/*",
@@ -105,6 +105,6 @@
]
},
"webcredentials": {
- "apps": ["368M544MTT.com.chat.expensify.chat"]
+ "apps": ["368M544MTT.com.chat.expensify.chat", "452835FXHF.com.expensify.expensifylite"]
}
}
diff --git a/README.md b/README.md
index 4a691045e7c2..730e745e368a 100644
--- a/README.md
+++ b/README.md
@@ -274,7 +274,7 @@ web: `npm run symbolicate-release:web`
- Perfetto UI (https://ui.perfetto.dev/)
- Google Chrome's Tracing UI (chrome://tracing)
----
+----
# App Structure and Conventions
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1b937b2b63b2..5ce4d0e5fddf 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009005104
- versionName "9.0.51-4"
+ versionCode 1009005401
+ versionName "9.0.54-1"
// 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/android/app/src/main/java/com/expensify/chat/MainApplication.kt b/android/app/src/main/java/com/expensify/chat/MainApplication.kt
index 2cc8b7780253..f476ad89c5b4 100644
--- a/android/app/src/main/java/com/expensify/chat/MainApplication.kt
+++ b/android/app/src/main/java/com/expensify/chat/MainApplication.kt
@@ -11,9 +11,11 @@ import com.expensify.chat.bootsplash.BootSplashPackage
import com.expensify.chat.shortcutManagerModule.ShortcutManagerPackage
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
+import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
+import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.modules.i18nmanager.I18nUtil
import com.facebook.soloader.SoLoader
@@ -44,6 +46,9 @@ class MainApplication : MultiDexApplication(), ReactApplication {
get() = BuildConfig.IS_HERMES_ENABLED
})
+ override val reactHost: ReactHost
+ get() = getDefaultReactHost(applicationContext, reactNativeHost)
+
override fun onCreate() {
super.onCreate()
ReactFontManager.getInstance().addCustomFont(this, "Expensify New Kansas", R.font.expensify_new_kansas)
@@ -59,7 +64,7 @@ class MainApplication : MultiDexApplication(), ReactApplication {
SoLoader.init(this, /* native exopackage */false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
- load(bridgelessEnabled = false)
+ load()
}
if (BuildConfig.DEBUG) {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false)
diff --git a/babel.config.js b/babel.config.js
index 663eb29d5d2f..3f0fff03736d 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -3,11 +3,14 @@ require('dotenv').config();
const IS_E2E_TESTING = process.env.E2E_TESTING === 'true';
const ReactCompilerConfig = {
- runtimeModule: 'react-compiler-runtime',
+ target: '18',
environment: {
enableTreatRefLikeIdentifiersAsRefs: true,
},
+ // We exclude 'tests' directory from compilation, but still compile components imported in test files.
+ sources: (filename) => !filename.includes('tests/') && !filename.includes('node_modules/'),
};
+
/**
* Setting targets to node 20 to reduce JS bundle size
* It is also recommended by babel:
@@ -52,6 +55,8 @@ const webpack = {
const metro = {
presets: [require('@react-native/babel-preset')],
plugins: [
+ ['babel-plugin-react-compiler', ReactCompilerConfig], // must run first!
+
// This is needed due to a react-native bug: https://github.com/facebook/react-native/issues/29084#issuecomment-1030732709
// It is included in metro-react-native-babel-preset but needs to be before plugin-proposal-class-properties or FlatList will break
'@babel/plugin-transform-flow-strip-types',
@@ -154,11 +159,5 @@ module.exports = (api) => {
const runningIn = api.caller((args = {}) => args.name);
console.debug(' - running in: ', runningIn);
- // don't include react-compiler in jest, because otherwise tests will fail
- if (runningIn !== 'babel-jest') {
- // must run first!
- metro.plugins.unshift(['babel-plugin-react-compiler', ReactCompilerConfig]);
- }
-
return ['metro', 'babel-jest'].includes(runningIn) ? metro : webpack;
};
diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md
index 917c3c007b28..dd913af1c497 100644
--- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md
+++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md
@@ -7,6 +7,8 @@ Our new QuickBooks Desktop integration allows you to automate the import and exp
# Step 1: Configure export settings
The following steps will determine how data will be exported from Expensify to QuickBooks Desktop.
+![Expensify export settings page for the QuickBooks Desktop integration](https://help.expensify.com/assets/images/quickbooks-desktop-export-settings.png){:width="100%"}
+
1. In Expensify, hover over **Settings** and click **Workspaces**.
2. Select the Workspace you want to connect to QuickBooks Desktop.
3. Click the **Connections** tab.
@@ -28,6 +30,8 @@ The following steps will determine how data will be exported from Expensify to Q
The following steps help you determine how data will be imported from QuickBooks Online to Expensify:
+![Expensify coding settings page for the QuickBooks Desktop integration](https://help.expensify.com/assets/images/quickbooks-desktop-coding-settings.png){:width="100%"}
+
1. Click Import under the QuickBooks Online connection.
2. Review each of the following import settings:
- **Chart of Accounts**: The Chart of Accounts is automatically imported from QuickBooks Desktop as categories. This cannot be amended.
@@ -39,6 +43,8 @@ The following steps help you determine how data will be imported from QuickBooks
The following steps help you determine the advanced settings for your connection, like auto-sync and employee invitation settings.
+![Expensify advanced settings page for the QuickBooks Desktop integration](https://help.expensify.com/assets/images/quickbooks-desktop-advanced-settings.png){:width="100%"}
+
1. Click **Advanced** under the QuickBooks Desktop connection.
2. **Enable or disable Auto-Sync**: If enabled, QuickBooks Desktop automatically communicates changes with Expensify to ensure that the data shared between the two systems is up to date. New report approvals/reimbursements will be synced during the next auto-sync period.
diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md
index 06f894ce7ef6..c832667080d5 100644
--- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md
+++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md
@@ -40,7 +40,13 @@ Generally, these errors indicate that there is a credentials issue.
4. Check that you have the correct permissions.
5. Log in to QuickBooks Desktop as an Admin (in single-user mode).
6. Go to **Edit** > **Preferences** > **Integrated Applications** > **Company Preferences**.
-7. Select the Web Connector and click **Properties**.
+
+![Company Preferences page of QuickBooks Desktop](https://help.expensify.com/assets/images/quickbooks-desktop-company-preferences.png){:width="100%"}
+
+7. Select the Web Connector and click **Properties**.
+
+![Web Connector Properties page in QuickBooks Desktop](https://help.expensify.com/assets/images/quickbooks-desktop-access-rights.png){:width="100%"}
+
8. Make sure that the "Allow this application to login automatically" checkbox is selected and click **OK**.
9. Close all windows in QuickBooks.
@@ -98,6 +104,11 @@ Generally, this is the result of not having both the QuickBooks Web Connector an
1. Make sure that the Web Connector and QuickBooks Desktop Company File are both open.
2. In the Web Connector, check that the Last Status is “Ok”.
+
+![QuickBooks Web Connector showing status "OK"](https://help.expensify.com/assets/images/quickbooks-desktop-web-connector.png){:width="100%"}
+
3. Check the Report Comments in Expensify to confirm that the report has been successfully exported to QuickBooks Desktop.
+![Expensify report showing the report was exported](https://help.expensify.com/assets/images/quickbooks-desktop-exported-report-comments.png){:width="100%"}
+
If these general troubleshooting steps don’t work, reach out to Concierge with your Expensify Report ID and a screenshot of your QuickBooks Web Connector.
diff --git a/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md b/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md
new file mode 100644
index 000000000000..5c146b279163
--- /dev/null
+++ b/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md
@@ -0,0 +1,42 @@
+---
+title: Personal and Corporate Karma
+description: Details about Personal and Corporate Karma
+---
+
+# Overview
+
+Expensify.org empowers individuals and communities to eliminate injustice around the world by making giving and volunteering more convenient, meaningful, and collaborative.
+
+## What is the Expensify.org giving model
+
+[Expensify.org](https://www.expensify.org/about) is built on creating a transparent and convenient way to create an emotional connection between donors, volunteers, and recipients.
+
+## Where do Expensify.org funds come from?
+
+Corporate Karma, Personal Karma, and monetary donations.
+
+## What is Personal Karma?
+
+Personal Karma allows individual users to automatically donate a small percentage of their monthly added expenses to Expensify.org.
+
+For every $500 of expenses added, you’ll donate $1 to a related Expensify.org fund. All reported and unreported expenses, including invoice expenses, on the Expenses page are calculated to get the donation amount. Each month, Expensify will charge the billing card on file for the donation amount, and you’ll receive a donation receipt via email.
+
+The fund from your Personal Karma is determined by the expense's MCC (Merchant Category Code). Each MCC supports one of Expensify.org's funds: Climate Justice, Food Security, Housing Equity, Reentry Services, and Youth Advocacy.
+
+## What is Corporate Karma?
+
+Corporate Karma is for companies that want to engage in social responsibility. Each month, the donation is calculated based on the total amount of all approved expense reports, including invoices, across all Workspace.
+
+For every $500 your team spends monthly, your company will donate $1 to a related Expensify.org fund. Expensify will charge the payment card on file for the donation amount each month, and you’ll receive a donation receipt via email.
+
+The fund to which your Corporate Karma goes is determined by the expense's MCC (Merchant Category Code). Each MCC supports one of Expensify.org's funds: Climate Justice, Food Security, Housing Equity, Reentry Services, and Youth Advocacy.
+
+{% include faq-begin.md %}
+
+**How do I opt-in to Personal or Corporate Karma donations?**
+
+You can donate Personal and Corporate Karma to Expensify.org in your company or personal workspace settings.
+
+Go to **Settings** > **Workspaces** > click on your Individual or Group workspace settings and Opt-in to Karma donations.
+
+{% include faq-end.md %}
diff --git a/docs/articles/new-expensify/connections/xero/Connect-to-Xero.md b/docs/articles/new-expensify/connections/xero/Connect-to-Xero.md
index eb35b1589db4..ff1b7fa00f1e 100644
--- a/docs/articles/new-expensify/connections/xero/Connect-to-Xero.md
+++ b/docs/articles/new-expensify/connections/xero/Connect-to-Xero.md
@@ -5,10 +5,10 @@ order: 1
---
{% include info.html %}
-To use the Xero connection, you must have a Xero account and an Expensify Collect plan.
+You must have a Xero account and an Expensify Collect plan to use the Xero connection.
{% include end-info.html %}
-To set up your Xero connection, complete the 4 steps below.
+To set up your Xero connection, complete the steps below.
# Step 1: Connect Expensify to Xero
@@ -29,68 +29,6 @@ To set up your Xero connection, complete the 4 steps below.
![The QuickBooks Online Connect button]({{site.url}}/assets/images/ExpensifyHelp-Xero-3.png){:width="100%"}
-# Step 2: Configure import settings
-
-The following steps help you determine how data will be imported from Xero to Expensify.
-
-
- Under the Accounting settings for your workspace, click Import under the Xero connection.
- Select an option for each of the following settings to determine what information will be imported from Xero into Expensify:
-
- Xero organization : Select which Xero organization your Expensify workspace is connected to. Each organization can only be connected to one workspace at a time.
- Chart of Accounts : Your Xero chart of accounts and any accounts marked as “Show In Expense Claims” will be automatically imported into Expensify as Categories. This cannot be amended.
- Tracking Categories : Choose whether to import your Xero categories for cost centers and regions as tags in Expensify.
- Re-bill Customers : When enabled, Xero customer contacts are imported into Expensify as tags for expense tracking. After exporting to Xero, tagged billable expenses can be included on a sales invoice to your customer.
- Taxes : Choose whether to import tax rates and tax defaults from Xero.
-
-
-
-# Step 3: Configure export settings
-The following steps help you determine how data will be exported from Expensify to Xero.
-
-
- Under the Accounting settings for your workspace, click Export under the Xero connection.
- Review each of the following export settings:
-
- Preferred Exporter : Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
-
-
-{% include info.html %}
-- Other Workspace Admins will still be able to export to Xero.
-- If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin.
-{% include end-info.html %}
-
-
-
- Export Out-of-Pocket Expenses as : All out-of-pocket expenses will be exported as purchase bills. This cannot be amended.
- Purchase Bill Date : Choose whether to use the date of last expense, export date, or submitted date.
- Export invoices as : All invoices exported to Xero will be as a sales invoice. This cannot be amended.
- Export company card expenses as : All company card expenses export to Xero as bank transactions. This cannot be amended.
- Xero Bank Account : Select which bank account will be used to post bank transactions when non-reimbursable expenses are exported.
-
-
-
-# Step 4: Configure advanced settings
-
-The following steps help you determine the advanced settings for your connection, like auto-sync.
-
-
- Under the Accounting settings for your workspace, click Advanced under the Xero connection.
- Select an option for each of the following settings:
-
- Auto-sync : Choose whether to enable Xero to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period. Once you’ve added a business bank account for ACH reimbursement, any reimbursable expenses will be sent to Xero automatically when the report is reimbursed. For non-reimbursable reports, Expensify automatically queues the report to export to Xero after it has completed the approval workflow in Expensify.
- Set Purchase Bill Status : Choose the status of your purchase bills:
-
- Draft
- Awaiting Approval
- Awaiting Payment
-
- Sync Reimbursed Reports : Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in Xero will also show in Expensify as Paid. If enabled, you must also select the Xero account that reimbursements are coming out of, and Expensify will automatically create the payment in Xero.
- Xero Bill Payment Account : If you enable Sync Reimbursed Reports, you must select the Xero Bill Payment account your reimbursements will come from.
- Xero Invoice Collections Account : If you are exporting invoices from Expensify, select the invoice collection account that you want invoices to appear under once they are marked as paid.
-
-
-
{% include faq-begin.md %}
**How do I disconnect Xero from Expensify?**
@@ -99,7 +37,7 @@ The following steps help you determine the advanced settings for your connection
2. Scroll down and click **Workspaces** in the left menu.
3. Select the workspace you want to disconnect from Xero.
4. Click **Accounting** in the left menu.
-5. Click the three dot menu icon to the right of Xero and select **Disconnect**.
+5. Click the three-dot menu icon to the right of Xero and select **Disconnect**.
6. Click **Disconnect** to confirm.
You will no longer see the imported options from Xero.
diff --git a/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png
new file mode 100644
index 000000000000..53c637736c95
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png differ
diff --git a/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png
new file mode 100644
index 000000000000..92e607756de2
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png differ
diff --git a/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png
new file mode 100644
index 000000000000..402afb86cc40
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png differ
diff --git a/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png
new file mode 100644
index 000000000000..7aeb0fdfb7c5
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png differ
diff --git a/docs/assets/images/cardfeeds-01.png b/docs/assets/images/cardfeeds-01.png
new file mode 100644
index 000000000000..ddf318fc05e8
Binary files /dev/null and b/docs/assets/images/cardfeeds-01.png differ
diff --git a/docs/assets/images/cardfeeds-02.png b/docs/assets/images/cardfeeds-02.png
new file mode 100644
index 000000000000..b0f047722444
Binary files /dev/null and b/docs/assets/images/cardfeeds-02.png differ
diff --git a/docs/assets/images/compcard-01.png b/docs/assets/images/compcard-01.png
new file mode 100644
index 000000000000..95b577714833
Binary files /dev/null and b/docs/assets/images/compcard-01.png differ
diff --git a/docs/assets/images/compcard-02.png b/docs/assets/images/compcard-02.png
new file mode 100644
index 000000000000..a34cdbfa1603
Binary files /dev/null and b/docs/assets/images/compcard-02.png differ
diff --git a/docs/assets/images/compcard-03.png b/docs/assets/images/compcard-03.png
new file mode 100644
index 000000000000..1e4bb6776e17
Binary files /dev/null and b/docs/assets/images/compcard-03.png differ
diff --git a/docs/assets/images/csv-01.png b/docs/assets/images/csv-01.png
new file mode 100644
index 000000000000..e6cfe9cf36f6
Binary files /dev/null and b/docs/assets/images/csv-01.png differ
diff --git a/docs/assets/images/csv-02.png b/docs/assets/images/csv-02.png
new file mode 100644
index 000000000000..72ba2b5cf583
Binary files /dev/null and b/docs/assets/images/csv-02.png differ
diff --git a/docs/assets/images/csv-03.png b/docs/assets/images/csv-03.png
new file mode 100644
index 000000000000..4aac1f72893c
Binary files /dev/null and b/docs/assets/images/csv-03.png differ
diff --git a/docs/assets/images/expenses-01.png b/docs/assets/images/expenses-01.png
new file mode 100644
index 000000000000..0169a20b2e2b
Binary files /dev/null and b/docs/assets/images/expenses-01.png differ
diff --git a/docs/assets/images/expenses-02.png b/docs/assets/images/expenses-02.png
new file mode 100644
index 000000000000..1164f341b033
Binary files /dev/null and b/docs/assets/images/expenses-02.png differ
diff --git a/docs/assets/images/expenses-03.png b/docs/assets/images/expenses-03.png
new file mode 100644
index 000000000000..75c06639cb81
Binary files /dev/null and b/docs/assets/images/expenses-03.png differ
diff --git a/docs/assets/images/expenses-04.png b/docs/assets/images/expenses-04.png
new file mode 100644
index 000000000000..16e9b9756d47
Binary files /dev/null and b/docs/assets/images/expenses-04.png differ
diff --git a/docs/assets/images/expenses-05.png b/docs/assets/images/expenses-05.png
new file mode 100644
index 000000000000..cf99d05eb1af
Binary files /dev/null and b/docs/assets/images/expenses-05.png differ
diff --git a/docs/assets/images/invoice-bulk-01.png b/docs/assets/images/invoice-bulk-01.png
new file mode 100644
index 000000000000..1dbf7fa5088d
Binary files /dev/null and b/docs/assets/images/invoice-bulk-01.png differ
diff --git a/docs/assets/images/invoice-bulk-02.png b/docs/assets/images/invoice-bulk-02.png
new file mode 100644
index 000000000000..82e388b0125f
Binary files /dev/null and b/docs/assets/images/invoice-bulk-02.png differ
diff --git a/docs/assets/images/invoice-bulk-03.png b/docs/assets/images/invoice-bulk-03.png
new file mode 100644
index 000000000000..f51abec046b7
Binary files /dev/null and b/docs/assets/images/invoice-bulk-03.png differ
diff --git a/docs/assets/images/invoice-bulk-04.png b/docs/assets/images/invoice-bulk-04.png
new file mode 100644
index 000000000000..35e12a095ba6
Binary files /dev/null and b/docs/assets/images/invoice-bulk-04.png differ
diff --git a/docs/assets/images/invoice-bulk-05.png b/docs/assets/images/invoice-bulk-05.png
new file mode 100644
index 000000000000..c7044c259de2
Binary files /dev/null and b/docs/assets/images/invoice-bulk-05.png differ
diff --git a/docs/assets/images/tax_tracking-01.png b/docs/assets/images/tax_tracking-01.png
new file mode 100644
index 000000000000..a35da6c1848a
Binary files /dev/null and b/docs/assets/images/tax_tracking-01.png differ
diff --git a/docs/assets/images/tax_tracking-02.png b/docs/assets/images/tax_tracking-02.png
new file mode 100644
index 000000000000..4d3df9eda60c
Binary files /dev/null and b/docs/assets/images/tax_tracking-02.png differ
diff --git a/docs/redirects.csv b/docs/redirects.csv
index a7d4d94adb5d..d3672618cfad 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -585,7 +585,8 @@ https://community.expensify.com/discussion/6699/faq-troubleshooting-known-bank-s
https://community.expensify.com/discussion/4730/faq-expenses-are-exporting-to-the-wrong-accounts-whys-that,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings
https://community.expensify.com/discussion/9000/how-to-integrate-with-deel,https://help.expensify.com/articles/expensify-classic/connections/Deel
https://community.expensify.com/categories/expensify-classroom,https://use.expensify.com
-https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription
+https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription
https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Send-Receive-for-Invoices,https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md
https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Bulk-Upload-Multiple-Invoices,https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Add-Invoices-in-Bulk
-https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills
\ No newline at end of file
+https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills
+https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription
diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg
index 76307ce1b460..567a867981e6 100644
Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ
diff --git a/ios/NewApp_Development.mobileprovision.gpg b/ios/NewApp_Development.mobileprovision.gpg
new file mode 100644
index 000000000000..34f034752b7f
Binary files /dev/null and b/ios/NewApp_Development.mobileprovision.gpg differ
diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj
index 96baba0d4e87..b3ec8febb1df 100644
--- a/ios/NewExpensify.xcodeproj/project.pbxproj
+++ b/ios/NewExpensify.xcodeproj/project.pbxproj
@@ -681,7 +681,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/FullStory/tools/FullStoryCommandLine\" \"${CONFIGURATION_BUILD_DIR}/${WRAPPER_NAME}\"\n";
+ shellScript = "if [ \"$CONFIGURATION\" != \"DebugDevelopment\" ]; then\n \"${PODS_ROOT}/FullStory/tools/FullStoryCommandLine\" \"${CONFIGURATION_BUILD_DIR}/${WRAPPER_NAME}\"\nelse\n echo \"Skipping FullStory Asset Uploader phase for DebugDevelopment scheme.\"\nfi\n";
};
5CF45ABA52C0BB0D7B9D139A /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
diff --git a/ios/NewExpensify/AppDelegate.mm b/ios/NewExpensify/AppDelegate.mm
index dc0ef2812031..5608c44823f4 100644
--- a/ios/NewExpensify/AppDelegate.mm
+++ b/ios/NewExpensify/AppDelegate.mm
@@ -88,11 +88,6 @@ - (NSURL *)bundleURL
#endif
}
-- (BOOL)bridgelessEnabled
-{
- return NO;
-}
-
// This methods is needed to support the hardware keyboard shortcuts
- (NSArray *)keyCommands {
return [HardwareShortcuts sharedInstance].keyCommands;
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 871e9aaa4836..361113013ae4 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.0.51
+ 9.0.54
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.51.4
+ 9.0.54.1
FullStory
OrgId
diff --git a/ios/NewExpensify/RCTBootSplash.h b/ios/NewExpensify/RCTBootSplash.h
index 5dc3def635f2..f25f3e28f561 100644
--- a/ios/NewExpensify/RCTBootSplash.h
+++ b/ios/NewExpensify/RCTBootSplash.h
@@ -1,12 +1,4 @@
-//
-// RCTBootSplash.h
-// NewExpensify
-//
-// Created by Mathieu Acthernoene on 07/01/2022.
-//
-
#import
-#import
@interface RCTBootSplash : NSObject
diff --git a/ios/NewExpensify/RCTBootSplash.mm b/ios/NewExpensify/RCTBootSplash.mm
index 3e4a086f07b1..ddb3f2d047ce 100644
--- a/ios/NewExpensify/RCTBootSplash.mm
+++ b/ios/NewExpensify/RCTBootSplash.mm
@@ -2,19 +2,16 @@
#import
-#if RCT_NEW_ARCH_ENABLED
#import
#import
-#else
#import
-#endif
-static NSMutableArray *_resolveQueue = nil;
+static RCTSurfaceHostingProxyRootView *_rootView = nil;
+
static UIView *_loadingView = nil;
-static UIView *_rootView = nil;
-static float _duration = 0;
+static NSMutableArray *_resolveQueue = [[NSMutableArray alloc] init];
+static bool _fade = false;
static bool _nativeHidden = false;
-static bool _transitioning = false;
@implementation RCTBootSplash
@@ -24,14 +21,18 @@ - (dispatch_queue_t)methodQueue {
return dispatch_get_main_queue();
}
++ (BOOL)requiresMainQueueSetup {
+ return NO;
+}
+
+ (void)invalidateBootSplash {
_resolveQueue = nil;
_rootView = nil;
_nativeHidden = false;
}
-+ (bool)isLoadingViewHidden {
- return _loadingView == nil || [_loadingView isHidden];
++ (bool)isLoadingViewVisible {
+ return _loadingView != nil && ![_loadingView isHidden];
}
+ (bool)hasResolveQueue {
@@ -41,7 +42,7 @@ + (bool)hasResolveQueue {
+ (void)clearResolveQueue {
if (![self hasResolveQueue])
return;
-
+
while ([_resolveQueue count] > 0) {
RCTPromiseResolveBlock resolve = [_resolveQueue objectAtIndex:0];
[_resolveQueue removeObjectAtIndex:0];
@@ -49,19 +50,15 @@ + (void)clearResolveQueue {
}
}
-+ (void)hideLoadingView {
- if ([self isLoadingViewHidden])
++ (void)hideAndClearPromiseQueue {
+ if (![self isLoadingViewVisible]) {
return [RCTBootSplash clearResolveQueue];
+ }
- if (_duration > 0) {
+ if (_fade) {
dispatch_async(dispatch_get_main_queue(), ^{
- _transitioning = true;
-
- if (_rootView == nil)
- return;
-
[UIView transitionWithView:_rootView
- duration:_duration / 1000.0
+ duration:0.250
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
_loadingView.hidden = YES;
@@ -70,7 +67,6 @@ + (void)hideLoadingView {
[_loadingView removeFromSuperview];
_loadingView = nil;
- _transitioning = false;
return [RCTBootSplash clearResolveQueue];
}];
});
@@ -85,30 +81,9 @@ + (void)hideLoadingView {
+ (void)initWithStoryboard:(NSString * _Nonnull)storyboardName
rootView:(UIView * _Nullable)rootView {
- if (rootView == nil
-#ifdef RCT_NEW_ARCH_ENABLED
- || ![rootView isKindOfClass:[RCTSurfaceHostingProxyRootView class]]
-#else
- || ![rootView isKindOfClass:[RCTRootView class]]
-#endif
- || _rootView != nil
- || [self hasResolveQueue] // hide has already been called, abort init
- || RCTRunningInAppExtension())
+ if (RCTRunningInAppExtension()) {
return;
-
-#ifdef RCT_NEW_ARCH_ENABLED
- RCTSurfaceHostingProxyRootView *proxy = (RCTSurfaceHostingProxyRootView *)rootView;
- _rootView = (RCTSurfaceHostingView *)proxy.surface.view;
-#else
- _rootView = (RCTRootView *)rootView;
-#endif
-
- UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
-
- _loadingView = [[storyboard instantiateInitialViewController] view];
- _loadingView.hidden = NO;
-
- [_rootView addSubview:_loadingView];
+ }
[NSTimer scheduledTimerWithTimeInterval:0.35
repeats:NO
@@ -117,19 +92,35 @@ + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName
_nativeHidden = true;
// hide has been called before native launch screen fade out
- if ([self hasResolveQueue])
- [self hideLoadingView];
+ if ([_resolveQueue count] > 0) {
+ [self hideAndClearPromiseQueue];
+ }
}];
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(onJavaScriptDidLoad)
- name:RCTJavaScriptDidLoadNotification
- object:nil];
+ if (rootView != nil) {
+ _rootView = (RCTSurfaceHostingProxyRootView *)rootView;
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(onJavaScriptDidFailToLoad)
- name:RCTJavaScriptDidFailToLoadNotification
- object:nil];
+ UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
+
+ _loadingView = [[storyboard instantiateInitialViewController] view];
+ _loadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ _loadingView.frame = _rootView.bounds;
+ _loadingView.center = (CGPoint){CGRectGetMidX(_rootView.bounds), CGRectGetMidY(_rootView.bounds)};
+ _loadingView.hidden = NO;
+
+ [_rootView disableActivityIndicatorAutoHide:YES];
+ [_rootView setLoadingView:_loadingView];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(onJavaScriptDidLoad)
+ name:RCTJavaScriptDidLoadNotification
+ object:nil];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(onJavaScriptDidFailToLoad)
+ name:RCTJavaScriptDidFailToLoadNotification
+ object:nil];
+ }
}
+ (void)onJavaScriptDidLoad {
@@ -137,50 +128,51 @@ + (void)onJavaScriptDidLoad {
}
+ (void)onJavaScriptDidFailToLoad {
- [self hideLoadingView];
+ [self hideAndClearPromiseQueue];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-- (void)hide:(double)duration
- resolve:(RCTPromiseResolveBlock)resolve
- reject:(RCTPromiseRejectBlock)reject {
- if (_resolveQueue == nil)
- _resolveQueue = [[NSMutableArray alloc] init];
+- (NSDictionary *)constantsToExport {
+ UIWindow *window = RCTKeyWindow();
+ __block bool darkModeEnabled = false;
- [_resolveQueue addObject:resolve];
+ RCTUnsafeExecuteOnMainQueueSync(^{
+ darkModeEnabled = window != nil && window.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
+ });
- if ([RCTBootSplash isLoadingViewHidden] || RCTRunningInAppExtension())
- return [RCTBootSplash clearResolveQueue];
+ return @{
+ @"darkModeEnabled": @(darkModeEnabled)
+ };
+}
+
+- (void)hideImpl:(BOOL)fade
+ resolve:(RCTPromiseResolveBlock)resolve {
+ if (_resolveQueue == nil)
+ _resolveQueue = [[NSMutableArray alloc] init];
+
+ [_resolveQueue addObject:resolve];
+
+ if (![RCTBootSplash isLoadingViewVisible] || RCTRunningInAppExtension())
+ return [RCTBootSplash clearResolveQueue];
- _duration = lroundf((float)duration);
+ _fade = fade;
- if (_nativeHidden)
- return [RCTBootSplash hideLoadingView];
+ if (_nativeHidden)
+ return [RCTBootSplash hideAndClearPromiseQueue];
}
-- (void)getVisibilityStatus:(RCTPromiseResolveBlock)resolve
- reject:(RCTPromiseRejectBlock)reject {
- if ([RCTBootSplash isLoadingViewHidden])
- return resolve(@"hidden");
- else if (_transitioning)
- return resolve(@"transitioning");
- else
- return resolve(@"visible");
+- (void)isVisibleImpl:(RCTPromiseResolveBlock)resolve {
+ resolve(@([RCTBootSplash isLoadingViewVisible]));
}
-RCT_REMAP_METHOD(hide,
- resolve:(RCTPromiseResolveBlock)resolve
- rejecte:(RCTPromiseRejectBlock)reject) {
- [self hide:0
- resolve:resolve
- reject:reject];
+RCT_EXPORT_METHOD(hide:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self hideImpl:0 resolve:resolve];
}
-RCT_REMAP_METHOD(getVisibilityStatus,
- getVisibilityStatusWithResolve:(RCTPromiseResolveBlock)resolve
- rejecte:(RCTPromiseRejectBlock)reject) {
- [self getVisibilityStatus:resolve
- reject:reject];
+RCT_EXPORT_METHOD(isVisible:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self isVisibleImpl:resolve];
}
@end
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 3ec04f0457fa..cb867d7af0b5 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.51
+ 9.0.54
CFBundleSignature
????
CFBundleVersion
- 9.0.51.4
+ 9.0.54.1
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index f87696730696..c7c9879bb2ab 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.0.51
+ 9.0.54
CFBundleVersion
- 9.0.51.4
+ 9.0.54.1
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/NotificationServiceExtension/NotificationService.swift b/ios/NotificationServiceExtension/NotificationService.swift
index e489cb368d17..b588c6be1d0f 100644
--- a/ios/NotificationServiceExtension/NotificationService.swift
+++ b/ios/NotificationServiceExtension/NotificationService.swift
@@ -8,12 +8,18 @@
import AirshipServiceExtension
import os.log
import Intents
+import AppLogs
class NotificationService: UANotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "com.expensify.chat.dev.NotificationServiceExtension", category: "NotificationService")
+ let appLogs: AppLogs = .init()
+
+ deinit {
+ appLogs.forwardLogsTo(appGroup: "group.com.expensify.new")
+ }
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
os_log("[NotificationService] didReceive() - received notification", log: log)
@@ -42,7 +48,7 @@ class NotificationService: UANotificationServiceExtension {
do {
notificationData = try parsePayload(notificationContent: notificationContent)
} catch ExpError.runtimeError(let errorMessage) {
- os_log("[NotificationService] configureCommunicationNotification() - couldn't parse the payload '%@'", log: log, type: .error, errorMessage)
+ os_log("[NotificationService] configureCommunicationNotification() - couldn't parse the payload '%{public}@'", log: log, type: .error, errorMessage)
contentHandler(notificationContent)
return
} catch {
@@ -212,7 +218,7 @@ class NotificationService: UANotificationServiceExtension {
let data = try Data(contentsOf: url)
return INImage(imageData: data)
} catch {
- os_log("[NotificationService] fetchINImage() - failed to fetch avatar. reportActionID: %@", log: self.log, type: .error, reportActionID)
+ os_log("[NotificationService] fetchINImage() - failed to fetch avatar. reportActionID: %{public}@", log: self.log, type: .error, reportActionID)
return nil
}
}
diff --git a/ios/Podfile b/ios/Podfile
index e807089c26b9..4d139711ef01 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -119,6 +119,7 @@ end
target 'NotificationServiceExtension' do
pod 'AirshipServiceExtension'
+ pod 'AppLogs', :path => '../node_modules/react-native-app-logs/AppLogsPod'
end
pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz'
\ No newline at end of file
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 1242ab7a5a39..9a706cc4e8aa 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -26,6 +26,7 @@ PODS:
- AppAuth/Core (1.7.5)
- AppAuth/ExternalUserAgent (1.7.5):
- AppAuth/Core
+ - AppLogs (0.1.0)
- boost (1.84.0)
- DoubleConversion (1.1.6)
- EXAV (14.0.7):
@@ -1564,6 +1565,27 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
+ - react-native-app-logs (0.3.1):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
- react-native-blob-util (0.19.4):
- DoubleConversion
- glog
@@ -2373,7 +2395,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.164):
+ - RNLiveMarkdown (0.1.176):
- DoubleConversion
- glog
- hermes-engine
@@ -2393,9 +2415,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNLiveMarkdown/newarch (= 0.1.164)
+ - RNLiveMarkdown/newarch (= 0.1.176)
- Yoga
- - RNLiveMarkdown/newarch (0.1.164):
+ - RNLiveMarkdown/newarch (0.1.176):
- DoubleConversion
- glog
- hermes-engine
@@ -2702,6 +2724,7 @@ PODS:
DEPENDENCIES:
- AirshipServiceExtension
+ - AppLogs (from `../node_modules/react-native-app-logs/AppLogsPod`)
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXAV (from `../node_modules/expo-av/ios`)
@@ -2751,6 +2774,7 @@ DEPENDENCIES:
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- "react-native-airship (from `../node_modules/@ua/react-native-airship`)"
+ - react-native-app-logs (from `../node_modules/react-native-app-logs`)
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-config (from `../node_modules/react-native-config`)
@@ -2864,6 +2888,8 @@ SPEC REPOS:
- Turf
EXTERNAL SOURCES:
+ AppLogs:
+ :path: "../node_modules/react-native-app-logs/AppLogsPod"
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
DoubleConversion:
@@ -2959,6 +2985,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-airship:
:path: "../node_modules/@ua/react-native-airship"
+ react-native-app-logs:
+ :path: "../node_modules/react-native-app-logs"
react-native-blob-util:
:path: "../node_modules/react-native-blob-util"
react-native-cameraroll:
@@ -3109,6 +3137,7 @@ SPEC CHECKSUMS:
AirshipFrameworkProxy: dbd862dc6fb21b13e8b196458d626123e2a43a50
AirshipServiceExtension: 9c73369f426396d9fb9ff222d86d842fac76ba46
AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
+ AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0
boost: 26992d1adf73c1c7676360643e687aee6dda994b
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
EXAV: afa491e598334bbbb92a92a2f4dd33d7149ad37f
@@ -3184,6 +3213,7 @@ SPEC CHECKSUMS:
React-Mapbuffer: 1c08607305558666fd16678b85ef135e455d5c96
React-microtasksnativemodule: f13f03163b6a5ec66665dfe80a0df4468bb766a6
react-native-airship: e10f6823d8da49bbcb2db4bdb16ff954188afccc
+ react-native-app-logs: b8a104816aafc78cd0965e923452de88dcf8ec67
react-native-blob-util: 221c61c98ae507b758472ac4d2d489119d1a6c44
react-native-cameraroll: 478a0c1fcdd39f08f6ac272b7ed06e92b2c7c129
react-native-config: 742a9e0a378a78d0eaff1fb3477d8c0ae222eb51
@@ -3242,7 +3272,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: b2bd97a6f1206be16cf6536c092fe39f986aca34
+ RNLiveMarkdown: 0b8756147a5e8eeea98d3e1187c0c27d5a96d1ff
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4
RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28
@@ -3261,6 +3291,6 @@ SPEC CHECKSUMS:
VisionCamera: c6c8aa4b028501fc87644550fbc35a537d4da3fb
Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8
-PODFILE CHECKSUM: a07e55247056ec5d84d1af31d694506efff3cfe2
+PODFILE CHECKSUM: 15e2f095b9c80d658459723edf84005a6867debf
COCOAPODS: 1.15.2
diff --git a/jest/setup.ts b/jest/setup.ts
index 6901ad3c66f3..7dbe91c32fda 100644
--- a/jest/setup.ts
+++ b/jest/setup.ts
@@ -1,5 +1,6 @@
/* eslint-disable max-classes-per-file */
import '@shopify/flash-list/jestSetup';
+import type * as RNAppLogs from 'react-native-app-logs';
import 'react-native-gesture-handler/jestSetup';
import type * as RNKeyboardController from 'react-native-keyboard-controller';
import mockStorage from 'react-native-onyx/dist/storage/__mocks__';
@@ -75,6 +76,8 @@ jest.mock('react-native-reanimated', () => ({
jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest'));
+jest.mock('react-native-app-logs', () => require('react-native-app-logs/jest'));
+
jest.mock('@src/libs/actions/Timing', () => ({
start: jest.fn(),
end: jest.fn(),
diff --git a/lib/react-compiler-runtime/index.js b/lib/react-compiler-runtime/index.js
deleted file mode 100644
index 54e88d2b703a..000000000000
--- a/lib/react-compiler-runtime/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// lib/react-compiler-runtime.js
-const $empty = Symbol.for("react.memo_cache_sentinel");
-const React = require('react');
-/**
- * DANGER: this hook is NEVER meant to be called directly!
- *
- * Note that this is a temporary userspace implementation of this function
- * from React 19. It is not as efficient and may invalidate more frequently
- * than the official API. Better to upgrade to React 19 as soon as we can.
- **/
-export function c(size) {
- return React.useState(() => {
- const $ = new Array(size);
- for (let ii = 0; ii < size; ii++) {
- $[ii] = $empty;
- }
- // @ts-ignore
- $[$empty] = true;
- return $;
- })[0];
-}
diff --git a/lib/react-compiler-runtime/package.json b/lib/react-compiler-runtime/package.json
deleted file mode 100644
index 3a0323538b6e..000000000000
--- a/lib/react-compiler-runtime/package.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "react-compiler-runtime",
- "version": "0.0.1",
- "description": "Runtime for React Compiler",
- "license": "MIT",
- "main": "index.js",
- "dependencies": {
- "react": "18.3.1"
- }
-}
diff --git a/package-lock.json b/package-lock.json
index bc99c7e363b8..7202ef76ab66 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
{
"name": "new.expensify",
- "version": "9.0.51-4",
+ "version": "9.0.54-1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.51-4",
+ "version": "9.0.54-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.164",
+ "@expensify/react-native-live-markdown": "0.1.176",
"@expo/metro-runtime": "~3.2.3",
"@firebase/app": "^0.10.10",
"@firebase/performance": "^0.6.8",
@@ -51,7 +51,7 @@
"date-fns-tz": "^3.2.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "2.0.94",
+ "expensify-common": "2.0.100",
"expo": "51.0.31",
"expo-av": "14.0.7",
"expo-image": "1.12.15",
@@ -77,6 +77,7 @@
"react-map-gl": "^7.1.3",
"react-native": "0.75.2",
"react-native-android-location-enabler": "^2.0.1",
+ "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a",
"react-native-blob-util": "0.19.4",
"react-native-collapsible": "^1.6.2",
"react-native-config": "1.5.3",
@@ -203,7 +204,7 @@
"babel-jest": "29.4.1",
"babel-loader": "^9.1.3",
"babel-plugin-module-resolver": "^5.0.0",
- "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725",
+ "babel-plugin-react-compiler": "^19.0.0-beta-8a03594-20241020",
"babel-plugin-react-native-web": "^0.18.7",
"babel-plugin-transform-remove-console": "^6.9.4",
"clean-webpack-plugin": "^4.0.0",
@@ -218,13 +219,13 @@
"electron-builder": "25.0.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-config-expensify": "^2.0.60",
+ "eslint-config-expensify": "^2.0.66",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jsdoc": "^46.2.6",
"eslint-plugin-lodash": "^7.4.0",
- "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
+ "eslint-plugin-react-compiler": "^19.0.0-beta-8a03594-20241020",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^6.2.2",
@@ -247,8 +248,8 @@
"portfinder": "^1.0.28",
"prettier": "^2.8.8",
"pusher-js-mock": "^0.3.3",
- "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725",
- "react-compiler-runtime": "file:./lib/react-compiler-runtime",
+ "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020",
+ "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020",
"react-is": "^18.3.1",
"react-native-clean-project": "^4.0.0-alpha4.0",
"react-test-renderer": "18.3.1",
@@ -274,16 +275,8 @@
"xlsx": "file:vendor/xlsx-0.20.3.tgz"
},
"engines": {
- "node": "20.15.1",
- "npm": "10.7.0"
- }
- },
- "lib/react-compiler-runtime": {
- "version": "0.0.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "react": "18.3.1"
+ "node": "20.18.0",
+ "npm": "10.8.2"
}
},
"node_modules/@actions/core": {
@@ -614,9 +607,10 @@
}
},
"node_modules/@babel/eslint-parser": {
- "version": "7.24.7",
+ "version": "7.25.8",
+ "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.8.tgz",
+ "integrity": "sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
"eslint-visitor-keys": "^2.1.0",
@@ -3636,9 +3630,10 @@
}
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.164",
- "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.164.tgz",
- "integrity": "sha512-x1/Oa+I1AI82xWEFYd2kSkSj4rZ1q2JG4aEDomUHSqcNjuQetQPw9kVFN5DaLHt0Iu0iKEUrXIhy5LpMSHJQLg==",
+ "version": "0.1.176",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.176.tgz",
+ "integrity": "sha512-0IS0Rfl0qYqrE2V8jsVX58c4K/zxeNC7o1CAL9Xu+HTbTtD58Yu5gOOwp5AljkS2qdPR86swGRZyLXGkGRKkPg==",
+ "license": "MIT",
"workspaces": [
"parser",
"example",
@@ -17982,33 +17977,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/babel-eslint": {
- "version": "10.1.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.0.0",
- "@babel/parser": "^7.7.0",
- "@babel/traverse": "^7.7.0",
- "@babel/types": "^7.7.0",
- "eslint-visitor-keys": "^1.0.0",
- "resolve": "^1.12.0"
- },
- "engines": {
- "node": ">=6"
- },
- "peerDependencies": {
- "eslint": ">= 4.12.1"
- }
- },
- "node_modules/babel-eslint/node_modules/eslint-visitor-keys": {
- "version": "1.3.0",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/babel-jest": {
"version": "29.4.1",
"dev": true,
@@ -18370,9 +18338,10 @@
}
},
"node_modules/babel-plugin-react-compiler": {
- "version": "0.0.0-experimental-334f00b-20240725",
+ "version": "19.0.0-beta-8a03594-20241020",
+ "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.0.0-beta-8a03594-20241020.tgz",
+ "integrity": "sha512-Wk0748DZzQEmjkEN4SbBujM5al4q5TfRBapA32ax0AID/Yek3emS+eyCvPvb4zPddYJTAF4LaJNLt8uHYfdKAQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/generator": "7.2.0",
"@babel/types": "^7.19.0",
@@ -22831,16 +22800,16 @@
}
},
"node_modules/eslint-config-expensify": {
- "version": "2.0.60",
- "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.60.tgz",
- "integrity": "sha512-VlulvhEasWeX2g+AXC4P91KA9czzX+aI3VSdJlZwm99GLOdfv7mM0JyO8vbqomjWNUxvLyJeJjmI02t2+fL/5Q==",
+ "version": "2.0.66",
+ "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.66.tgz",
+ "integrity": "sha512-6L9EIAiOxZnqOcFEsIwEUmX0fvglvboyqQh7LTqy+1O2h2W3mmrMSx87ymXeyFMg1nJQtqkFnrLv5ENGS0QC3Q==",
"dev": true,
"dependencies": {
+ "@babel/eslint-parser": "^7.25.7",
"@lwc/eslint-plugin-lwc": "^1.7.2",
"@typescript-eslint/parser": "^7.12.0",
"@typescript-eslint/rule-tester": "^7.16.1",
"@typescript-eslint/utils": "^7.12.0",
- "babel-eslint": "^10.1.0",
"eslint": "^8.56.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-airbnb-base": "15.0.0",
@@ -23556,9 +23525,10 @@
}
},
"node_modules/eslint-plugin-react-compiler": {
- "version": "0.0.0-experimental-9ed098e-20240725",
+ "version": "19.0.0-beta-8a03594-20241020",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-8a03594-20241020.tgz",
+ "integrity": "sha512-bYg1COih1s3r14IV/AKdQs/SN7CQmNI0ZaMtPdgZ6gp1S1Q/KGP9P43w7R6dHJ4wYpuMBvekNJHQdVu+x6UM+A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
@@ -24080,9 +24050,9 @@
}
},
"node_modules/expensify-common": {
- "version": "2.0.94",
- "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.94.tgz",
- "integrity": "sha512-Cco5X6u4IL5aQlFqa2IgGgR+vAffYLxpPN2d7bzfptW/pRLY2L2JRJohgvXEswlCcTKFVt4nIJ4bx9YIOvzxBA==",
+ "version": "2.0.100",
+ "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.100.tgz",
+ "integrity": "sha512-mektI+OuTywYU47Valjsn2+kLQ1/Wc9sWCY1/a0Vo8IHTXroQWvbKs5IXlkiqODi4SRonVZwOL3ha/oJD7o7nQ==",
"dependencies": {
"awesome-phonenumber": "^5.4.0",
"classnames": "2.5.0",
@@ -34101,9 +34071,10 @@
}
},
"node_modules/react-compiler-healthcheck": {
- "version": "0.0.0-experimental-b130d5f-20240625",
+ "version": "19.0.0-beta-8a03594-20241020",
+ "resolved": "https://registry.npmjs.org/react-compiler-healthcheck/-/react-compiler-healthcheck-19.0.0-beta-8a03594-20241020.tgz",
+ "integrity": "sha512-wupgZ4fASQ+oRI88V6QIERKCHZUo6322LXlH8EotsWQDc8c4EXgPdkZHO/zH+zDh4Np4qZM36bFbZgHPXtI0FA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
@@ -34186,8 +34157,13 @@
}
},
"node_modules/react-compiler-runtime": {
- "resolved": "lib/react-compiler-runtime",
- "link": true
+ "version": "19.0.0-beta-8a03594-20241020",
+ "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.0.0-beta-8a03594-20241020.tgz",
+ "integrity": "sha512-YWl8SjxsWGU1dpxHvWS0vxTkpeLXTZ/Y7IVzwZGj6yAfXOEie1MduuAR0TFiGeV0RxFLp5jKUIWl+ZglN4dMQw==",
+ "dev": true,
+ "peerDependencies": {
+ "react": "^18.2.0 || ^19.0.0"
+ }
},
"node_modules/react-content-loader": {
"version": "7.0.0",
@@ -34426,6 +34402,18 @@
"prop-types": "^15.7.2"
}
},
+ "node_modules/react-native-app-logs": {
+ "version": "0.3.1",
+ "resolved": "git+ssh://git@github.com/margelo/react-native-app-logs.git#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a",
+ "integrity": "sha512-GFZFbUe9bUIbuH2zTAS7JAXCAIYnyf4cTnsz6pSzYCl3F+nF+O3fRa5ZM8P7zr+wTG7fZoVs0b6XFfcFUcxY2A==",
+ "workspaces": [
+ "example"
+ ],
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-blob-util": {
"version": "0.19.4",
"license": "MIT",
@@ -35427,8 +35415,8 @@
"underscore": "^1.13.6"
},
"engines": {
- "node": ">=20.15.1",
- "npm": ">=10.7.0"
+ "node": ">=20.18.0",
+ "npm": ">=10.8.2"
},
"peerDependencies": {
"idb-keyval": "^6.2.1",
diff --git a/package.json b/package.json
index 509ef020b6a6..f0425a747967 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.51-4",
+ "version": "9.0.54-1",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -67,7 +67,7 @@
},
"dependencies": {
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.164",
+ "@expensify/react-native-live-markdown": "0.1.176",
"@expo/metro-runtime": "~3.2.3",
"@firebase/app": "^0.10.10",
"@firebase/performance": "^0.6.8",
@@ -107,7 +107,7 @@
"date-fns-tz": "^3.2.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "2.0.94",
+ "expensify-common": "2.0.100",
"expo": "51.0.31",
"expo-av": "14.0.7",
"expo-image": "1.12.15",
@@ -133,6 +133,7 @@
"react-map-gl": "^7.1.3",
"react-native": "0.75.2",
"react-native-android-location-enabler": "^2.0.1",
+ "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a",
"react-native-blob-util": "0.19.4",
"react-native-collapsible": "^1.6.2",
"react-native-config": "1.5.3",
@@ -259,7 +260,7 @@
"babel-jest": "29.4.1",
"babel-loader": "^9.1.3",
"babel-plugin-module-resolver": "^5.0.0",
- "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725",
+ "babel-plugin-react-compiler": "^19.0.0-beta-8a03594-20241020",
"babel-plugin-react-native-web": "^0.18.7",
"babel-plugin-transform-remove-console": "^6.9.4",
"clean-webpack-plugin": "^4.0.0",
@@ -274,13 +275,13 @@
"electron-builder": "25.0.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-config-expensify": "^2.0.60",
+ "eslint-config-expensify": "^2.0.66",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jsdoc": "^46.2.6",
"eslint-plugin-lodash": "^7.4.0",
- "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
+ "eslint-plugin-react-compiler": "^19.0.0-beta-8a03594-20241020",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^6.2.2",
@@ -303,8 +304,8 @@
"portfinder": "^1.0.28",
"prettier": "^2.8.8",
"pusher-js-mock": "^0.3.3",
- "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725",
- "react-compiler-runtime": "file:./lib/react-compiler-runtime",
+ "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020",
+ "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020",
"react-is": "^18.3.1",
"react-native-clean-project": "^4.0.0-alpha4.0",
"react-test-renderer": "18.3.1",
@@ -376,7 +377,7 @@
]
},
"engines": {
- "node": "20.15.1",
- "npm": "10.7.0"
+ "node": "20.18.0",
+ "npm": "10.8.2"
}
}
diff --git a/patches/@react-native-firebase+app+12.9.3+002+bridgeless.patch b/patches/@react-native-firebase+app+12.9.3+002+bridgeless.patch
new file mode 100644
index 000000000000..a085cdbcfbe2
--- /dev/null
+++ b/patches/@react-native-firebase+app+12.9.3+002+bridgeless.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/@react-native-firebase/app/lib/internal/registry/nativeModule.js b/node_modules/@react-native-firebase/app/lib/internal/registry/nativeModule.js
+index 03f001c..23d467d 100644
+--- a/node_modules/@react-native-firebase/app/lib/internal/registry/nativeModule.js
++++ b/node_modules/@react-native-firebase/app/lib/internal/registry/nativeModule.js
+@@ -65,7 +65,7 @@ function nativeModuleWrapped(namespace, NativeModule, argToPrepend) {
+ return NativeModule;
+ }
+
+- const properties = Object.keys(NativeModule);
++ const properties = Object.keys(Object.getPrototypeOf(NativeModule));
+
+ for (let i = 0, len = properties.length; i < len; i++) {
+ const property = properties[i];
diff --git a/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch b/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch
new file mode 100644
index 000000000000..b840e3da7b12
--- /dev/null
+++ b/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch
@@ -0,0 +1,83 @@
+diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt
+index 5bebc1b..80a4be4 100644
+--- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt
++++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt
+@@ -5,6 +5,8 @@ import com.facebook.react.bridge.ReactApplicationContext
+ import com.facebook.react.uimanager.ThemedReactContext
+ import com.facebook.react.uimanager.annotations.ReactProp
+ import com.facebook.react.viewmanagers.RNMBXRasterSourceManagerInterface
++import com.rnmapbox.rnmbx.events.constants.EventKeys
++import com.rnmapbox.rnmbx.events.constants.eventMapOf
+ import javax.annotation.Nonnull
+
+ class RNMBXRasterSourceManager(reactApplicationContext: ReactApplicationContext) :
+@@ -26,7 +28,10 @@ class RNMBXRasterSourceManager(reactApplicationContext: ReactApplicationContext)
+ }
+
+ override fun customEvents(): Map? {
+- return null
++ return eventMapOf(
++ EventKeys.RASTER_SOURCE_LAYER_CLICK to "onMapboxRasterSourcePress",
++ EventKeys.MAP_ANDROID_CALLBACK to "onAndroidCallback"
++ )
+ }
+
+ companion object {
+diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt
+index d059b2c..3882f1e 100644
+--- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt
++++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt
+@@ -4,35 +4,37 @@ private fun ns(name: String): String {
+ val namespace = "rct.mapbox"
+ return String.format("%s.%s", namespace, name)
+ }
++
+ enum class EventKeys(val value: String) {
+ // map events
+- MAP_CLICK(ns("map.press")),
+- MAP_LONG_CLICK(ns("map.longpress")),
+- MAP_ONCHANGE(ns("map.change")),
+- MAP_ON_LOCATION_CHANGE(ns("map.location.change")),
+- MAP_ANDROID_CALLBACK(ns("map.androidcallback")),
+- MAP_USER_TRACKING_MODE_CHANGE(ns("map.usertrackingmodechange")),
++ MAP_CLICK("topPress"),
++ MAP_LONG_CLICK("topLongPress"),
++ MAP_ONCHANGE("topMapChange"),
++ MAP_ON_LOCATION_CHANGE("topLocationChange"),
++ MAP_ANDROID_CALLBACK("topAndroidCallback"),
++ MAP_USER_TRACKING_MODE_CHANGE("topUserTrackingModeChange"),
+
+ // point annotation events
+- POINT_ANNOTATION_SELECTED(ns("pointannotation.selected")),
+- POINT_ANNOTATION_DESELECTED(ns("pointannotation.deselected")),
+- POINT_ANNOTATION_DRAG_START(ns("pointannotation.dragstart")),
+- POINT_ANNOTATION_DRAG(ns("pointannotation.drag")),
+- POINT_ANNOTATION_DRAG_END(ns("pointannotation.dragend")),
++ POINT_ANNOTATION_SELECTED("topMapboxPointAnnotationSelected"),
++ POINT_ANNOTATION_DESELECTED("topMapboxPointAnnotationDeselected"),
++ POINT_ANNOTATION_DRAG_START("topMapboxPointAnnotationDragStart"),
++ POINT_ANNOTATION_DRAG("topMapboxPointAnnotationDrag"),
++ POINT_ANNOTATION_DRAG_END("topMapboxPointAnnotationDragEnd"),
+
+ // source events
+- SHAPE_SOURCE_LAYER_CLICK(ns("shapesource.layer.pressed")),
+- VECTOR_SOURCE_LAYER_CLICK(ns("vectorsource.layer.pressed")),
+- RASTER_SOURCE_LAYER_CLICK(ns("rastersource.layer.pressed")),
++ SHAPE_SOURCE_LAYER_CLICK("topMapboxShapeSourcePress"),
++ VECTOR_SOURCE_LAYER_CLICK("topMapboxVectorSourcePress"),
++ RASTER_SOURCE_LAYER_CLICK("topMapboxRasterSourcePress"),
+
+ // images event
+- IMAGES_MISSING(ns("images.missing")),
++ IMAGES_MISSING("topImageMissing"),
+
+ // location events
++ // TODO: not sure about this one since it is not registered anywhere
+ USER_LOCATION_UPDATE(ns("user.location.update")),
+
+ // viewport events
+- VIEWPORT_STATUS_CHANGE(ns("viewport.statuschange"))
++ VIEWPORT_STATUS_CHANGE("topStatusChanged")
+ }
+
+ fun eventMapOf(vararg values: Pair): Map {
diff --git a/patches/lottie-react-native+6.5.1.patch b/patches/lottie-react-native+6.5.1+001+recycling.patch
similarity index 100%
rename from patches/lottie-react-native+6.5.1.patch
rename to patches/lottie-react-native+6.5.1+001+recycling.patch
diff --git a/patches/lottie-react-native+6.5.1+002+bridgeless.patch b/patches/lottie-react-native+6.5.1+002+bridgeless.patch
new file mode 100644
index 000000000000..854d26f9beb9
--- /dev/null
+++ b/patches/lottie-react-native+6.5.1+002+bridgeless.patch
@@ -0,0 +1,25 @@
+diff --git a/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationFailureEvent.kt b/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationFailureEvent.kt
+index aa538d3..0185eaf 100644
+--- a/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationFailureEvent.kt
++++ b/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationFailureEvent.kt
+@@ -21,6 +21,6 @@ constructor(surfaceId: Int, viewId: Int, private val error: Throwable) :
+ }
+
+ companion object {
+- const val EVENT_NAME = "topAnimationFailureEvent"
++ const val EVENT_NAME = "topAnimationFailure"
+ }
+ }
+\ No newline at end of file
+diff --git a/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationLoadedEvent.kt b/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationLoadedEvent.kt
+index f17cff9..4ebe3ba 100644
+--- a/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationLoadedEvent.kt
++++ b/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationLoadedEvent.kt
+@@ -16,6 +16,6 @@ class OnAnimationLoadedEvent constructor(surfaceId: Int, viewId: Int) :
+ }
+
+ companion object {
+- const val EVENT_NAME = "topAnimationLoadedEvent"
++ const val EVENT_NAME = "topAnimationLoaded"
+ }
+ }
diff --git a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+001+initial.patch b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+initial.patch
similarity index 66%
rename from patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+001+initial.patch
rename to patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+initial.patch
index d7c02701a636..03b386587338 100644
--- a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+001+initial.patch
+++ b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+initial.patch
@@ -1,8 +1,8 @@
diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js
-index b427385..4bf23db 100755
+index 5a4060d..460339b 100755
--- a/node_modules/react-compiler-healthcheck/dist/index.js
+++ b/node_modules/react-compiler-healthcheck/dist/index.js
-@@ -69154,7 +69154,7 @@ var reactCompilerCheck = {
+@@ -56969,7 +56969,7 @@ var reactCompilerCheck = {
compile(source, path);
}
},
@@ -11,11 +11,11 @@ index b427385..4bf23db 100755
const totalComponents =
SucessfulCompilation.length +
countUniqueLocInEvents(OtherFailures) +
-@@ -69164,6 +69164,50 @@ var reactCompilerCheck = {
+@@ -56979,6 +56979,50 @@ var reactCompilerCheck = {
`Successfully compiled ${SucessfulCompilation.length} out of ${totalComponents} components.`
)
);
-+
++
+ if (verbose) {
+ for (const compilation of [...SucessfulCompilation, ...ActionableFailures, ...OtherFailures]) {
+ const filename = compilation.fnLoc?.filename;
@@ -38,33 +38,33 @@ index b427385..4bf23db 100755
+ if (compilation.kind === "CompileError") {
+ const { reason, severity, loc } = compilation.detail;
+
-+ const lnNo = loc.start?.line;
-+ const colNo = loc.start?.column;
++ const lnNo = loc.start?.line;
++ const colNo = loc.start?.column;
+
-+ const isTodo = severity === ErrorSeverity.Todo;
++ const isTodo = severity === ErrorSeverity.Todo;
+
-+ console.log(
-+ chalk[isTodo ? 'yellow' : 'red'](
-+ `Failed to compile ${
-+ filename
-+ }${
-+ lnNo !== undefined ? `:${lnNo}${
-+ colNo !== undefined ? `:${colNo}` : ""
-+ }.` : ""
-+ }`
-+ ),
-+ chalk[isTodo ? 'yellow' : 'red'](reason? `Reason: ${reason}` : "")
-+ );
-+ console.log("\n");
++ console.log(
++ chalk[isTodo ? 'yellow' : 'red'](
++ `Failed to compile ${
++ filename
++ }${
++ lnNo !== undefined ? `:${lnNo}${
++ colNo !== undefined ? `:${colNo}` : ""
++ }.` : ""
++ }`
++ ),
++ chalk[isTodo ? 'yellow' : 'red'](reason? `Reason: ${reason}` : "")
++ );
++ console.log("\n");
+ }
+ }
+ }
},
};
const JsFileExtensionRE = /(js|ts|jsx|tsx)$/;
-@@ -69200,9 +69244,16 @@ function main() {
- type: "string",
- default: "**/+(*.{js,mjs,jsx,ts,tsx}|package.json)",
+@@ -57015,9 +57059,16 @@ function main() {
+ type: 'string',
+ default: '**/+(*.{js,mjs,jsx,ts,tsx}|package.json)',
})
+ .option('verbose', {
+ description: 'run with verbose logging',
@@ -73,13 +73,13 @@ index b427385..4bf23db 100755
+ alias: 'v',
+ })
.parseSync();
- const spinner = ora("Checking").start();
+ const spinner = ora('Checking').start();
let src = argv.src;
+ let verbose = argv.verbose;
const globOptions = {
onlyFiles: true,
ignore: [
-@@ -69222,7 +69273,7 @@ function main() {
+@@ -57037,7 +57088,7 @@ function main() {
libraryCompatCheck.run(source, path);
}
spinner.stop();
diff --git a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+002+enable-ref-identifiers.patch b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch
similarity index 65%
rename from patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+002+enable-ref-identifiers.patch
rename to patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch
index 6caa4ad4c373..8ae46e379619 100644
--- a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+002+enable-ref-identifiers.patch
+++ b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch
@@ -1,28 +1,28 @@
diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js
-index 4bf23db..fa2ab22 100755
+index 460339b..17b0f96 100755
--- a/node_modules/react-compiler-healthcheck/dist/index.js
+++ b/node_modules/react-compiler-healthcheck/dist/index.js
-@@ -69088,6 +69088,9 @@ const COMPILER_OPTIONS = {
- compilationMode: "infer",
- panicThreshold: "critical_errors",
- logger: logger,
+@@ -56902,6 +56902,9 @@ const COMPILER_OPTIONS = {
+ noEmit: true,
+ compilationMode: 'infer',
+ panicThreshold: 'critical_errors',
+ environment: {
+ enableTreatRefLikeIdentifiersAsRefs: true,
+ },
+ logger: logger,
};
function isActionableDiagnostic(detail) {
- switch (detail.severity) {
diff --git a/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts b/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts
-index 09c9b9b..d2418e0 100644
+index 3094548..fd05b76 100644
--- a/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts
+++ b/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts
-@@ -51,6 +51,9 @@ const COMPILER_OPTIONS: Partial = {
- compilationMode: "infer",
- panicThreshold: "critical_errors",
- logger,
+@@ -50,6 +50,9 @@ const COMPILER_OPTIONS: Partial = {
+ noEmit: true,
+ compilationMode: 'infer',
+ panicThreshold: 'critical_errors',
+ environment: {
+ enableTreatRefLikeIdentifiersAsRefs: true,
+ },
+ logger,
};
- function isActionableDiagnostic(detail: CompilerErrorDetailOptions) {
diff --git a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+003+json.patch b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch
similarity index 88%
rename from patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+003+json.patch
rename to patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch
index a3de7a365889..246351351195 100644
--- a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+003+json.patch
+++ b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch
@@ -1,8 +1,8 @@
diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js
-index fa2ab22..93be1fb 100755
+index 17b0f96..e386e34 100755
--- a/node_modules/react-compiler-healthcheck/dist/index.js
+++ b/node_modules/react-compiler-healthcheck/dist/index.js
-@@ -69157,16 +69157,28 @@ var reactCompilerCheck = {
+@@ -56972,16 +56972,28 @@ var reactCompilerCheck = {
compile(source, path);
}
},
@@ -24,7 +24,7 @@ index fa2ab22..93be1fb 100755
+ )
+ );
+ }
-+
++
+ if (json) {
+ const extractFileName = (output) => output.fnLoc.filename;
+ const successfulFiles = SucessfulCompilation.map(extractFileName);
@@ -34,10 +34,10 @@ index fa2ab22..93be1fb 100755
+ failure: unsuccessfulFiles,
+ }));
+ }
-
+
if (verbose) {
for (const compilation of [...SucessfulCompilation, ...ActionableFailures, ...OtherFailures]) {
-@@ -69253,10 +69265,17 @@ function main() {
+@@ -57068,10 +57080,17 @@ function main() {
default: false,
alias: 'v',
})
@@ -48,14 +48,14 @@ index fa2ab22..93be1fb 100755
+ alias: 'j',
+ })
.parseSync();
- const spinner = ora("Checking").start();
+ const spinner = ora('Checking').start();
let src = argv.src;
let verbose = argv.verbose;
+ let json = argv.json;
const globOptions = {
onlyFiles: true,
ignore: [
-@@ -69276,9 +69295,12 @@ function main() {
+@@ -57091,9 +57110,11 @@ function main() {
libraryCompatCheck.run(source, path);
}
spinner.stop();
@@ -63,7 +63,6 @@ index fa2ab22..93be1fb 100755
- strictModeCheck.report();
- libraryCompatCheck.report();
+ reactCompilerCheck.report(verbose, json);
-+ // using json option we only want to get list of files
+ if (!json) {
+ strictModeCheck.report();
+ libraryCompatCheck.report();
diff --git a/patches/react-native+0.75.2+011+textinput-clear-command.patch b/patches/react-native+0.75.2+011+textinput-clear-command.patch
index 773dde04ef44..6723d36d6c6c 100644
--- a/patches/react-native+0.75.2+011+textinput-clear-command.patch
+++ b/patches/react-native+0.75.2+011+textinput-clear-command.patch
@@ -1,3 +1,51 @@
+diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
+index a77e5b4..6c4bbb2 100644
+--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
++++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
+@@ -412,6 +412,13 @@ export type NativeProps = $ReadOnly<{|
+ $ReadOnly<{|target: Int32, text: string|}>,
+ >,
+
++ /**
++ * Invoked when the user performs the clear action.
++ */
++ onClear?: ?BubblingEventHandler<
++ $ReadOnly<{|target: Int32, eventCount: Int32, text: string|}>,
++ >,
++
+ /**
+ * Callback that is called when a key is pressed.
+ * This will be called with `{ nativeEvent: { key: keyValue } }`
+@@ -655,6 +662,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
+ },
+ },
+ directEventTypes: {
++ topClear: {
++ registrationName: 'onClear',
++ },
+ topScroll: {
+ registrationName: 'onScroll',
+ },
+@@ -693,6 +703,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
+ textTransform: true,
+ returnKeyType: true,
+ keyboardType: true,
++ onClear: true,
+ multiline: true,
+ color: {process: require('../../StyleSheet/processColor').default},
+ autoComplete: true,
+diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
+index 0aa8965..0b14171 100644
+--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
++++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
+@@ -146,6 +146,7 @@ const RCTTextInputViewConfig = {
+ lineBreakStrategyIOS: true,
+ smartInsertDelete: true,
+ ...ConditionallyIgnoredEventHandlers({
++ onClear: true,
+ onChange: true,
+ onSelectionChange: true,
+ onContentSizeChange: true,
diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
index 0aa8965..3bfe22c 100644
--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch
index cc9c8531e3a3..bd65871cf5ac 100644
--- a/patches/react-native-modal+13.0.1.patch
+++ b/patches/react-native-modal+13.0.1.patch
@@ -11,7 +11,7 @@ index b63bcfc..bd6419e 100644
buildPanResponder: () => void;
getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number;
diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js
-index 80f4e75..5a58eae 100644
+index 80f4e75..46277ea 100644
--- a/node_modules/react-native-modal/dist/modal.js
+++ b/node_modules/react-native-modal/dist/modal.js
@@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component {
@@ -28,7 +28,18 @@ index 80f4e75..5a58eae 100644
this.shouldPropagateSwipe = (evt, gestureState) => {
return typeof this.props.propagateSwipe === 'function'
? this.props.propagateSwipe(evt, gestureState)
-@@ -453,10 +460,18 @@ export class ReactNativeModal extends React.Component {
+@@ -383,7 +390,9 @@ export class ReactNativeModal extends React.Component {
+ this.setState({
+ isVisible: false,
+ }, () => {
+- this.props.onModalHide();
++ if (Platform.OS !== 'ios') {
++ this.props.onModalHide();
++ }
+ });
+ });
+ }
+@@ -453,10 +462,18 @@ export class ReactNativeModal extends React.Component {
if (this.state.isVisible) {
this.open();
}
@@ -48,7 +59,7 @@ index 80f4e75..5a58eae 100644
if (this.didUpdateDimensionsEmitter) {
this.didUpdateDimensionsEmitter.remove();
}
-@@ -464,6 +479,9 @@ export class ReactNativeModal extends React.Component {
+@@ -464,6 +481,9 @@ export class ReactNativeModal extends React.Component {
InteractionManager.clearInteractionHandle(this.interactionHandle);
this.interactionHandle = null;
}
@@ -58,9 +69,21 @@ index 80f4e75..5a58eae 100644
}
componentDidUpdate(prevProps) {
// If the animations have been changed then rebuild them to make sure we're
-@@ -525,7 +543,7 @@ export class ReactNativeModal extends React.Component {
+@@ -490,7 +510,7 @@ export class ReactNativeModal extends React.Component {
+ }
+ render() {
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+- const { animationIn, animationInTiming, animationOut, animationOutTiming, avoidKeyboard, coverScreen, hasBackdrop, backdropColor, backdropOpacity, backdropTransitionInTiming, backdropTransitionOutTiming, customBackdrop, children, isVisible, onModalShow, onBackButtonPress, useNativeDriver, propagateSwipe, style, ...otherProps } = this.props;
++ const { animationIn, animationInTiming, animationOut, animationOutTiming, avoidKeyboard, coverScreen, hasBackdrop, backdropColor, backdropOpacity, backdropTransitionInTiming, backdropTransitionOutTiming, customBackdrop, children, isVisible, onModalShow, onBackButtonPress, useNativeDriver, propagateSwipe, style, onDismiss, ...otherProps } = this.props;
+ const { testID, ...containerProps } = otherProps;
+ const computedStyle = [
+ { margin: this.getDeviceWidth() * 0.05, transform: [{ translateY: 0 }] },
+@@ -523,9 +543,9 @@ export class ReactNativeModal extends React.Component {
+ this.makeBackdrop(),
+ containerView));
}
- return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps),
+- return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps),
++ return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress, onDismiss: () => {onDismiss();if (Platform.OS === 'ios'){this.props.onModalHide();}} }, otherProps),
this.makeBackdrop(),
- avoidKeyboard ? (React.createElement(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, pointerEvents: "box-none", style: computedStyle.concat([{ margin: 0 }]) }, containerView)) : (containerView)));
+ avoidKeyboard ? (React.createElement(KeyboardAvoidingView, { behavior: 'padding', pointerEvents: "box-none", style: computedStyle.concat([{ margin: 0 }]) }, containerView)) : (containerView)));
diff --git a/patches/react-native-pager-view+6.4.1.patch b/patches/react-native-pager-view+6.4.1.patch
new file mode 100644
index 000000000000..64b2b580ecd3
--- /dev/null
+++ b/patches/react-native-pager-view+6.4.1.patch
@@ -0,0 +1,73 @@
+--- a/node_modules/react-native-pager-view/ios/Fabric/RNCPagerViewComponentView.mm
++++ b/node_modules/react-native-pager-view/ios/Fabric/RNCPagerViewComponentView.mm
+@@ -195,13 +195,10 @@ -(void)scrollViewDidScroll:(UIScrollView *)scrollView {
+
+ strongEventEmitter.onPageScroll(RNCViewPagerEventEmitter::OnPageScroll{.position = static_cast(position), .offset = offset});
+
+- //This is temporary workaround to allow animations based on onPageScroll event
+- //until Fabric implements proper NativeAnimationDriver
+- RCTBridge *bridge = [RCTBridge currentBridge];
+-
+- if (bridge) {
+- [bridge.eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:[NSNumber numberWithInt:self.tag] position:@(position) offset:@(offset)]];
+- }
++ NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[[RCTOnPageScrollEvent alloc] initWithReactTag:[NSNumber numberWithInt:self.tag] position:@(position) offset:@(offset)], @"event", nil];
++ [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED"
++ object:nil
++ userInfo:userInfo];
+ }
+
+ #pragma mark - Internal methods
+diff --git a/node_modules/react-native-pager-view/ios/LEGACY/Fabric/LEGACY_RNCPagerViewComponentView.mm b/node_modules/react-native-pager-view/ios/LEGACY/Fabric/LEGACY_RNCPagerViewComponentView.mm
+index 7608645..84f6f60 100644
+--- a/node_modules/react-native-pager-view/ios/LEGACY/Fabric/LEGACY_RNCPagerViewComponentView.mm
++++ b/node_modules/react-native-pager-view/ios/LEGACY/Fabric/LEGACY_RNCPagerViewComponentView.mm
+@@ -363,14 +363,10 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
+ int eventPosition = (int) position;
+ strongEventEmitter.onPageScroll(LEGACY_RNCViewPagerEventEmitter::OnPageScroll{.position = static_cast(eventPosition), .offset = interpolatedOffset});
+
+- //This is temporary workaround to allow animations based on onPageScroll event
+- //until Fabric implements proper NativeAnimationDriver
+- RCTBridge *bridge = [RCTBridge currentBridge];
+-
+- if (bridge) {
+- [bridge.eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:[NSNumber numberWithInt:self.tag] position:@(position) offset:@(interpolatedOffset)]];
+- }
+-
++ NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[[RCTOnPageScrollEvent alloc] initWithReactTag:[NSNumber numberWithInt:self.tag] position:@(position) offset:@(interpolatedOffset)], @"event", nil];
++ [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED"
++ object:nil
++ userInfo:userInfo];
+ }
+
+
+diff --git a/node_modules/react-native-pager-view/ios/LEGACY/LEGACY_RNCPagerView.m b/node_modules/react-native-pager-view/ios/LEGACY/LEGACY_RNCPagerView.m
+index 5f6c535..fd6c2a1 100644
+--- a/node_modules/react-native-pager-view/ios/LEGACY/LEGACY_RNCPagerView.m
++++ b/node_modules/react-native-pager-view/ios/LEGACY/LEGACY_RNCPagerView.m
+@@ -1,5 +1,5 @@
+ #import "LEGACY_RNCPagerView.h"
+-#import "React/RCTLog.h"
++#import
+ #import
+
+ #import "UIViewController+CreateExtension.h"
+diff --git a/node_modules/react-native-pager-view/ios/RNCPagerView.m b/node_modules/react-native-pager-view/ios/RNCPagerView.m
+index 584aada..978496f 100644
+--- a/node_modules/react-native-pager-view/ios/RNCPagerView.m
++++ b/node_modules/react-native-pager-view/ios/RNCPagerView.m
+@@ -1,12 +1,12 @@
+
+ #import "RNCPagerView.h"
+-#import "React/RCTLog.h"
++#import
+ #import
+
+ #import "UIViewController+CreateExtension.h"
+ #import "RCTOnPageScrollEvent.h"
+ #import "RCTOnPageScrollStateChanged.h"
+-#import "React/RCTUIManagerObserverCoordinator.h"
++#import
+ #import "RCTOnPageSelected.h"
+ #import
+
diff --git a/patches/react-native-performance+5.1.0+001+bridgeless.patch b/patches/react-native-performance+5.1.0+001+bridgeless.patch
new file mode 100644
index 000000000000..7aed8cf57487
--- /dev/null
+++ b/patches/react-native-performance+5.1.0+001+bridgeless.patch
@@ -0,0 +1,30 @@
+diff --git a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java
+index 2fa7d5d..10e1ba6 100644
+--- a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java
++++ b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java
+@@ -17,7 +17,7 @@ import java.util.Queue;
+ import java.util.concurrent.ConcurrentLinkedQueue;
+
+ // Should extend NativeRNPerformanceManagerSpec when codegen for old architecture is solved
+-public class PerformanceModule extends ReactContextBaseJavaModule implements TurboModule, RNPerformance.MarkerListener {
++public class PerformanceModule extends NativeRNPerformanceManagerSpec implements RNPerformance.MarkerListener {
+ public static final String PERFORMANCE_MODULE = "RNPerformanceManager";
+ public static final String BRIDGE_SETUP_START = "bridgeSetupStart";
+
+@@ -118,6 +118,16 @@ public class PerformanceModule extends ReactContextBaseJavaModule implements Tur
+ return PERFORMANCE_MODULE;
+ }
+
++ @Override
++ public void addListener(String eventName) {
++ // needed for spec
++ }
++
++ @Override
++ public void removeListeners(double count) {
++ // needed for spec
++ }
++
+ private void emitNativeStartupTime() {
+ safelyEmitMark(new PerformanceMark("nativeLaunchStart", StartTimeProvider.getStartTime()));
+ safelyEmitMark(new PerformanceMark("nativeLaunchEnd", StartTimeProvider.getEndTime()));
diff --git a/patches/react-native-quick-sqlite+8.1.0+001+bridgeless.patch b/patches/react-native-quick-sqlite+8.1.0+001+bridgeless.patch
new file mode 100644
index 000000000000..8f8a13d684e5
--- /dev/null
+++ b/patches/react-native-quick-sqlite+8.1.0+001+bridgeless.patch
@@ -0,0 +1,41 @@
+diff --git a/node_modules/react-native-quick-sqlite/ios/QuickSQLite.mm b/node_modules/react-native-quick-sqlite/ios/QuickSQLite.mm
+index 519f31a..308f746 100644
+--- a/node_modules/react-native-quick-sqlite/ios/QuickSQLite.mm
++++ b/node_modules/react-native-quick-sqlite/ios/QuickSQLite.mm
+@@ -12,12 +12,12 @@ @implementation QuickSQLite
+
+ RCT_EXPORT_MODULE(QuickSQLite)
+
++@synthesize bridge = _bridge;
+
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
+ NSLog(@"Installing QuickSQLite module...");
+
+- RCTBridge *bridge = [RCTBridge currentBridge];
+- RCTCxxBridge *cxxBridge = (RCTCxxBridge *)bridge;
++ RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge;
+ if (cxxBridge == nil) {
+ return @false;
+ }
+@@ -29,7 +29,7 @@ @implementation QuickSQLite
+ return @false;
+ }
+ auto &runtime = *jsiRuntime;
+- auto callInvoker = bridge.jsCallInvoker;
++ auto callInvoker = cxxBridge.jsCallInvoker;
+
+ // Get appGroupID value from Info.plist using key "AppGroup"
+ NSString *appGroupID = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"ReactNativeQuickSQLite_AppGroup"];
+diff --git a/node_modules/react-native-quick-sqlite/src/index.ts b/node_modules/react-native-quick-sqlite/src/index.ts
+index b3e7fc7..7d8930a 100644
+--- a/node_modules/react-native-quick-sqlite/src/index.ts
++++ b/node_modules/react-native-quick-sqlite/src/index.ts
+@@ -15,7 +15,7 @@ if (global.__QuickSQLiteProxy == null) {
+ }
+
+ // Check if we are running on-device (JSI)
+- if (global.nativeCallSyncHook == null || QuickSQLiteModule.install == null) {
++ if ((!global.nativeCallSyncHook && !global.RN$Bridgeless) || QuickSQLiteModule.install == null) {
+ throw new Error(
+ 'Failed to install react-native-quick-sqlite: React Native is not running on-device. QuickSQLite can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.'
+ );
diff --git a/patches/react-native-vision-camera+4.0.0-beta.13+001+rn75-compatibility.patch b/patches/react-native-vision-camera+4.0.0-beta.13+001+rn75-compatibility.patch
index 4e0961ec536a..7c585ddf9f27 100644
--- a/patches/react-native-vision-camera+4.0.0-beta.13+001+rn75-compatibility.patch
+++ b/patches/react-native-vision-camera+4.0.0-beta.13+001+rn75-compatibility.patch
@@ -729,10 +729,10 @@ index 25e1f55..33b9dd3 100644
+ }
}
diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt
-index f2b284c..e348e5c 100644
+index f2b284c..4bb2ebc 100644
--- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt
+++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt
-@@ -4,7 +4,10 @@ import com.facebook.react.bridge.ReadableMap
+@@ -4,8 +4,18 @@ import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
@@ -740,10 +740,18 @@ index f2b284c..e348e5c 100644
import com.facebook.react.uimanager.annotations.ReactProp
+import com.facebook.react.viewmanagers.CameraViewManagerDelegate
+import com.facebook.react.viewmanagers.CameraViewManagerInterface
++import com.mrousavy.camera.types.CameraCodeScannedEvent
import com.mrousavy.camera.types.CameraDeviceFormat
++import com.mrousavy.camera.types.CameraErrorEvent
++import com.mrousavy.camera.types.CameraInitializedEvent
++import com.mrousavy.camera.types.CameraShutterEvent
++import com.mrousavy.camera.types.CameraStartedEvent
++import com.mrousavy.camera.types.CameraStoppedEvent
++import com.mrousavy.camera.types.CameraViewReadyEvent
import com.mrousavy.camera.types.CodeScannerOptions
import com.mrousavy.camera.types.Orientation
-@@ -16,10 +19,19 @@ import com.mrousavy.camera.types.Torch
+ import com.mrousavy.camera.types.PixelFormat
+@@ -16,10 +26,19 @@ import com.mrousavy.camera.types.Torch
import com.mrousavy.camera.types.VideoStabilizationMode
@Suppress("unused")
@@ -764,7 +772,28 @@ index f2b284c..e348e5c 100644
public override fun createViewInstance(context: ThemedReactContext): CameraView = CameraView(context)
override fun onAfterUpdateTransaction(view: CameraView) {
-@@ -46,37 +58,37 @@ class CameraViewManager : ViewGroupManager() {
+@@ -29,13 +48,13 @@ class CameraViewManager : ViewGroupManager() {
+
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap? =
+ MapBuilder.builder()
+- .put("cameraViewReady", MapBuilder.of("registrationName", "onViewReady"))
+- .put("cameraInitialized", MapBuilder.of("registrationName", "onInitialized"))
+- .put("cameraStarted", MapBuilder.of("registrationName", "onStarted"))
+- .put("cameraStopped", MapBuilder.of("registrationName", "onStopped"))
+- .put("cameraShutter", MapBuilder.of("registrationName", "onShutter"))
+- .put("cameraError", MapBuilder.of("registrationName", "onError"))
+- .put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned"))
++ .put(CameraViewReadyEvent.EVENT_NAME, MapBuilder.of("registrationName", "onViewReady"))
++ .put(CameraInitializedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onInitialized"))
++ .put(CameraStartedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onStarted"))
++ .put(CameraStoppedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onStopped"))
++ .put(CameraShutterEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShutter"))
++ .put(CameraErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onError"))
++ .put(CameraCodeScannedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCodeScanned"))
+ .build()
+
+ override fun getName(): String = TAG
+@@ -46,37 +65,37 @@ class CameraViewManager : ViewGroupManager() {
}
@ReactProp(name = "cameraId")
@@ -809,7 +838,7 @@ index f2b284c..e348e5c 100644
if (pixelFormat != null) {
val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat)
view.pixelFormat = newPixelFormat
-@@ -86,27 +98,27 @@ class CameraViewManager : ViewGroupManager() {
+@@ -86,27 +105,27 @@ class CameraViewManager : ViewGroupManager() {
}
@ReactProp(name = "enableDepthData")
@@ -842,7 +871,7 @@ index f2b284c..e348e5c 100644
if (videoStabilizationMode != null) {
val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode)
view.videoStabilizationMode = newMode
-@@ -116,12 +128,12 @@ class CameraViewManager : ViewGroupManager() {
+@@ -116,12 +135,12 @@ class CameraViewManager : ViewGroupManager() {
}
@ReactProp(name = "enablePortraitEffectsMatteDelivery")
@@ -857,7 +886,7 @@ index f2b284c..e348e5c 100644
if (format != null) {
val newFormat = CameraDeviceFormat.fromJSValue(format)
view.format = newFormat
-@@ -131,7 +143,7 @@ class CameraViewManager : ViewGroupManager() {
+@@ -131,7 +150,7 @@ class CameraViewManager : ViewGroupManager() {
}
@ReactProp(name = "resizeMode")
@@ -866,7 +895,7 @@ index f2b284c..e348e5c 100644
if (resizeMode != null) {
val newMode = ResizeMode.fromUnionValue(resizeMode)
view.resizeMode = newMode
-@@ -141,7 +153,7 @@ class CameraViewManager : ViewGroupManager() {
+@@ -141,7 +160,7 @@ class CameraViewManager : ViewGroupManager() {
}
@ReactProp(name = "androidPreviewViewType")
@@ -875,7 +904,7 @@ index f2b284c..e348e5c 100644
if (androidPreviewViewType != null) {
val newMode = PreviewViewType.fromUnionValue(androidPreviewViewType)
view.androidPreviewViewType = newMode
-@@ -154,17 +166,17 @@ class CameraViewManager : ViewGroupManager() {
+@@ -154,17 +173,17 @@ class CameraViewManager : ViewGroupManager() {
// We're treating -1 as "null" here, because when I make the fps parameter
// of type "Int?" the react bridge throws an error.
@ReactProp(name = "fps", defaultInt = -1)
@@ -896,7 +925,7 @@ index f2b284c..e348e5c 100644
if (photoQualityBalance != null) {
val newMode = QualityBalance.fromUnionValue(photoQualityBalance)
view.photoQualityBalance = newMode
-@@ -174,22 +186,22 @@ class CameraViewManager : ViewGroupManager() {
+@@ -174,22 +193,22 @@ class CameraViewManager : ViewGroupManager() {
}
@ReactProp(name = "videoHdr")
@@ -923,7 +952,7 @@ index f2b284c..e348e5c 100644
if (torch != null) {
val newMode = Torch.fromUnionValue(torch)
view.torch = newMode
-@@ -199,17 +211,17 @@ class CameraViewManager : ViewGroupManager() {
+@@ -199,17 +218,17 @@ class CameraViewManager : ViewGroupManager() {
}
@ReactProp(name = "zoom")
@@ -944,7 +973,7 @@ index f2b284c..e348e5c 100644
if (orientation != null) {
val newMode = Orientation.fromUnionValue(orientation)
view.orientation = newMode
-@@ -219,7 +231,7 @@ class CameraViewManager : ViewGroupManager() {
+@@ -219,7 +238,7 @@ class CameraViewManager : ViewGroupManager() {
}
@ReactProp(name = "codeScannerOptions")
@@ -953,7 +982,7 @@ index f2b284c..e348e5c 100644
if (codeScannerOptions != null) {
val newCodeScannerOptions = CodeScannerOptions.fromJSValue(codeScannerOptions)
view.codeScannerOptions = newCodeScannerOptions
-@@ -227,4 +239,8 @@ class CameraViewManager : ViewGroupManager() {
+@@ -227,4 +246,8 @@ class CameraViewManager : ViewGroupManager() {
view.codeScannerOptions = null
}
}
@@ -981,6 +1010,79 @@ index b9d3f67..cb70963 100644
@Suppress("KotlinJniMissingFunction") // we use fbjni.
class VisionCameraProxy(private val reactContext: ReactApplicationContext) {
companion object {
+diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/types/Events.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/types/Events.kt
+index 1ed0355..b8ff7cf 100644
+--- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/types/Events.kt
++++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/types/Events.kt
+@@ -3,39 +3,61 @@ package com.mrousavy.camera.types
+ import com.facebook.react.bridge.Arguments
+ import com.facebook.react.bridge.WritableMap
+ import com.facebook.react.uimanager.events.Event
++import com.mrousavy.camera.types.CameraInitializedEvent.Companion.EVENT_NAME
+
+ class CameraInitializedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) {
+- override fun getEventName() = "cameraInitialized"
++ override fun getEventName() = EVENT_NAME
+ override fun getEventData(): WritableMap = Arguments.createMap()
++ companion object {
++ const val EVENT_NAME = "topInitialized"
++ }
+ }
+
+ class CameraStartedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) {
+- override fun getEventName() = "cameraStarted"
++ override fun getEventName() = EVENT_NAME
+ override fun getEventData(): WritableMap = Arguments.createMap()
++ companion object {
++ const val EVENT_NAME = "topStarted"
++ }
+ }
+
+ class CameraStoppedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) {
+- override fun getEventName() = "cameraStopped"
++ override fun getEventName() = EVENT_NAME
+ override fun getEventData(): WritableMap = Arguments.createMap()
++ companion object {
++ const val EVENT_NAME = "topStopped"
++ }
+ }
+
+ class CameraShutterEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : Event(surfaceId, viewId) {
+- override fun getEventName() = "cameraShutter"
++ override fun getEventName() = EVENT_NAME
+ override fun getEventData() = data
++ companion object {
++ const val EVENT_NAME = "topShutter"
++ }
+ }
+
+ class CameraErrorEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : Event(surfaceId, viewId) {
+- override fun getEventName() = "cameraError"
++ override fun getEventName() = EVENT_NAME
+ override fun getEventData() = data
++ companion object {
++ const val EVENT_NAME = "topError"
++ }
+ }
+
+ class CameraViewReadyEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) {
+- override fun getEventName() = "cameraViewReady"
++ override fun getEventName() = EVENT_NAME
+ override fun getEventData(): WritableMap = Arguments.createMap()
++ companion object {
++ const val EVENT_NAME = "topViewReady"
++ }
+ }
+
+ class CameraCodeScannedEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) :
+ Event(surfaceId, viewId) {
+- override fun getEventName() = "cameraCodeScanned"
++ override fun getEventName() = EVENT_NAME
+ override fun getEventData() = data
++ companion object {
++ const val EVENT_NAME = "topCodeScanned"
++ }
+ }
diff --git a/node_modules/react-native-vision-camera/ios/.swift-version b/node_modules/react-native-vision-camera/ios/.swift-version
new file mode 100644
index 0000000..ef425ca
diff --git a/src/CONST.ts b/src/CONST.ts
index 440f942e1244..dd048f95d374 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -297,6 +297,9 @@ const CONST = {
// Regex to get link in href prop inside of component
REGEX_LINK_IN_ANCHOR: /]*?\s+)?href="([^"]*)"/gi,
+ // Regex to read violation value from string given by backend
+ VIOLATION_LIMIT_REGEX: /[^0-9]+/g,
+
MERCHANT_NAME_MAX_LENGTH: 255,
MASKED_PAN_PREFIX: 'XXXXXXXXXXXX',
@@ -1143,6 +1146,9 @@ const CONST = {
SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300,
RESIZE_DEBOUNCE_TIME: 100,
UNREAD_UPDATE_DEBOUNCE_TIME: 300,
+ SEARCH_CONVERT_SEARCH_VALUES: 'search_convert_search_values',
+ SEARCH_MAKE_TREE: 'search_make_tree',
+ SEARCH_BUILD_TREE: 'search_build_tree',
SEARCH_FILTER_OPTIONS: 'search_filter_options',
USE_DEBOUNCED_STATE_DELAY: 300,
},
@@ -1574,6 +1580,7 @@ const CONST = {
TRACKING_CATEGORY_OPTIONS: {
DEFAULT: 'DEFAULT',
TAG: 'TAG',
+ REPORT_FIELD: 'REPORT_FIELD',
},
},
@@ -1626,6 +1633,12 @@ const CONST = {
JOURNAL_ENTRY: 'journal_entry',
},
+ QUICKBOOKS_NON_REIMBURSABLE_ACCOUNT_TYPE: {
+ CREDIT_CARD: 'credit_card',
+ DEBIT_CARD: 'debit_card',
+ VENDOR_BILL: 'bill',
+ },
+
QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE: {
VENDOR_BILL: 'VENDOR_BILL',
CHECK: 'CHECK',
@@ -2599,6 +2612,7 @@ const CONST = {
MONTHLY: 'monthly',
},
CARD_TITLE_INPUT_LIMIT: 255,
+ MANAGE_EXPENSIFY_CARDS_ARTICLE_LINK: 'https://help.expensify.com/articles/new-expensify/expensify-card/Manage-Expensify-Cards',
},
COMPANY_CARDS: {
CONNECTION_ERROR: 'connectionError',
@@ -2635,6 +2649,7 @@ const CONST = {
},
BANK_CONNECTIONS: {
WELLS_FARGO: 'wellsfargo',
+ BANK_OF_AMERICA: 'bankofamerica',
CHASE: 'chase',
BREX: 'brex',
CAPITAL_ONE: 'capitalone',
@@ -4825,10 +4840,11 @@ const CONST = {
'\n' +
'Here’s how to request money:\n' +
'\n' +
- '1. Click the green *+* button.\n' +
- '2. Choose *Split expense*.\n' +
- '3. Scan a receipt or enter an amount.\n' +
- '4. Add your friend(s) to the request.\n' +
+ '1. Hit the green *+* button.\n' +
+ '2. Choose *Start chat*.\n' +
+ '3. Enter any email, SMS, or name of who you want to split with.\n' +
+ '4. From within the chat, hit the *+* button on the message bar, and hit *Split expense*.\n' +
+ '5. Create the expense by selecting Manual, Scan or Distance.\n' +
'\n' +
'Feel free to add more details if you want, or just send it off. Let’s get you paid back!',
},
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index d083a46d7760..427e05052ae3 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -3,6 +3,7 @@ import type CONST from './CONST';
import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST';
import type * as FormTypes from './types/form';
import type * as OnyxTypes from './types/onyx';
+import type {Attendee} from './types/onyx/IOU';
import type Onboarding from './types/onyx/Onboarding';
import type AssertTypesEqual from './types/utils/AssertTypesEqual';
import type DeepValueOf from './types/utils/DeepValueOf';
@@ -112,6 +113,9 @@ const ONYXKEYS = {
/** Boolean flag only true when first set */
NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser',
+ /** This NVP contains list of at most 5 recent attendees */
+ NVP_RECENT_ATTENDEES: 'nvp_expensify_recentAttendees',
+
/** This NVP contains information about whether the onboarding flow was completed or not */
NVP_ONBOARDING: 'nvp_onboarding',
@@ -905,6 +909,7 @@ type OnyxValuesMapping = {
// The value of this nvp is a string representation of the date when the block expires, or an empty string if the user is not blocked
[ONYXKEYS.NVP_BLOCKED_FROM_CHAT]: string;
[ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string;
+ [ONYXKEYS.NVP_RECENT_ATTENDEES]: Attendee[];
[ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean;
[ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION]: boolean;
[ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index cf15013fed9b..c346da6cadcb 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -148,7 +148,10 @@ const ROUTES = {
},
SETTINGS_DELEGATE_CONFIRM: {
route: 'settings/security/delegate/:login/role/:role/confirm',
- getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/confirm` as const,
+ getRoute: (login: string, role: string, showValidateActionModal?: boolean) => {
+ const validateActionModalParam = showValidateActionModal ? `?showValidateActionModal=true` : '';
+ return `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/confirm${validateActionModalParam}` as const;
+ },
},
SETTINGS_ABOUT: 'settings/about',
SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links',
@@ -444,6 +447,59 @@ const ROUTES = {
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
+ MONEY_REQUEST_ATTENDEE: {
+ route: ':action/:iouType/attendees/:transactionID/:reportID',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/attendees/${transactionID}/${reportID}`, backTo),
+ },
+ SETTINGS_TAGS_ROOT: {
+ route: 'settings/:policyID/tags',
+ getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags`, backTo),
+ },
+ SETTINGS_TAGS_SETTINGS: {
+ route: 'settings/:policyID/tags/settings',
+ getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/settings` as const, backTo),
+ },
+ SETTINGS_TAGS_EDIT: {
+ route: 'settings/:policyID/tags/:orderWeight/edit',
+ getRoute: (policyID: string, orderWeight: number, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/${orderWeight}/edit` as const, backTo),
+ },
+ SETTINGS_TAG_CREATE: {
+ route: 'settings/:policyID/tags/new',
+ getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/new` as const, backTo),
+ },
+ SETTINGS_TAG_EDIT: {
+ route: 'settings/:policyID/tag/:orderWeight/:tagName/edit',
+ getRoute: (policyID: string, orderWeight: number, tagName: string, backTo = '') =>
+ getUrlWithBackToParam(`settings/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/edit` as const, backTo),
+ },
+ SETTINGS_TAG_SETTINGS: {
+ route: 'settings/:policyID/tag/:orderWeight/:tagName',
+ getRoute: (policyID: string, orderWeight: number, tagName: string, backTo = '') =>
+ getUrlWithBackToParam(`settings/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}` as const, backTo),
+ },
+ SETTINGS_TAG_APPROVER: {
+ route: 'settings/:policyID/tag/:orderWeight/:tagName/approver',
+ getRoute: (policyID: string, orderWeight: number, tagName: string, backTo = '') =>
+ getUrlWithBackToParam(`settings/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/approver` as const, backTo),
+ },
+ SETTINGS_TAG_LIST_VIEW: {
+ route: 'settings/:policyID/tag-list/:orderWeight',
+ getRoute: (policyID: string, orderWeight: number, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tag-list/${orderWeight}` as const, backTo),
+ },
+ SETTINGS_TAG_GL_CODE: {
+ route: 'settings/:policyID/tag/:orderWeight/:tagName/gl-code',
+ getRoute: (policyID: string, orderWeight: number, tagName: string, backTo = '') =>
+ getUrlWithBackToParam(`settings/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/gl-code` as const, backTo),
+ },
+ SETTINGS_TAGS_IMPORT: {
+ route: 'settings/:policyID/tags/import',
+ getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/import` as const, backTo),
+ },
+ SETTINGS_TAGS_IMPORTED: {
+ route: 'settings/:policyID/tags/imported',
+ getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/imported` as const, backTo),
+ },
SETTINGS_CATEGORIES_ROOT: {
route: 'settings/:policyID/categories',
getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories`, backTo),
@@ -462,7 +518,25 @@ const ROUTES = {
},
SETTINGS_CATEGORY_EDIT: {
route: 'settings/:policyID/category/:categoryName/edit',
- getRoute: (policyID: string, categoryName: string, backTo = '') => getUrlWithBackToParam(`settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/edit`, backTo),
+ getRoute: (policyID: string, categoryName: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/category/${encodeURIComponent(categoryName)}/edit`, backTo),
+ },
+ SETTINGS_CATEGORIES_IMPORT: {
+ route: 'settings/:policyID/categories/import',
+ getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/import` as const, backTo),
+ },
+ SETTINGS_CATEGORIES_IMPORTED: {
+ route: 'settings/:policyID/categories/imported',
+ getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/imported` as const, backTo),
+ },
+ SETTINGS_CATEGORY_PAYROLL_CODE: {
+ route: 'settings/:policyID/category/:categoryName/payroll-code',
+ getRoute: (policyID: string, categoryName: string, backTo = '') =>
+ getUrlWithBackToParam(`settings/${policyID}/category/${encodeURIComponent(categoryName)}/payroll-code` as const, backTo),
+ },
+ SETTINGS_CATEGORY_GL_CODE: {
+ route: 'settings/:policyID/category/:categoryName/gl-code',
+ getRoute: (policyID: string, categoryName: string, backTo = '') =>
+ getUrlWithBackToParam(`settings/${policyID}/category/${encodeURIComponent(categoryName)}/gl-code` as const, backTo),
},
MONEY_REQUEST_STEP_CURRENCY: {
route: ':action/:iouType/currency/:transactionID/:reportID/:pageIndex?',
@@ -514,10 +588,6 @@ const ROUTES = {
getRoute: (action: IOUAction, iouType: IOUType, orderWeight: number, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
getUrlWithBackToParam(`${action as string}/${iouType as string}/tag/${orderWeight}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
- SETTINGS_TAGS_ROOT: {
- route: 'settings/:policyID/tags',
- getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags`, backTo),
- },
MONEY_REQUEST_STEP_WAYPOINT: {
route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID?: string, pageIndex = '', backTo = '') =>
@@ -958,7 +1028,7 @@ const ROUTES = {
},
WORKSPACE_TAG_APPROVER: {
route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/approver',
- getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${tagName}/approver` as const,
+ getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/approver` as const,
},
WORKSPACE_TAG_LIST_VIEW: {
route: 'settings/workspaces/:policyID/tag-list/:orderWeight',
@@ -1380,14 +1450,26 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/classes',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/classes` as const,
},
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/classes/displayed-as',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/classes/displayed-as` as const,
+ },
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/customers',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/customers` as const,
},
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/customers/displayed-as',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/customers/displayed-as` as const,
+ },
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/locations',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/locations` as const,
},
+ POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/locations/displayed-as',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/locations/displayed-as` as const,
+ },
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_TAXES: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/taxes',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/taxes` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index ff428edcd7eb..2e44c5ed5695 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -175,6 +175,7 @@ const SCREENS = {
SEARCH_ADVANCED_FILTERS: 'SearchAdvancedFilters',
SEARCH_SAVED_SEARCH: 'SearchSavedSearch',
SETTINGS_CATEGORIES: 'SettingsCategories',
+ SETTINGS_TAGS: 'SettingsTags',
EXPENSIFY_CARD: 'ExpensifyCard',
DOMAIN_CARD: 'DomainCard',
RESTRICTED_ACTION: 'RestrictedAction',
@@ -218,6 +219,7 @@ const SCREENS = {
EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
RECEIPT: 'Money_Request_Receipt',
STATE_SELECTOR: 'Money_Request_State_Selector',
+ STEP_ATTENDEES: 'Money_Request_Attendee',
},
TRANSACTION_DUPLICATE: {
@@ -244,6 +246,10 @@ const SCREENS = {
SETTINGS_CATEGORY_CREATE: 'Settings_Category_Create',
SETTINGS_CATEGORY_EDIT: 'Settings_Category_Edit',
SETTINGS_CATEGORIES_ROOT: 'Settings_Categories',
+ SETTINGS_CATEGORIES_IMPORT: 'Settings_Categories_Import',
+ SETTINGS_CATEGORIES_IMPORTED: 'Settings_Categories_Imported',
+ SETTINGS_CATEGORY_PAYROLL_CODE: 'Settings_Category_Payroll_Code',
+ SETTINGS_CATEGORY_GL_CODE: 'Settings_Category_GL_Code',
},
EXPENSIFY_CARD: {
EXPENSIFY_CARD_DETAILS: 'Expensify_Card_Details',
@@ -256,7 +262,19 @@ const SCREENS = {
DOMAIN_CARD_REPORT_FRAUD: 'Domain_Card_Report_Fraud',
},
- SETTINGS_TAGS_ROOT: 'Settings_Tags',
+ SETTINGS_TAGS: {
+ SETTINGS_TAGS_ROOT: 'Settings_Tags',
+ SETTINGS_TAGS_SETTINGS: 'Settings_Tags_Settings',
+ SETTINGS_TAGS_EDIT: 'Settings_Tags_Edit',
+ SETTINGS_TAG_CREATE: 'Settings_Tag_Create',
+ SETTINGS_TAG_EDIT: 'Settings_Tag_Edit',
+ SETTINGS_TAG_SETTINGS: 'Settings_Tag_Settings',
+ SETTINGS_TAG_APPROVER: 'Settings_Tag_Approver',
+ SETTINGS_TAG_LIST_VIEW: 'Settings_Tag_List_View',
+ SETTINGS_TAG_GL_CODE: 'Settings_Tag_GL_Code',
+ SETTINGS_TAGS_IMPORT: 'Settings_Tags_Import',
+ SETTINGS_TAGS_IMPORTED: 'Settings_Tags_Imported',
+ },
REPORT_SETTINGS: {
ROOT: 'Report_Settings_Root',
@@ -314,6 +332,9 @@ const SCREENS = {
QUICKBOOKS_ONLINE_ADVANCED: 'Policy_Accounting_Quickbooks_Online_Advanced',
QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Account_Selector',
QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Invoice_Account_Selector',
+ QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Classes_Displayed_As',
+ QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Customers_Displayed_As',
+ QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Locations_Displayed_As',
QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense_Account_Select',
QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense_Select',
QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense',
diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
index 443a553d4689..ae74a11c7e9d 100644
--- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
+++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
@@ -22,7 +22,7 @@ type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & {
onPressOut?: () => void;
};
-function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) {
+function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut, isDeleted}: BaseAnchorForAttachmentsOnlyProps) {
const sourceURLWithAuth = addEncryptedAuthTokenToURL(source);
const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1];
@@ -63,6 +63,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP
shouldShowDownloadIcon={!!sourceID && !isOffline}
shouldShowLoadingSpinnerIcon={isDownloading}
isUsedAsChatAttachment
+ isDeleted={!!isDeleted}
isUploading={!sourceID}
/>
diff --git a/src/components/AnchorForAttachmentsOnly/types.ts b/src/components/AnchorForAttachmentsOnly/types.ts
index a5186d8c0d90..67a5bb532c27 100644
--- a/src/components/AnchorForAttachmentsOnly/types.ts
+++ b/src/components/AnchorForAttachmentsOnly/types.ts
@@ -9,6 +9,9 @@ type AnchorForAttachmentsOnlyProps = {
/** Any additional styles to apply */
style?: StyleProp;
+
+ /** Whether the attachment is deleted */
+ isDeleted?: boolean;
};
export default AnchorForAttachmentsOnlyProps;
diff --git a/src/components/AttachmentDeletedIndicator.tsx b/src/components/AttachmentDeletedIndicator.tsx
new file mode 100644
index 000000000000..06e700c2fd73
--- /dev/null
+++ b/src/components/AttachmentDeletedIndicator.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import {View} from 'react-native';
+import type {StyleProp, ViewStyle} from 'react-native';
+import useNetwork from '@hooks/useNetwork';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
+
+type AttachmentDeletedIndicatorProps = {
+ /** Additional styles for container */
+ containerStyles?: StyleProp;
+};
+
+function AttachmentDeletedIndicator({containerStyles}: AttachmentDeletedIndicatorProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
+
+ if (!isOffline) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+AttachmentDeletedIndicator.displayName = 'AttachmentDeletedIndicator';
+
+export default AttachmentDeletedIndicator;
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index 975ea6c548c0..d3a51c7fc0f0 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -417,6 +417,7 @@ function AttachmentPicker({
}}
isVisible={isVisible}
anchorRef={popoverRef}
+ // eslint-disable-next-line react-compiler/react-compiler
onModalHide={onModalHide.current}
>
@@ -431,6 +432,7 @@ function AttachmentPicker({
))}
+ {/* eslint-disable-next-line react-compiler/react-compiler */}
{renderChildren()}
>
);
diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx
index c4979f544080..f3c880fcb835 100644
--- a/src/components/AttachmentPicker/index.tsx
+++ b/src/components/AttachmentPicker/index.tsx
@@ -98,6 +98,7 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a
}}
accept={acceptedFileTypes ? getAcceptableFileTypesFromAList(acceptedFileTypes) : getAcceptableFileTypes(type)}
/>
+ {/* eslint-disable-next-line react-compiler/react-compiler */}
{children({
openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => {
onPicked.current = newOnPicked;
diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx
index a1408aaf400e..d9c4f7e93fbe 100644
--- a/src/components/Attachments/AttachmentCarousel/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -255,6 +255,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi
scrollTo(scrollRef, newIndex * cellWidth, 0, true);
})
+ // eslint-disable-next-line react-compiler/react-compiler
.withRef(pagerRef as MutableRefObject),
[attachments.length, canUseTouchScreen, cellWidth, page, scale, scrollRef],
);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
index 1e3cded92bd5..c6e7984b793f 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
@@ -32,7 +32,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
const Pan = Gesture.Pan()
.manualActivation(true)
.onTouchesMove((evt) => {
- if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled) {
+ if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled && scale.value === 1) {
const translateX = Math.abs((evt.allTouches.at(0)?.absoluteX ?? 0) - offsetX.value);
const translateY = Math.abs((evt.allTouches.at(0)?.absoluteY ?? 0) - offsetY.value);
const allowEnablingScroll = !isPanGestureActive.value || isScrollEnabled.value;
@@ -40,7 +40,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
// if the value of X is greater than Y and the pdf is not zoomed in,
// enable the pager scroll so that the user
// can swipe to the next attachment otherwise disable it.
- if (translateX > translateY && translateX > SCROLL_THRESHOLD && scale.value === 1 && allowEnablingScroll) {
+ if (translateX > translateY && translateX > SCROLL_THRESHOLD && allowEnablingScroll) {
// eslint-disable-next-line react-compiler/react-compiler
isScrollEnabled.value = true;
} else if (translateY > SCROLL_THRESHOLD) {
@@ -57,7 +57,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
if (!isScrollEnabled) {
return;
}
- isScrollEnabled.value = true;
+ isScrollEnabled.value = scale.value === 1;
});
const Content = useMemo(
diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
index e6ac9f9f21c7..23e13833df64 100644
--- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
@@ -25,11 +25,14 @@ type DefaultAttachmentViewProps = {
icon?: IconAsset;
+ /** Whether the attachment is deleted */
+ isDeleted?: boolean;
+
/** Flag indicating if the attachment is being uploaded. */
isUploading?: boolean;
};
-function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isUploading}: DefaultAttachmentViewProps) {
+function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isUploading, isDeleted}: DefaultAttachmentViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -43,7 +46,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa
/>
- {fileName}
+ {fileName}
{!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && (
diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index 080e0ec589ec..0af1a86992e7 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -72,6 +72,9 @@ type AttachmentViewProps = Attachment & {
/* Flag indicating whether the attachment has been uploaded. */
isUploaded?: boolean;
+ /** Whether the attachment is deleted */
+ isDeleted?: boolean;
+
/** Flag indicating if the attachment is being uploaded. */
isUploading?: boolean;
};
@@ -98,14 +101,14 @@ function AttachmentView({
duration,
isUsedAsChatAttachment,
isUploaded = true,
+ isDeleted,
isUploading = false,
}: AttachmentViewProps) {
+ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
const {translate} = useLocalize();
const {updateCurrentlyPlayingURL} = usePlaybackContext();
const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);
- const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
-
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -292,6 +295,7 @@ function AttachmentView({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
shouldShowLoadingSpinnerIcon={shouldShowLoadingSpinnerIcon || isUploading}
containerStyles={containerStyles}
+ isDeleted={isDeleted}
isUploading={isUploading}
/>
);
diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx
index 1a606b35f6d2..dca0d08d11d5 100644
--- a/src/components/AvatarCropModal/AvatarCropModal.tsx
+++ b/src/components/AvatarCropModal/AvatarCropModal.tsx
@@ -336,6 +336,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose
}
const newSliderValue = clamp(locationX, [0, sliderContainerSize]);
const newScale = newScaleValue(newSliderValue, sliderContainerSize);
+ // eslint-disable-next-line react-compiler/react-compiler
translateSlider.value = newSliderValue;
const differential = newScale / scale.value;
scale.value = newScale;
diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx
index e1d7beb850d0..c44054c15445 100644
--- a/src/components/ButtonWithDropdownMenu/index.tsx
+++ b/src/components/ButtonWithDropdownMenu/index.tsx
@@ -53,6 +53,7 @@ function ButtonWithDropdownMenu({
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null);
const {windowWidth, windowHeight} = useWindowDimensions();
const dropdownAnchor = useRef(null);
+ // eslint-disable-next-line react-compiler/react-compiler
const dropdownButtonRef = isSplitButton ? buttonRef : mergeRefs(buttonRef, dropdownAnchor);
const selectedItem = options.at(selectedItemIndex) ?? options.at(0);
const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize);
@@ -200,6 +201,7 @@ function ButtonWithDropdownMenu({
onItemSelected={() => setIsMenuVisible(false)}
anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverButtonDropdownMenuOffset(windowWidth) : popoverAnchorPosition}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
+ // eslint-disable-next-line react-compiler/react-compiler
anchorRef={nullCheckRef(dropdownAnchor)}
withoutOverlay
anchorAlignment={anchorAlignment}
diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts
index da6b3d93433d..766c0df950b4 100644
--- a/src/components/ButtonWithDropdownMenu/types.ts
+++ b/src/components/ButtonWithDropdownMenu/types.ts
@@ -25,6 +25,7 @@ type DropdownOption = {
iconWidth?: number;
iconHeight?: number;
iconDescription?: string;
+ additionalIconStyles?: StyleProp;
onSelected?: () => void;
disabled?: boolean;
iconFill?: string;
diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx
index e63b8bb91874..fd2013c6bde7 100755
--- a/src/components/ConfirmModal.tsx
+++ b/src/components/ConfirmModal.tsx
@@ -137,6 +137,7 @@ function ConfirmModal({
restoreFocusType,
}: ConfirmModalProps) {
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const styles = useThemeStyles();
diff --git a/src/components/ConnectBankAccountButton.tsx b/src/components/ConnectBankAccountButton.tsx
deleted file mode 100644
index ee6ad04d727e..000000000000
--- a/src/components/ConnectBankAccountButton.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
-import {View} from 'react-native';
-import useLocalize from '@hooks/useLocalize';
-import useNetwork from '@hooks/useNetwork';
-import Navigation from '@libs/Navigation/Navigation';
-import * as ReimbursementAccount from '@userActions/ReimbursementAccount';
-import Button from './Button';
-import * as Expensicons from './Icon/Expensicons';
-import Text from './Text';
-
-type ConnectBankAccountButtonProps = {
- /** PolicyID for navigating to bank account route of that policy */
- policyID: string;
-
- /** Button styles, also applied for offline message wrapper */
- style?: StyleProp;
-};
-
-function ConnectBankAccountButton({style, policyID}: ConnectBankAccountButtonProps) {
- const {isOffline} = useNetwork();
- const {translate} = useLocalize();
- const activeRoute = Navigation.getActiveRouteWithoutParams();
-
- return isOffline ? (
-
- {`${translate('common.youAppearToBeOffline')} ${translate('common.thisFeatureRequiresInternet')}`}
-
- ) : (
- ReimbursementAccount.navigateToBankAccountRoute(policyID, activeRoute)}
- icon={Expensicons.Bank}
- style={style}
- shouldShowRightIcon
- large
- success
- />
- );
-}
-
-ConnectBankAccountButton.displayName = 'ConnectBankAccountButton';
-
-export default ConnectBankAccountButton;
diff --git a/src/components/ConnectToQuickbooksDesktopFlow/index.tsx b/src/components/ConnectToQuickbooksDesktopFlow/index.tsx
index 07ca376a7449..6f5a983e4250 100644
--- a/src/components/ConnectToQuickbooksDesktopFlow/index.tsx
+++ b/src/components/ConnectToQuickbooksDesktopFlow/index.tsx
@@ -5,6 +5,7 @@ import ROUTES from '@src/ROUTES';
import type {ConnectToQuickbooksDesktopFlowProps} from './types';
function ConnectToQuickbooksDesktopFlow({policyID}: ConnectToQuickbooksDesktopFlowProps) {
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
useEffect(() => {
diff --git a/src/components/ConnectToXeroFlow/index.native.tsx b/src/components/ConnectToXeroFlow/index.native.tsx
index ab9fa3054261..fbf7bf01ab5c 100644
--- a/src/components/ConnectToXeroFlow/index.native.tsx
+++ b/src/components/ConnectToXeroFlow/index.native.tsx
@@ -40,14 +40,14 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {
return (
<>
- {isRequire2FAModalOpen && (
+ {!is2FAEnabled && (
{
setIsRequire2FAModalOpen(false);
Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.POLICY_ACCOUNTING.getRoute(policyID), getXeroSetupLink(policyID)));
}}
onCancel={() => setIsRequire2FAModalOpen(false)}
- isVisible
+ isVisible={isRequire2FAModalOpen}
description={translate('twoFactorAuth.twoFactorAuthIsRequiredDescription')}
/>
)}
diff --git a/src/components/ConnectToXeroFlow/index.tsx b/src/components/ConnectToXeroFlow/index.tsx
index 5d0e88e1512b..ad41ba8082b1 100644
--- a/src/components/ConnectToXeroFlow/index.tsx
+++ b/src/components/ConnectToXeroFlow/index.tsx
@@ -29,7 +29,7 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- if (isRequire2FAModalOpen) {
+ if (!is2FAEnabled) {
return (
{
@@ -39,7 +39,7 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {
onCancel={() => {
setIsRequire2FAModalOpen(false);
}}
- isVisible
+ isVisible={isRequire2FAModalOpen}
description={translate('twoFactorAuth.twoFactorAuthIsRequiredDescription')}
/>
);
diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
index 86edbb3b4c5e..acc1a7f40b47 100644
--- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
+++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
@@ -15,6 +15,7 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit
const styles = useThemeStyles();
const containerRef = useRef(null);
const childRefs = useRef([]);
+ // eslint-disable-next-line react-compiler/react-compiler
const isEllipsisActive = !!containerRef.current?.offsetWidth && !!containerRef.current?.scrollWidth && containerRef.current.offsetWidth < containerRef.current.scrollWidth;
/**
diff --git a/src/components/DragAndDrop/NoDropZone/index.tsx b/src/components/DragAndDrop/NoDropZone/index.tsx
index 3438bfff7c05..b55db0e6c212 100644
--- a/src/components/DragAndDrop/NoDropZone/index.tsx
+++ b/src/components/DragAndDrop/NoDropZone/index.tsx
@@ -11,12 +11,14 @@ function NoDropZone({children}: NoDropZoneProps) {
const noDropZone = useRef(null);
useDragAndDrop({
+ // eslint-disable-next-line react-compiler/react-compiler
dropZone: htmlDivElementRef(noDropZone),
shouldAllowDrop: false,
});
return (
diff --git a/src/components/DragAndDrop/Provider/index.tsx b/src/components/DragAndDrop/Provider/index.tsx
index 1011fa161312..a403c7ecca0d 100644
--- a/src/components/DragAndDrop/Provider/index.tsx
+++ b/src/components/DragAndDrop/Provider/index.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-compiler/react-compiler */
import {PortalHost} from '@gorhom/portal';
import {Str} from 'expensify-common';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
diff --git a/src/components/DraggableList/useDraggableInPortal.ts b/src/components/DraggableList/useDraggableInPortal.ts
index 3b4610ce1e5e..e4e01ee4b133 100644
--- a/src/components/DraggableList/useDraggableInPortal.ts
+++ b/src/components/DraggableList/useDraggableInPortal.ts
@@ -7,6 +7,7 @@ type DraggableInPortal = {
};
export default function useDraggableInPortal({shouldUsePortal}: DraggableInPortal): (render: DraggableChildrenFn) => DraggableChildrenFn {
+ // eslint-disable-next-line react-compiler/react-compiler
const element = useRef(document.createElement('div')).current;
useEffect(() => {
diff --git a/src/components/EmojiPicker/EmojiPicker.tsx b/src/components/EmojiPicker/EmojiPicker.tsx
index 706265f2e11a..79af5bc0a4f2 100644
--- a/src/components/EmojiPicker/EmojiPicker.tsx
+++ b/src/components/EmojiPicker/EmojiPicker.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-compiler/react-compiler */
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import type {ForwardedRef, RefObject} from 'react';
import {Dimensions, View} from 'react-native';
@@ -117,7 +118,7 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef open(onPicked, newOnCanceled),
});
+ // eslint-disable-next-line react-compiler/react-compiler
return <>{renderChildren()}>;
}
diff --git a/src/components/FilePicker/index.tsx b/src/components/FilePicker/index.tsx
index 2514a16053bd..3d4242d22420 100644
--- a/src/components/FilePicker/index.tsx
+++ b/src/components/FilePicker/index.tsx
@@ -65,6 +65,7 @@ function FilePicker({children, acceptableFileTypes = ''}: FilePickerProps): Reac
}}
accept={acceptableFileTypes}
/>
+ {/* eslint-disable-next-line react-compiler/react-compiler */}
{children({
openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => {
onPicked.current = newOnPicked;
diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx
index 9bca23efb384..be0227375470 100644
--- a/src/components/FlatList/index.tsx
+++ b/src/components/FlatList/index.tsx
@@ -50,6 +50,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
const lastScrollOffsetRef = useRef(0);
const isListRenderedRef = useRef(false);
const mvcpAutoscrollToTopThresholdRef = useRef(mvcpAutoscrollToTopThreshold);
+ // eslint-disable-next-line react-compiler/react-compiler
mvcpAutoscrollToTopThresholdRef.current = mvcpAutoscrollToTopThreshold;
const getScrollOffset = useCallback((): number => {
diff --git a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
index 2608c58a4d23..63a33899822c 100644
--- a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
+++ b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx
@@ -1,6 +1,7 @@
import FocusTrap from 'focus-trap-react';
import React from 'react';
import sharedTrapStack from '@components/FocusTrap/sharedTrapStack';
+import blurActiveElement from '@libs/Accessibility/blurActiveElement';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import type FocusTrapForModalProps from './FocusTrapForModalProps';
@@ -9,6 +10,7 @@ function FocusTrapForModal({children, active, initialFocus = false}: FocusTrapFo
(p
const {registerInput} = useContext(FormContext);
const {shouldSetTouchedOnBlurOnly, blurOnSubmit, shouldSubmitForm} = computeComponentSpecificRegistrationParams(props as InputComponentBaseProps);
+ // eslint-disable-next-line react-compiler/react-compiler
const {key, ...registerInputProps} = registerInput(inputID, shouldSubmitForm, {ref, valueType, ...rest, shouldSetTouchedOnBlurOnly, blurOnSubmit});
return (
diff --git a/src/components/FormElement/index.tsx b/src/components/FormElement/index.tsx
index 9a344eb3c39c..a4b864d69466 100644
--- a/src/components/FormElement/index.tsx
+++ b/src/components/FormElement/index.tsx
@@ -13,6 +13,7 @@ const preventFormDefault = (event: SubmitEvent) => {
function FormElement(props: ViewProps, outerRef: ForwardedRef) {
const formRef = useRef(null);
+ // eslint-disable-next-line react-compiler/react-compiler
const mergedRef = mergeRefs(formRef, outerRef);
useEffect(() => {
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
index d2e407ff8b55..122db1e7877b 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
@@ -25,12 +25,15 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) {
const isAttachment = !!htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
const tNodeChild = tnode?.domNode?.children?.at(0);
const displayName = tNodeChild && 'data' in tNodeChild && typeof tNodeChild.data === 'string' ? tNodeChild.data : '';
- const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {};
const attrHref = htmlAttribs.href || htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] || '';
+ const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {};
const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref);
const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref);
const isVideo = attrHref && Str.isVideo(attrHref);
+ const isDeleted = HTMLEngineUtils.isDeletedNode(tnode);
+ const textDecorationLineStyle = isDeleted ? styles.underlineLineThrough : {};
+
if (!HTMLEngineUtils.isChildOfComment(tnode)) {
// This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click.
// We don't have this behaviour in other links in NewDot
@@ -51,13 +54,11 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) {
);
}
- const hasStrikethroughStyle = 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through';
- const textDecorationLineStyle = hasStrikethroughStyle ? styles.underlineLineThrough : {};
-
return (
setHasLoadFailed(true)}
onMeasure={() => setHasLoadFailed(false)}
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
index ce822af14cb8..ad7ea87f4c9b 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import type {CustomRendererProps, TBlock} from 'react-native-render-html';
import {AttachmentContext} from '@components/AttachmentContext';
+import {isDeletedNode} from '@components/HTMLEngineProvider/htmlEngineUtils';
import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import VideoPlayerPreview from '@components/VideoPlayerPreview';
import useCurrentReportID from '@hooks/useCurrentReportID';
@@ -25,6 +26,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) {
const height = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE]);
const duration = Number(htmlAttribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]);
const currentReportIDValue = useCurrentReportID();
+ const isDeleted = isDeletedNode(tnode);
return (
@@ -39,6 +41,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) {
thumbnailUrl={thumbnailUrl}
videoDimensions={{width, height}}
videoDuration={duration}
+ isDeleted={isDeleted}
onShowModalPress={() => {
if (!sourceURL || !type) {
return;
diff --git a/src/components/HTMLEngineProvider/htmlEngineUtils.ts b/src/components/HTMLEngineProvider/htmlEngineUtils.ts
index 5f082424a565..fba467add14b 100644
--- a/src/components/HTMLEngineProvider/htmlEngineUtils.ts
+++ b/src/components/HTMLEngineProvider/htmlEngineUtils.ts
@@ -59,4 +59,12 @@ function isChildOfH1(tnode: TNode): boolean {
return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && node.domNode.name.toLowerCase() === 'h1');
}
-export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1};
+/**
+ * Check if the parent node has deleted style.
+ */
+function isDeletedNode(tnode: TNode): boolean {
+ const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {};
+ return 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through';
+}
+
+export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1, isDeletedNode};
diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx
index 891a68cb38c4..b71f4db246a8 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -263,8 +263,8 @@ function HeaderWithBackButton({
)}
- {shouldDisplaySearchRouter && }
+ {shouldDisplaySearchRouter && }
);
diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx
index fd3d4f3d19e8..9bc0e846aaf1 100644
--- a/src/components/Hoverable/ActiveHoverable.tsx
+++ b/src/components/Hoverable/ActiveHoverable.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-compiler/react-compiler */
import type {Ref} from 'react';
import {cloneElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {DeviceEventEmitter} from 'react-native';
diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx
index e3357fd963c4..3ff28a8da451 100644
--- a/src/components/Hoverable/index.tsx
+++ b/src/components/Hoverable/index.tsx
@@ -16,6 +16,7 @@ function Hoverable({isDisabled, ...props}: HoverableProps, ref: Ref
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (isDisabled || !hasHoverSupport()) {
const child = getReturnValue(props.children, false);
+ // eslint-disable-next-line react-compiler/react-compiler
return cloneElement(child, {ref: mergeRefs(ref, child.ref)});
}
diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx
index e12be53d01ae..266ed2eed16a 100644
--- a/src/components/ImageView/index.tsx
+++ b/src/components/ImageView/index.tsx
@@ -210,6 +210,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
}
return (
();
const [attachmentInvalidReason, setAttachmentValidReason] = useState();
+ // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use different copies depending on the screen size
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const [isDraggingOver, setIsDraggingOver] = useState(false);
@@ -121,7 +123,7 @@ function ImportSpreedsheet({backTo, goTo}: ImportSpreedsheetProps) {
{translate('spreadsheet.upload')}
@@ -167,7 +169,7 @@ function ImportSpreedsheet({backTo, goTo}: ImportSpreedsheetProps) {
Navigation.navigate(backTo)}
+ onBackButtonPress={() => Navigation.goBack(backTo)}
/>
diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx
index 7d1e6614c716..b846449faafd 100644
--- a/src/components/KYCWall/BaseKYCWall.tsx
+++ b/src/components/KYCWall/BaseKYCWall.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-compiler/react-compiler */
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Dimensions} from 'react-native';
import type {EmitterSubscription, GestureResponderEvent, View} from 'react-native';
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index 8253a1708c81..3c40210a5d99 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -54,10 +54,13 @@ function OptionRowLHNData({
transactionViolations,
invoiceReceiverPolicy,
});
+ // eslint-disable-next-line react-compiler/react-compiler
if (deepEqual(item, optionItemRef.current)) {
+ // eslint-disable-next-line react-compiler/react-compiler
return optionItemRef.current;
}
+ // eslint-disable-next-line react-compiler/react-compiler
optionItemRef.current = item;
return item;
diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx
index ce4f3380a9b7..ff2db66dbc4a 100644
--- a/src/components/MagicCodeInput.tsx
+++ b/src/components/MagicCodeInput.tsx
@@ -212,6 +212,7 @@ function MagicCodeInput(
*/
const tapGesture = Gesture.Tap()
.runOnJS(true)
+ // eslint-disable-next-line react-compiler/react-compiler
.onBegin((event) => {
const index = Math.floor(event.x / (inputWidth.current / maxLength));
shouldFocusLast.current = false;
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index f530e1781034..a40fd925cd2e 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -114,6 +114,9 @@ type MenuItemBaseProps = {
/** Any additional styles to pass to the icon container. */
iconStyles?: StyleProp;
+ /** Additional styles to pass to the icon itself */
+ additionalIconStyles?: StyleProp;
+
/** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */
fallbackIcon?: IconAsset;
@@ -333,6 +336,9 @@ type MenuItemBaseProps = {
/** Handles what to do when hiding the tooltip */
onHideTooltip?: () => void;
+
+ /** Should use auto width for the icon container. */
+ shouldIconUseAutoWidthStyle?: boolean;
};
type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps;
@@ -430,8 +436,10 @@ function MenuItem(
tooltipShiftHorizontal = 0,
tooltipShiftVertical = 0,
renderTooltipContent,
+ additionalIconStyles,
shouldShowSelectedItemCheck = false,
onHideTooltip,
+ shouldIconUseAutoWidthStyle = false,
}: MenuItemProps,
ref: PressableRef,
) {
@@ -623,10 +631,22 @@ function MenuItem(
/>
)}
{!icon && shouldPutLeftPaddingWhenNoIcon && (
-
+
)}
{icon && !Array.isArray(icon) && (
-
+
{typeof icon !== 'string' &&
iconType === CONST.ICON_TYPE_ICON &&
(!shouldShowLoadingSpinnerIcon ? (
@@ -647,6 +667,7 @@ function MenuItem(
isPaneMenu,
)
}
+ additionalStyles={additionalIconStyles}
/>
) : (
;
+
+ /** Collection of draft categories attached to a policy */
+ policyCategoriesDraft: OnyxEntry;
+
+ /** Collection of tags attached to a policy */
+ policyTags: OnyxEntry;
+
+ /** The policy of the report */
+ policy: OnyxEntry;
+
+ /** The draft policy of the report */
+ policyDraft: OnyxEntry;
+
+ /** Mileage rate default for the policy */
+ defaultMileageRate: OnyxEntry;
+
+ /** Last selected distance rates */
+ lastSelectedDistanceRates: OnyxEntry>;
+
+ /** List of currencies */
+ currencyList: OnyxEntry;
+};
+
+type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & {
/** Callback to inform parent modal of success */
onConfirm?: (selectedParticipants: Participant[]) => void;
@@ -58,6 +85,9 @@ type MoneyRequestConfirmationListProps = {
/** IOU amount */
iouAmount: number;
+ /** IOU attendees list */
+ iouAttendees?: Attendee[];
+
/** IOU comment */
iouComment?: string;
@@ -148,11 +178,16 @@ function MoneyRequestConfirmationList({
onConfirm,
iouType = CONST.IOU.TYPE.SUBMIT,
iouAmount,
+ policyCategories: policyCategoriesReal,
+ policyCategoriesDraft,
isDistanceRequest = false,
+ policy: policyReal,
+ policyDraft,
isPolicyExpenseChat = false,
iouCategory = '',
shouldShowSmartScanFields = true,
isEditingSplitBill,
+ policyTags,
iouCurrencyCode,
iouMerchant,
selectedParticipants: selectedParticipantsProp,
@@ -162,6 +197,7 @@ function MoneyRequestConfirmationList({
policyID = '',
reportID = '',
receiptPath = '',
+ iouAttendees,
iouComment,
receiptFilename = '',
iouCreated,
@@ -169,21 +205,14 @@ function MoneyRequestConfirmationList({
onToggleBillable,
hasSmartScanFailed,
reportActionID,
+ defaultMileageRate,
+ lastSelectedDistanceRates,
action = CONST.IOU.ACTION.CREATE,
+ currencyList,
shouldDisplayReceipt = false,
shouldPlaySound = true,
isConfirmed,
}: MoneyRequestConfirmationListProps) {
- const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`);
- const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`);
- const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
- const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`);
- const [defaultMileageRate] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, {
- selector: (selectedPolicy) => DistanceRequestUtils.getDefaultMileageRate(selectedPolicy),
- });
- const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`);
- const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES);
- const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST);
const policy = policyReal ?? policyDraft;
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;
@@ -263,7 +292,12 @@ function MoneyRequestConfirmationList({
const formattedAmount = isDistanceRequestWithPendingRoute
? ''
: CurrencyUtils.convertToDisplayString(shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount, isDistanceRequest ? currency : iouCurrencyCode);
-
+ const formattedAmountPerAttendee = isDistanceRequestWithPendingRoute
+ ? ''
+ : CurrencyUtils.convertToDisplayString(
+ (shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount) / (iouAttendees?.length && iouAttendees.length > 0 ? iouAttendees.length : 1),
+ isDistanceRequest ? currency : iouCurrencyCode,
+ );
const isFocused = useIsFocused();
const [formError, debouncedFormError, setFormError] = useDebouncedState('');
@@ -358,7 +392,7 @@ function MoneyRequestConfirmationList({
let taxableAmount: number;
let taxCode: string;
if (isDistanceRequest) {
- const customUnitRate = getCustomUnitRate(policy, customUnitRateID);
+ const customUnitRate = getDistanceRateCustomUnitRate(policy, customUnitRateID);
taxCode = customUnitRate?.attributes?.taxRateExternalID ?? '';
taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance);
} else {
@@ -874,8 +908,10 @@ function MoneyRequestConfirmationList({
didConfirm={!!didConfirm}
distance={distance}
formattedAmount={formattedAmount}
+ formattedAmountPerAttendee={formattedAmountPerAttendee}
formError={formError}
hasRoute={hasRoute}
+ iouAttendees={iouAttendees}
iouCategory={iouCategory}
iouComment={iouComment}
iouCreated={iouCreated}
@@ -936,35 +972,67 @@ function MoneyRequestConfirmationList({
MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList';
-export default memo(
- MoneyRequestConfirmationList,
- (prevProps, nextProps) =>
- lodashIsEqual(prevProps.transaction, nextProps.transaction) &&
- prevProps.onSendMoney === nextProps.onSendMoney &&
- prevProps.onConfirm === nextProps.onConfirm &&
- prevProps.iouType === nextProps.iouType &&
- prevProps.iouAmount === nextProps.iouAmount &&
- prevProps.isDistanceRequest === nextProps.isDistanceRequest &&
- prevProps.isPolicyExpenseChat === nextProps.isPolicyExpenseChat &&
- prevProps.iouCategory === nextProps.iouCategory &&
- prevProps.shouldShowSmartScanFields === nextProps.shouldShowSmartScanFields &&
- prevProps.isEditingSplitBill === nextProps.isEditingSplitBill &&
- prevProps.iouCurrencyCode === nextProps.iouCurrencyCode &&
- prevProps.iouMerchant === nextProps.iouMerchant &&
- lodashIsEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) &&
- lodashIsEqual(prevProps.payeePersonalDetails, nextProps.payeePersonalDetails) &&
- prevProps.isReadOnly === nextProps.isReadOnly &&
- prevProps.bankAccountRoute === nextProps.bankAccountRoute &&
- prevProps.policyID === nextProps.policyID &&
- prevProps.reportID === nextProps.reportID &&
- prevProps.receiptPath === nextProps.receiptPath &&
- prevProps.iouComment === nextProps.iouComment &&
- prevProps.receiptFilename === nextProps.receiptFilename &&
- prevProps.iouCreated === nextProps.iouCreated &&
- prevProps.iouIsBillable === nextProps.iouIsBillable &&
- prevProps.onToggleBillable === nextProps.onToggleBillable &&
- prevProps.hasSmartScanFailed === nextProps.hasSmartScanFailed &&
- prevProps.reportActionID === nextProps.reportActionID &&
- lodashIsEqual(prevProps.action, nextProps.action) &&
- prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt,
+export default withOnyx({
+ policyCategories: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ },
+ policyCategoriesDraft: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`,
+ },
+ policyTags: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ },
+ defaultMileageRate: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ selector: DistanceRequestUtils.getDefaultMileageRate,
+ },
+ policy: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ },
+ policyDraft: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`,
+ },
+ lastSelectedDistanceRates: {
+ key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES,
+ },
+ currencyList: {
+ key: ONYXKEYS.CURRENCY_LIST,
+ },
+})(
+ memo(
+ MoneyRequestConfirmationList,
+ (prevProps, nextProps) =>
+ lodashIsEqual(prevProps.transaction, nextProps.transaction) &&
+ prevProps.onSendMoney === nextProps.onSendMoney &&
+ prevProps.onConfirm === nextProps.onConfirm &&
+ prevProps.iouType === nextProps.iouType &&
+ prevProps.iouAmount === nextProps.iouAmount &&
+ prevProps.isDistanceRequest === nextProps.isDistanceRequest &&
+ prevProps.isPolicyExpenseChat === nextProps.isPolicyExpenseChat &&
+ prevProps.iouCategory === nextProps.iouCategory &&
+ prevProps.shouldShowSmartScanFields === nextProps.shouldShowSmartScanFields &&
+ prevProps.isEditingSplitBill === nextProps.isEditingSplitBill &&
+ prevProps.iouCurrencyCode === nextProps.iouCurrencyCode &&
+ prevProps.iouMerchant === nextProps.iouMerchant &&
+ lodashIsEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) &&
+ lodashIsEqual(prevProps.payeePersonalDetails, nextProps.payeePersonalDetails) &&
+ prevProps.isReadOnly === nextProps.isReadOnly &&
+ prevProps.bankAccountRoute === nextProps.bankAccountRoute &&
+ prevProps.policyID === nextProps.policyID &&
+ prevProps.reportID === nextProps.reportID &&
+ prevProps.receiptPath === nextProps.receiptPath &&
+ prevProps.iouAttendees === nextProps.iouAttendees &&
+ prevProps.iouComment === nextProps.iouComment &&
+ prevProps.receiptFilename === nextProps.receiptFilename &&
+ prevProps.iouCreated === nextProps.iouCreated &&
+ prevProps.iouIsBillable === nextProps.iouIsBillable &&
+ prevProps.onToggleBillable === nextProps.onToggleBillable &&
+ prevProps.hasSmartScanFailed === nextProps.hasSmartScanFailed &&
+ prevProps.reportActionID === nextProps.reportActionID &&
+ lodashIsEqual(prevProps.defaultMileageRate, nextProps.defaultMileageRate) &&
+ lodashIsEqual(prevProps.lastSelectedDistanceRates, nextProps.lastSelectedDistanceRates) &&
+ lodashIsEqual(prevProps.action, nextProps.action) &&
+ lodashIsEqual(prevProps.currencyList, nextProps.currencyList) &&
+ prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt,
+ ),
);
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index e6b957f16b60..dcfe72369651 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -25,7 +25,7 @@ import type {IOUAction, IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
-import type {Participant} from '@src/types/onyx/IOU';
+import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {Unit} from '@src/types/onyx/Policy';
import ConfirmedRoute from './ConfirmedRoute';
import MentionReportContext from './HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
@@ -57,6 +57,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** The formatted amount of the transaction */
formattedAmount: string;
+ /** The formatted amount of the transaction per 1 attendee */
+ formattedAmountPerAttendee: string;
+
/** The error message for the form */
formError: string;
@@ -66,6 +69,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** The category of the IOU */
iouCategory: string;
+ /** The list of attendees */
+ iouAttendees: Attendee[] | undefined;
+
/** The comment of the IOU */
iouComment: string | undefined;
@@ -176,8 +182,10 @@ function MoneyRequestConfirmationListFooter({
didConfirm,
distance,
formattedAmount,
+ formattedAmountPerAttendee,
formError,
hasRoute,
+ iouAttendees,
iouCategory,
iouComment,
iouCreated,
@@ -226,6 +234,7 @@ function MoneyRequestConfirmationListFooter({
const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);
const isMultilevelTags = useMemo(() => PolicyUtils.isMultiLevelTags(policyTags), [policyTags]);
+ const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]);
const senderWorkspace = useMemo(() => {
const senderWorkspaceParticipant = selectedParticipants.find((participant) => participant.isSender);
@@ -514,6 +523,25 @@ function MoneyRequestConfirmationListFooter({
shouldShow: shouldShowTax,
isSupplementary: true,
},
+ {
+ item: (
+ item?.displayName ?? item?.login).join(', ')}
+ description={`${translate('iou.attendees')} ${
+ iouAttendees?.length && iouAttendees.length > 1 ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : ''
+ }`}
+ style={[styles.moneyRequestMenuItem]}
+ titleStyle={styles.flex1}
+ onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ interactive
+ shouldRenderAsHTML
+ />
+ ),
+ shouldShow: shouldShowAttendees,
+ isSupplementary: true,
+ },
{
item: (
({options, initializeOptions, areOptionsInitialized: areOptionsInitialized.current}), [options, initializeOptions])}>
{children}
diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx
index 332b42e06119..67ecac27afbd 100644
--- a/src/components/Popover/index.tsx
+++ b/src/components/Popover/index.tsx
@@ -31,6 +31,7 @@ function Popover(props: PopoverProps) {
} = props;
// We need to use isSmallScreenWidth to apply the correct modal type and popoverAnchorPosition
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const withoutOverlayRef = useRef(null);
const {close, popover} = React.useContext(PopoverContext);
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index b1aa2fc28338..7c8c99d6305d 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -161,6 +161,7 @@ function PopoverMenu({
const styles = useThemeStyles();
const theme = useTheme();
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply correct popover styles
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const [currentMenuItems, setCurrentMenuItems] = useState(menuItems);
const currentMenuItemsFocusedIndex = currentMenuItems?.findIndex((option) => option.isSelected);
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index 82f3c6c7d61a..b59d1604a5aa 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -117,6 +117,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
() => ({
onOpen,
close: closePopover,
+ // eslint-disable-next-line react-compiler/react-compiler
popover: activePopoverRef.current,
isOpen,
}),
diff --git a/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
index 84a595a7bf05..1765560eaae3 100644
--- a/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
+++ b/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
@@ -148,6 +148,7 @@ function GenericPressable(
onLayout={shouldUseAutoHitSlop ? onLayout : undefined}
ref={ref as ForwardedRef}
disabled={fullDisabled}
+ // eslint-disable-next-line react-compiler/react-compiler
onPress={!isDisabled ? singleExecution(onPressHandler) : undefined}
onLongPress={!isDisabled && onLongPress ? onLongPressHandler : undefined}
onKeyDown={!isDisabled ? onKeyDown : undefined}
diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx
index d140e71bceae..3d6ad9006dc5 100644
--- a/src/components/ProcessMoneyReportHoldMenu.tsx
+++ b/src/components/ProcessMoneyReportHoldMenu.tsx
@@ -60,7 +60,8 @@ function ProcessMoneyReportHoldMenu({
}: ProcessMoneyReportHoldMenuProps) {
const {translate} = useLocalize();
const isApprove = requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE;
- // We need to use shouldUseNarrowLayout instead of shouldUseNarrowLayout to apply the correct modal type
+ // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const onSubmit = (full: boolean) => {
diff --git a/src/components/PushRowWithModal/PushRowModal.tsx b/src/components/PushRowWithModal/PushRowModal.tsx
new file mode 100644
index 000000000000..79fbc53c1e2c
--- /dev/null
+++ b/src/components/PushRowWithModal/PushRowModal.tsx
@@ -0,0 +1,120 @@
+import React, {useEffect, useState} from 'react';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import Modal from '@components/Modal';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import useLocalize from '@hooks/useLocalize';
+import CONST from '@src/CONST';
+
+type PushRowModalProps = {
+ /** Whether the modal is visible */
+ isVisible: boolean;
+
+ /** The currently selected option */
+ selectedOption: string;
+
+ /** Function to call when the user selects an option */
+ onOptionChange: (option: string) => void;
+
+ /** Function to call when the user closes the modal */
+ onClose: () => void;
+
+ /** The list of items to render */
+ optionsList: Record;
+
+ /** The title of the modal */
+ headerTitle: string;
+
+ /** The title of the search input */
+ searchInputTitle?: string;
+};
+
+type ListItemType = {
+ value: string;
+ text: string;
+ keyForList: string;
+ isSelected: boolean;
+};
+
+function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optionsList, headerTitle, searchInputTitle}: PushRowModalProps) {
+ const {translate} = useLocalize();
+
+ const allOptions = Object.entries(optionsList).map(([key, value]) => ({
+ value: key,
+ text: value,
+ keyForList: key,
+ isSelected: key === selectedOption,
+ }));
+ const [searchbarInputText, setSearchbarInputText] = useState('');
+ const [optionListItems, setOptionListItems] = useState(allOptions);
+
+ useEffect(() => {
+ setOptionListItems((prevOptionListItems) =>
+ prevOptionListItems.map((option) => ({
+ ...option,
+ isSelected: option.value === selectedOption,
+ })),
+ );
+ }, [selectedOption]);
+
+ const filterShownOptions = (searchText: string) => {
+ setSearchbarInputText(searchText);
+ const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) ?? [];
+ setOptionListItems(
+ allOptions.filter((option) =>
+ searchWords.every((word) =>
+ option.text
+ .toLowerCase()
+ .replace(/[^a-z0-9]/g, ' ')
+ .includes(word),
+ ),
+ ),
+ );
+ };
+
+ const handleSelectRow = (option: ListItemType) => {
+ onOptionChange(option.value);
+ onClose();
+ };
+
+ return (
+
+
+
+ option.value === selectedOption)?.keyForList}
+ showScrollIndicator
+ shouldShowTooltips={false}
+ ListItem={RadioListItem}
+ />
+
+
+ );
+}
+
+PushRowModal.displayName = 'PushRowModal';
+
+export type {ListItemType};
+
+export default PushRowModal;
diff --git a/src/components/PushRowWithModal/index.tsx b/src/components/PushRowWithModal/index.tsx
new file mode 100644
index 000000000000..11c7ff4386d4
--- /dev/null
+++ b/src/components/PushRowWithModal/index.tsx
@@ -0,0 +1,88 @@
+import React, {useState} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import CONST from '@src/CONST';
+import PushRowModal from './PushRowModal';
+
+type PushRowWithModalProps = {
+ /** The list of options that we want to display where key is option code and value is option name */
+ optionsList: Record;
+
+ /** The currently selected option */
+ selectedOption: string;
+
+ /** Function to call when the user selects an option */
+ onOptionChange: (value: string) => void;
+
+ /** Additional styles to apply to container */
+ wrapperStyles?: StyleProp;
+
+ /** The description for the picker */
+ description: string;
+
+ /** The title of the modal */
+ modalHeaderTitle: string;
+
+ /** The title of the search input */
+ searchInputTitle: string;
+
+ /** Whether the selected option is editable */
+ shouldAllowChange?: boolean;
+
+ /** Text to display on error message */
+ errorText?: string;
+};
+
+function PushRowWithModal({
+ selectedOption,
+ onOptionChange,
+ optionsList,
+ wrapperStyles,
+ description,
+ modalHeaderTitle,
+ searchInputTitle,
+ shouldAllowChange = true,
+ errorText,
+}: PushRowWithModalProps) {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+
+ const handleModalClose = () => {
+ setIsModalVisible(false);
+ };
+
+ const handleModalOpen = () => {
+ setIsModalVisible(true);
+ };
+
+ const handleOptionChange = (value: string) => {
+ onOptionChange(value);
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+PushRowWithModal.displayName = 'PushRowWithModal';
+
+export default PushRowWithModal;
diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.tsx b/src/components/Reactions/ReportActionItemEmojiReactions.tsx
index 943158607db4..bc5f48f9001c 100644
--- a/src/components/Reactions/ReportActionItemEmojiReactions.tsx
+++ b/src/components/Reactions/ReportActionItemEmojiReactions.tsx
@@ -104,6 +104,7 @@ function ReportActionItemEmojiReactions({
if (reactionCount === 0) {
return null;
}
+ // eslint-disable-next-line react-compiler/react-compiler
totalReactionCount += reactionCount;
const onPress = () => {
diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
index 2f96b9d7dfaf..50e753b25755 100644
--- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
+++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
@@ -12,6 +12,7 @@ import * as ReportActions from '@libs/actions/Report';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import type {ExportType} from '@pages/home/report/ReportDetailsExportPage';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report} from '@src/types/onyx';
@@ -55,6 +56,9 @@ function ExportWithDropdownMenu({
icon: iconToDisplay,
disabled: !canBeExported,
displayInDefaultIconColor: true,
+ iconWidth: variables.iconSizeMenuItem,
+ iconHeight: variables.iconSizeMenuItem,
+ additionalIconStyles: styles.integrationIcon,
};
const options = [
{
diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx
index c9fa9a6f2df0..9ac60e3a3a21 100644
--- a/src/components/ReportActionItem/IssueCardMessage.tsx
+++ b/src/components/ReportActionItem/IssueCardMessage.tsx
@@ -6,6 +6,7 @@ import RenderHTML from '@components/RenderHTML';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -24,6 +25,9 @@ function IssueCardMessage({action, policyID}: IssueCardMessageProps) {
const styles = useThemeStyles();
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
const [session] = useOnyx(ONYXKEYS.SESSION);
+ const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID ?? '-1');
+ const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
+ const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`);
const assigneeAccountID = (ReportActionsUtils.getOriginalMessage(action) as IssueNewCardOriginalMessage)?.assigneeAccountID;
@@ -38,10 +42,21 @@ function IssueCardMessage({action, policyID}: IssueCardMessageProps) {
const isAssigneeCurrentUser = !isEmptyObject(session) && session.accountID === assigneeAccountID;
const shouldShowAddMissingDetailsButton = action?.actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && missingDetails && isAssigneeCurrentUser;
+ const cardIssuedActionOriginalMessage = ReportActionsUtils.isActionOfType(
+ action,
+ CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED,
+ CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL,
+ CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS,
+ )
+ ? ReportActionsUtils.getOriginalMessage(action)
+ : undefined;
+ const cardID = cardIssuedActionOriginalMessage?.cardID ?? -1;
+ const isPolicyAdmin = PolicyUtils.isPolicyAdmin(PolicyUtils.getPolicy(policyID));
+ const card = isPolicyAdmin ? cardsList?.[cardID] : cardList[cardID];
return (
<>
- ${ReportActionsUtils.getCardIssuedMessage(action, true, policyID)}`} />
+ ${ReportActionsUtils.getCardIssuedMessage(action, true, policyID, !!card)}`} />
{shouldShowAddMissingDetailsButton && (
Navigation.navigate(ROUTES.MISSING_PERSONAL_DETAILS)}
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 0e2383295d2a..efc7dc0e198e 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -115,6 +115,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const {
created: transactionDate,
amount: transactionAmount,
+ attendees: transactionAttendees,
taxAmount: transactionTaxAmount,
currency: transactionCurrency,
comment: transactionDescription,
@@ -128,6 +129,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction);
const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : '';
+ const formattedPerAttendeeAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount / (transactionAttendees?.length ?? 1), transactionCurrency) : '';
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
const cardProgramName = TransactionUtils.getCardName(transaction);
@@ -184,6 +186,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
+ const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]);
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest);
const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport);
@@ -721,6 +724,25 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
}}
/>
)}
+ {shouldShowAttendees && (
+
+ item?.displayName ?? item?.login).join(', ')}
+ description={`${translate('iou.attendees')} ${
+ transactionAttendees?.length && transactionAttendees.length > 1 ? `${formattedPerAttendeeAmount} ${translate('common.perPerson')}` : ''
+ }`}
+ style={[styles.moneyRequestMenuItem]}
+ titleStyle={styles.flex1}
+ onPress={() =>
+ Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ }
+ interactive
+ shouldRenderAsHTML
+ />
+
+ )}
{shouldShowBillable && (
diff --git a/src/components/RequireTwoFactorAuthenticationModal.tsx b/src/components/RequireTwoFactorAuthenticationModal.tsx
index 229231e8ff25..ad4f2db28c1c 100644
--- a/src/components/RequireTwoFactorAuthenticationModal.tsx
+++ b/src/components/RequireTwoFactorAuthenticationModal.tsx
@@ -47,6 +47,7 @@ function RequireTwoFactorAuthenticationModal({onCancel = () => {}, description,
type={shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
innerContainerStyle={{...styles.pb5, ...styles.pt3, ...styles.boxShadowNone}}
shouldEnableNewFocusManagement={shouldEnableNewFocusManagement}
+ animationOutTiming={500}
>
(false);
+ // eslint-disable-next-line react-compiler/react-compiler
isKeyboardShownRef.current = keyboardState?.isKeyboardShown ?? false;
const panResponder = useRef(
diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx
index 30825ed3bfba..f3206868d556 100644
--- a/src/components/Search/SearchContext.tsx
+++ b/src/components/Search/SearchContext.tsx
@@ -1,6 +1,6 @@
import React, {useCallback, useContext, useMemo, useState} from 'react';
import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchUIUtils from '@libs/SearchUIUtils';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type {SearchContext, SelectedTransactions} from './types';
@@ -23,8 +23,8 @@ function getReportsFromSelectedTransactions(data: TransactionListItemType[] | Re
return (data ?? [])
.filter(
(item) =>
- !SearchUtils.isTransactionListItemType(item) &&
- !SearchUtils.isReportActionListItemType(item) &&
+ !SearchUIUtils.isTransactionListItemType(item) &&
+ !SearchUIUtils.isReportActionListItemType(item) &&
item.reportID &&
item?.transactions?.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected),
)
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
index e544c13236a1..65d86005207c 100644
--- a/src/components/Search/SearchPageHeader.tsx
+++ b/src/components/Search/SearchPageHeader.tsx
@@ -23,7 +23,7 @@ import * as SearchActions from '@libs/actions/Search';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -121,6 +121,8 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
const styles = useThemeStyles();
const {isOffline} = useNetwork();
const {activeWorkspaceID} = useActiveWorkspace();
+ // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const {selectedTransactions, clearSelectedTransactions, selectedReports} = useSearchContext();
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE);
@@ -136,8 +138,8 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false);
const {status, type} = queryJSON;
- const isCannedQuery = SearchUtils.isCannedSearchQuery(queryJSON);
- const headerText = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates);
+ const isCannedQuery = SearchQueryUtils.isCannedSearchQuery(queryJSON);
+ const headerText = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchQueryUtils.buildUserReadableQueryString(queryJSON, personalDetails, cardList, reports, taxRates);
const [inputValue, setInputValue] = useState(headerText);
useEffect(() => {
@@ -327,7 +329,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
}
const onPress = () => {
- const filterFormValues = SearchUtils.buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, cardList, reports, taxRates);
+ const filterFormValues = SearchQueryUtils.buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, cardList, reports, taxRates);
SearchActions.updateAdvancedFilters(filterFormValues);
Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS);
@@ -337,10 +339,10 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
if (!inputValue) {
return;
}
- const inputQueryJSON = SearchUtils.buildSearchQueryJSON(inputValue);
+ const inputQueryJSON = SearchQueryUtils.buildSearchQueryJSON(inputValue);
if (inputQueryJSON) {
- const standardizedQuery = SearchUtils.standardizeQueryJSON(inputQueryJSON, cardList, taxRates);
- const query = SearchUtils.buildSearchQueryString(standardizedQuery);
+ const standardizedQuery = SearchQueryUtils.standardizeQueryJSON(inputQueryJSON, cardList, taxRates);
+ const query = SearchQueryUtils.buildSearchQueryString(standardizedQuery);
SearchActions.clearAllFilters();
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
} else {
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index 36cadd486528..cc9e9c6ca024 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -12,11 +12,12 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
+import FastSearch from '@libs/FastSearch';
import Log from '@libs/Log';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import {getAllTaxRates} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
import * as Report from '@userActions/Report';
@@ -40,7 +41,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES);
const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false});
- const {isSmallScreenWidth} = useResponsiveLayout();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const listRef = useRef(null);
const taxRates = getAllTaxRates();
@@ -63,6 +64,49 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
return OptionsListUtils.getSearchOptions(options, '', betas ?? []);
}, [areOptionsInitialized, betas, options]);
+ /**
+ * Builds a suffix tree and returns a function to search in it.
+ */
+ const findInSearchTree = useMemo(() => {
+ const fastSearch = FastSearch.createFastSearch([
+ {
+ data: searchOptions.personalDetails,
+ toSearchableString: (option) => {
+ const displayName = option.participantsList?.[0]?.displayName ?? '';
+ return [option.login ?? '', option.login !== displayName ? displayName : ''].join();
+ },
+ },
+ {
+ data: searchOptions.recentReports,
+ toSearchableString: (option) => {
+ const searchStringForTree = [option.text ?? '', option.login ?? ''];
+
+ if (option.isThread) {
+ if (option.alternateText) {
+ searchStringForTree.push(option.alternateText);
+ }
+ } else if (!!option.isChatRoom || !!option.isPolicyExpenseChat) {
+ if (option.subtitle) {
+ searchStringForTree.push(option.subtitle);
+ }
+ }
+
+ return searchStringForTree.join();
+ },
+ },
+ ]);
+ function search(searchInput: string) {
+ const [personalDetails, recentReports] = fastSearch.search(searchInput);
+
+ return {
+ personalDetails,
+ recentReports,
+ };
+ }
+
+ return search;
+ }, [searchOptions.personalDetails, searchOptions.recentReports]);
+
const filteredOptions = useMemo(() => {
if (debouncedInputValue.trim() === '') {
return {
@@ -73,15 +117,25 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
}
Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS);
- const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedInputValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true});
+ const newOptions = findInSearchTree(debouncedInputValue);
Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS);
- return {
+ const recentReports = newOptions.recentReports.concat(newOptions.personalDetails);
+
+ const userToInvite = OptionsListUtils.pickUserToInvite({
+ canInviteUser: true,
recentReports: newOptions.recentReports,
personalDetails: newOptions.personalDetails,
- userToInvite: newOptions.userToInvite,
+ searchValue: debouncedInputValue,
+ optionsToExclude: [{login: CONST.EMAIL.NOTIFICATIONS}],
+ });
+
+ return {
+ recentReports,
+ personalDetails: [],
+ userToInvite,
};
- }, [debouncedInputValue, searchOptions]);
+ }, [debouncedInputValue, findInSearchTree]);
const recentReports: OptionData[] = useMemo(() => {
if (debouncedInputValue === '') {
@@ -116,7 +170,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
return;
}
listRef.current?.updateAndScrollToFocusedIndex(0);
- const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery);
+ const queryJSON = SearchQueryUtils.buildSearchQueryJSON(userQuery);
if (queryJSON) {
setUserSearchQuery(queryJSON);
@@ -147,8 +201,8 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
return;
}
onRouterClose();
- const standardizedQuery = SearchUtils.standardizeQueryJSON(query, cardList, taxRates);
- const queryString = SearchUtils.buildSearchQueryString(standardizedQuery);
+ const standardizedQuery = SearchQueryUtils.standardizeQueryJSON(query, cardList, taxRates);
+ const queryString = SearchQueryUtils.buildSearchQueryString(standardizedQuery);
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString}));
clearUserQuery();
},
@@ -161,14 +215,14 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
closeAndClearRouter();
});
- const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.searchRouterPopoverWidth};
+ const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth};
return (
- {isSmallScreenWidth && (
+ {shouldUseNarrowLayout && (
onRouterClose()}
@@ -177,15 +231,15 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
{
- onSearchSubmit(SearchUtils.buildSearchQueryJSON(textInputValue));
+ onSearchSubmit(SearchQueryUtils.buildSearchQueryJSON(textInputValue));
}}
routerListRef={listRef}
shouldShowOfflineMessage
wrapperStyle={[styles.border, styles.alignItemsCenter]}
- outerWrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2]}
+ outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]}
wrapperFocusedStyle={[styles.borderColorFocus]}
isSearchingForReports={isSearchingForReports}
/>
diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx
index 68142a7bc041..8433b67dc7d7 100644
--- a/src/components/Search/SearchRouter/SearchRouterList.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterList.tsx
@@ -16,7 +16,7 @@ import Navigation from '@libs/Navigation/Navigation';
import Performance from '@libs/Performance';
import {getAllTaxRates} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import * as Report from '@userActions/Report';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
@@ -55,6 +55,10 @@ const setPerformanceTimersEnd = () => {
Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER);
};
+function getContextualSearchQuery(reportID: string) {
+ return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${CONST.SEARCH.DATA_TYPES.CHAT} in:${reportID}`;
+}
+
function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem {
if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) {
return true;
@@ -92,7 +96,7 @@ function SearchRouterList(
) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {isSmallScreenWidth} = useResponsiveLayout();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const personalDetails = usePersonalDetails();
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
@@ -120,7 +124,7 @@ function SearchRouterList(
{
text: `${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`,
singleIcon: Expensicons.MagnifyingGlass,
- query: SearchUtils.getContextualSuggestionQuery(reportForContextualSearch.reportID),
+ query: getContextualSearchQuery(reportForContextualSearch.reportID),
itemStyle: styles.activeComponentBG,
keyForList: 'contextualSearch',
isContextualSearchItem: true,
@@ -130,9 +134,9 @@ function SearchRouterList(
}
const recentSearchesData = recentSearches?.map(({query, timestamp}) => {
- const searchQueryJSON = SearchUtils.buildSearchQueryJSON(query);
+ const searchQueryJSON = SearchQueryUtils.buildSearchQueryJSON(query);
return {
- text: searchQueryJSON ? SearchUtils.getSearchHeaderTitle(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query,
+ text: searchQueryJSON ? SearchQueryUtils.buildUserReadableQueryString(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query,
singleIcon: Expensicons.History,
query,
keyForList: timestamp,
@@ -159,7 +163,7 @@ function SearchRouterList(
if (!item?.query) {
return;
}
- onSearchSubmit(SearchUtils.buildSearchQueryJSON(item?.query));
+ onSearchSubmit(SearchQueryUtils.buildSearchQueryJSON(item?.query));
}
// Handle selection of "Recent chat"
@@ -179,11 +183,11 @@ function SearchRouterList(
onSelectRow={onSelectRow}
ListItem={SearchRouterItem}
containerStyle={[styles.mh100]}
- sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb2]}
+ sectionListStyle={[shouldUseNarrowLayout ? styles.ph5 : styles.ph2, styles.pb2]}
listItemWrapperStyle={[styles.pr3, styles.pl3]}
onLayout={setPerformanceTimersEnd}
ref={ref}
- showScrollIndicator={!isSmallScreenWidth}
+ showScrollIndicator={!shouldUseNarrowLayout}
sectionTitleStyles={styles.mhn2}
shouldSingleExecuteRowSelect
/>
diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx
index 62cdb38246b4..2626b8565e2a 100644
--- a/src/components/Search/SearchRouter/SearchRouterModal.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx
@@ -8,10 +8,10 @@ import SearchRouter from './SearchRouter';
import {useSearchRouterContext} from './SearchRouterContext';
function SearchRouterModal() {
- const {isSmallScreenWidth} = useResponsiveLayout();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext();
- const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE : CONST.MODAL.MODAL_TYPE.POPOVER;
+ const modalType = shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE : CONST.MODAL.MODAL_TYPE.POPOVER;
return (
{
const onPress = singleExecution(() => {
onStatusChange?.();
- const query = SearchUtils.buildSearchQueryString({...queryJSON, status: item.status});
+ const query = SearchQueryUtils.buildSearchQueryString({...queryJSON, status: item.status});
Navigation.setParams({q: query});
});
const isActive = queryJSON.status === item.status;
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 0b0d91051bd2..4a1a67505d91 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -1,4 +1,4 @@
-import {useNavigation} from '@react-navigation/native';
+import {useIsFocused, useNavigation} from '@react-navigation/native';
import type {StackNavigationProp} from '@react-navigation/stack';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
@@ -21,8 +21,10 @@ import * as SearchActions from '@libs/actions/Search';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import memoize from '@libs/memoize';
+import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane';
import * as ReportUtils from '@libs/ReportUtils';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
+import * as SearchUIUtils from '@libs/SearchUIUtils';
import Navigation from '@navigation/Navigation';
import type {AuthScreensParamList} from '@navigation/types';
import EmptySearchView from '@pages/Search/EmptySearchView';
@@ -60,11 +62,11 @@ function mapToItemWithSelectionInfo(
canSelectMultiple: boolean,
shouldAnimateInHighlight: boolean,
) {
- if (SearchUtils.isReportActionListItemType(item)) {
+ if (SearchUIUtils.isReportActionListItemType(item)) {
return item;
}
- return SearchUtils.isTransactionListItemType(item)
+ return SearchUIUtils.isTransactionListItemType(item)
? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight)
: {
...item,
@@ -88,8 +90,11 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
const {isOffline} = useNetwork();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const styles = useThemeStyles();
+ // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for enabling the selection mode on small screens only
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth, isLargeScreenWidth} = useResponsiveLayout();
const navigation = useNavigation>();
+ const isFocused = useIsFocused();
const lastSearchResultsRef = useRef>();
const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions, setShouldShowStatusBarLoading, lastSearchType, setLastSearchType} =
useSearchContext();
@@ -139,7 +144,7 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
const getItemHeight = useCallback(
(item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
- if (SearchUtils.isTransactionListItemType(item) || SearchUtils.isReportActionListItemType(item)) {
+ if (SearchUIUtils.isTransactionListItemType(item) || SearchUIUtils.isReportActionListItemType(item)) {
return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding;
}
@@ -167,10 +172,13 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
});
// save last non-empty search results to avoid ugly flash of loading screen when hash changes and onyx returns empty data
+ // eslint-disable-next-line react-compiler/react-compiler
if (currentSearchResults?.data && currentSearchResults !== lastSearchResultsRef.current) {
+ // eslint-disable-next-line react-compiler/react-compiler
lastSearchResultsRef.current = currentSearchResults;
}
+ // eslint-disable-next-line react-compiler/react-compiler
const searchResults = currentSearchResults?.data ? currentSearchResults : lastSearchResultsRef.current;
const {newSearchResultKey, handleSelectionListScroll} = useSearchHighlightAndScroll({
@@ -186,9 +194,9 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
const isDataLoaded = searchResults?.data !== undefined && searchResults?.search?.type === type && searchResults?.search?.status === status;
const shouldShowLoadingState = !isOffline && !isDataLoaded;
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
- const isSearchResultsEmpty = !searchResults?.data || SearchUtils.isSearchResultsEmpty(searchResults);
+ const isSearchResultsEmpty = !searchResults?.data || SearchUIUtils.isSearchResultsEmpty(searchResults);
const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty);
- const data = searchResults === undefined ? [] : SearchUtils.getSections(type, status, searchResults.data, searchResults.search);
+ const data = searchResults === undefined ? [] : SearchUIUtils.getSections(type, status, searchResults.data, searchResults.search);
useEffect(() => {
/** We only want to display the skeleton for the status filters the first time we load them for a specific data type */
@@ -246,6 +254,17 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
turnOffMobileSelectionMode();
}, [isSearchResultsEmpty, prevIsSearchResultEmpty]);
+ useEffect(
+ () => () => {
+ if (isSearchTopmostCentralPane()) {
+ return;
+ }
+ clearSelectedTransactions();
+ turnOffMobileSelectionMode();
+ },
+ [isFocused, clearSelectedTransactions],
+ );
+
if (shouldShowLoadingState) {
return (
{null};
}
- const ListItem = SearchUtils.getListItem(type, status);
- const sortedData = SearchUtils.getSortedSections(type, status, data, sortBy, sortOrder);
+ const ListItem = SearchUIUtils.getListItem(type, status);
+ const sortedData = SearchUIUtils.getSortedSections(type, status, data, sortBy, sortOrder);
const sortedSelectedData = sortedData.map((item) => {
const baseKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`;
// Check if the base key matches the newSearchResultKey (TransactionListItemType)
@@ -288,10 +307,10 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
}
const toggleTransaction = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
- if (SearchUtils.isReportActionListItemType(item)) {
+ if (SearchUIUtils.isReportActionListItemType(item)) {
return;
}
- if (SearchUtils.isTransactionListItemType(item)) {
+ if (SearchUIUtils.isTransactionListItemType(item)) {
if (!item.keyForList) {
return;
}
@@ -322,21 +341,21 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
const openReport = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORTID;
- let reportID = SearchUtils.isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID;
+ let reportID = SearchUIUtils.isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID;
if (!reportID) {
return;
}
// If we're trying to open a legacy transaction without a transaction thread, let's create the thread and navigate the user
- if (SearchUtils.isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) {
+ if (SearchUIUtils.isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) {
reportID = ReportUtils.generateReportID();
SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID);
}
const backTo = Navigation.getActiveRoute();
- if (SearchUtils.isReportActionListItemType(item)) {
+ if (SearchUIUtils.isReportActionListItemType(item)) {
const reportActionID = item.reportActionID;
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo}));
return;
@@ -372,11 +391,11 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
};
const onSortPress = (column: SearchColumnType, order: SortOrder) => {
- const newQuery = SearchUtils.buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order});
+ const newQuery = SearchQueryUtils.buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order});
navigation.setParams({q: newQuery});
};
- const shouldShowYear = SearchUtils.shouldShowYear(searchResults?.data);
+ const shouldShowYear = SearchUIUtils.shouldShowYear(searchResults?.data);
const shouldShowSorting = sortableSearchStatuses.includes(status);
return (
@@ -401,7 +420,7 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
)
}
isSelected={(item) =>
- status !== CONST.SEARCH.STATUS.EXPENSE.ALL && SearchUtils.isReportListItemType(item)
+ status !== CONST.SEARCH.STATUS.EXPENSE.ALL && SearchUIUtils.isReportListItemType(item)
? item.transactions.some((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)
: !!item.isSelected
}
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index c7779001b38d..57423992e43e 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -24,7 +24,7 @@ import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset';
import Log from '@libs/Log';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchUIUtils from '@libs/SearchUIUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -438,7 +438,7 @@ function BaseSelectionList(
const showTooltip = shouldShowTooltips && normalizedIndex < 10;
const handleOnCheckboxPress = () => {
- if (SearchUtils.isReportListItemType(item)) {
+ if (SearchUIUtils.isReportListItemType(item)) {
return onCheckboxPress;
}
return onCheckboxPress ? () => onCheckboxPress(item) : undefined;
diff --git a/src/components/SelectionList/ListItemRightCaretWithLabel.tsx b/src/components/SelectionList/ListItemRightCaretWithLabel.tsx
index f20c4179f0fe..74c519333a28 100644
--- a/src/components/SelectionList/ListItemRightCaretWithLabel.tsx
+++ b/src/components/SelectionList/ListItemRightCaretWithLabel.tsx
@@ -3,6 +3,7 @@ import {View} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
+import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -11,13 +12,14 @@ type ListItemRightCaretWithLabelProps = {
shouldShowCaret?: boolean;
};
-function ListItemRightCaretWithLabel({labelText, shouldShowCaret = true}: ListItemRightCaretWithLabelProps) {
+function ListItemRightCaretWithLabel({labelText, shouldShowCaret = false}: ListItemRightCaretWithLabelProps) {
const styles = useThemeStyles();
const theme = useTheme();
+ const StyleUtils = useStyleUtils();
return (
- {!!labelText && {labelText} }
+ {!!labelText && {labelText} }
{shouldShowCaret && (
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index 147e1686be5b..7e283557819a 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -110,7 +110,7 @@ function ReportListItem({
const participantFrom = reportItem.from;
const participantTo = reportItem.to;
- // These values should come as part of the item via SearchUtils.getSections() but ReportListItem is not yet 100% handled
+ // These values should come as part of the item via SearchUIUtils.getSections() but ReportListItem is not yet 100% handled
// This will be simplified in future once sorting of ReportListItem is done
const participantFromDisplayName = participantFrom?.displayName ?? participantFrom?.login ?? '';
const participantToDisplayName = participantTo?.displayName ?? participantTo?.login ?? '';
diff --git a/src/components/SelectionList/Search/UserInfoCell.tsx b/src/components/SelectionList/Search/UserInfoCell.tsx
index 3a6c98178a3b..6a653471683a 100644
--- a/src/components/SelectionList/Search/UserInfoCell.tsx
+++ b/src/components/SelectionList/Search/UserInfoCell.tsx
@@ -4,7 +4,7 @@ import Avatar from '@components/Avatar';
import Text from '@components/Text';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchUIUtils from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import type {SearchPersonalDetails} from '@src/types/onyx/SearchResults';
@@ -18,7 +18,7 @@ function UserInfoCell({participant, displayName}: UserInfoCellProps) {
const {isLargeScreenWidth} = useResponsiveLayout();
const avatarURL = participant?.avatar;
- if (!SearchUtils.isCorrectSearchUserName(displayName)) {
+ if (!SearchUIUtils.isCorrectSearchUserName(displayName)) {
return null;
}
diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx
index f54532a7f318..0c257a9bd824 100644
--- a/src/components/SelectionList/SearchTableHeader.tsx
+++ b/src/components/SelectionList/SearchTableHeader.tsx
@@ -5,7 +5,7 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchUIUtils from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type * as OnyxTypes from '@src/types/onyx';
@@ -39,12 +39,12 @@ const expenseHeaders: SearchColumnConfig[] = [
{
columnName: CONST.SEARCH.TABLE_COLUMNS.MERCHANT,
translationKey: 'common.merchant',
- shouldShow: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowMerchant(data),
+ shouldShow: (data: OnyxTypes.SearchResults['data']) => SearchUIUtils.getShouldShowMerchant(data),
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION,
translationKey: 'common.description',
- shouldShow: (data: OnyxTypes.SearchResults['data']) => !SearchUtils.getShouldShowMerchant(data),
+ shouldShow: (data: OnyxTypes.SearchResults['data']) => !SearchUIUtils.getShouldShowMerchant(data),
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.FROM,
@@ -105,6 +105,7 @@ type SearchTableHeaderProps = {
function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shouldShowYear, shouldShowSorting}: SearchTableHeaderProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout();
const {translate} = useLocalize();
const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth;
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 13a90d13d465..b2e175418813 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -17,6 +17,7 @@ import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
// eslint-disable-next-line no-restricted-imports
import type CursorStyles from '@styles/utils/cursor/types';
import type CONST from '@src/CONST';
+import type {Attendee} from '@src/types/onyx/IOU';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {SearchPersonalDetails, SearchReport, SearchReportAction, SearchTransaction} from '@src/types/onyx/SearchResults';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
@@ -234,6 +235,9 @@ type TransactionListItemType = ListItem &
/** Key used internally by React */
keyForList: string;
+
+ /** Attendees in the transaction */
+ attendees?: Attendee[];
};
type ReportActionListItemType = ListItem &
diff --git a/src/components/SelectionListWithModal/CustomListHeader.tsx b/src/components/SelectionListWithModal/CustomListHeader.tsx
new file mode 100644
index 000000000000..30ad32b33a59
--- /dev/null
+++ b/src/components/SelectionListWithModal/CustomListHeader.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import {View} from 'react-native';
+import Text from '@components/Text';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+
+type CustomListHeaderProps = {
+ canSelectMultiple: boolean | undefined;
+ leftHeaderText?: string | undefined;
+ rightHeaderText?: string | undefined;
+};
+
+function CustomListHeader({canSelectMultiple, leftHeaderText = '', rightHeaderText = ''}: CustomListHeaderProps) {
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+
+ const header = (
+
+ {leftHeaderText}
+
+ {rightHeaderText}
+
+
+ );
+
+ if (canSelectMultiple) {
+ return header;
+ }
+ return {header} ;
+}
+
+export default CustomListHeader;
diff --git a/src/components/SelectionListWithModal/index.tsx b/src/components/SelectionListWithModal/index.tsx
index 46d6494d1d21..25123d5454d4 100644
--- a/src/components/SelectionListWithModal/index.tsx
+++ b/src/components/SelectionListWithModal/index.tsx
@@ -28,6 +28,7 @@ function SelectionListWithModal(
const {translate} = useLocalize();
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here because there is a race condition that causes shouldUseNarrowLayout to change indefinitely in this component
// See https://github.com/Expensify/App/issues/48675 for more details
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const isFocused = useIsFocused();
diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx
index d1d0ff8ccb71..4c3c9adf8ef1 100644
--- a/src/components/SettlementButton/index.tsx
+++ b/src/components/SettlementButton/index.tsx
@@ -191,7 +191,7 @@ function SettlementButton({
if (iouPaymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || iouPaymentType === CONST.IOU.PAYMENT_TYPE.VBBA) {
if (!isUserValidated) {
- Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_ADD_BANK_ACCOUNT));
+ Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.route);
return;
}
triggerKYCFlow(event, iouPaymentType);
diff --git a/src/components/SwipeableView/index.native.tsx b/src/components/SwipeableView/index.native.tsx
index e5b6d371e606..4376585c6f0a 100644
--- a/src/components/SwipeableView/index.native.tsx
+++ b/src/components/SwipeableView/index.native.tsx
@@ -7,6 +7,7 @@ function SwipeableView({children, onSwipeDown}: SwipeableViewProps) {
const minimumPixelDistance = CONST.COMPOSER_MAX_HEIGHT;
const oldYRef = useRef(0);
const panResponder = useRef(
+ // eslint-disable-next-line react-compiler/react-compiler
PanResponder.create({
// The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance & swipe direction is downwards
onMoveShouldSetPanResponderCapture: (_event, gestureState) => {
@@ -22,10 +23,8 @@ function SwipeableView({children, onSwipeDown}: SwipeableViewProps) {
}),
).current;
- return (
- // eslint-disable-next-line react/jsx-props-no-spreading
- {children}
- );
+ // eslint-disable-next-line react/jsx-props-no-spreading, react-compiler/react-compiler
+ return {children} ;
}
SwipeableView.displayName = 'SwipeableView';
diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx
index 1ddc65bbd0fc..d2b3f2c3a4ac 100644
--- a/src/components/Switch.tsx
+++ b/src/components/Switch.tsx
@@ -69,6 +69,7 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, dis
hoverDimmingValue={1}
pressDimmingValue={0.8}
>
+ {/* eslint-disable-next-line react-compiler/react-compiler */}
{(!!disabled || !!showLockIcon) && (
void;
@@ -81,6 +85,7 @@ function ThumbnailImage({
fallbackIconColor,
fallbackIconBackground,
objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL,
+ isDeleted,
onLoadFailure,
onMeasure,
}: ThumbnailImageProps) {
@@ -141,6 +146,7 @@ function ThumbnailImage({
return (
+ {isDeleted && }
StyleUtils.getTooltipStyles({
+ // eslint-disable-next-line react-compiler/react-compiler
tooltip: rootWrapper.current,
currentSize: animation,
windowWidth,
diff --git a/src/components/Tooltip/BaseGenericTooltip/index.tsx b/src/components/Tooltip/BaseGenericTooltip/index.tsx
index 4477c991e3ac..28f2458699b7 100644
--- a/src/components/Tooltip/BaseGenericTooltip/index.tsx
+++ b/src/components/Tooltip/BaseGenericTooltip/index.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-compiler/react-compiler */
import React, {useLayoutEffect, useMemo, useRef, useState} from 'react';
import ReactDOM from 'react-dom';
import {Animated, View} from 'react-native';
diff --git a/src/components/Tooltip/GenericTooltip.tsx b/src/components/Tooltip/GenericTooltip.tsx
index a59819a77f6c..7309359b8e0c 100644
--- a/src/components/Tooltip/GenericTooltip.tsx
+++ b/src/components/Tooltip/GenericTooltip.tsx
@@ -157,6 +157,7 @@ function GenericTooltip({
// Skip the tooltip and return the children if the text is empty, we don't have a render function.
if (StringUtils.isEmptyString(text) && renderTooltipContent == null) {
+ // eslint-disable-next-line react-compiler/react-compiler
return children({isVisible, showTooltip, hideTooltip, updateTargetBounds});
}
@@ -164,6 +165,7 @@ function GenericTooltip({
<>
{isRendered && (
)}
-
+ {/* eslint-disable-next-line react-compiler/react-compiler */}
{children({isVisible, showTooltip, hideTooltip, updateTargetBounds})}
>
);
diff --git a/src/components/Tooltip/PopoverAnchorTooltip.tsx b/src/components/Tooltip/PopoverAnchorTooltip.tsx
index 5eb1f45dafcc..1af0f01cf957 100644
--- a/src/components/Tooltip/PopoverAnchorTooltip.tsx
+++ b/src/components/Tooltip/PopoverAnchorTooltip.tsx
@@ -9,7 +9,7 @@ function PopoverAnchorTooltip({shouldRender = true, children, ...props}: Tooltip
const tooltipRef = useRef(null);
const isPopoverRelatedToTooltipOpen = useMemo(() => {
- // eslint-disable-next-line @typescript-eslint/dot-notation
+ // eslint-disable-next-line @typescript-eslint/dot-notation, react-compiler/react-compiler
const tooltipNode = (tooltipRef.current?.['_childNode'] as Node | undefined) ?? null;
if (
diff --git a/src/components/ValidateAccountMessage.tsx b/src/components/ValidateAccountMessage.tsx
index d9810e859bfa..d27e2704af3c 100644
--- a/src/components/ValidateAccountMessage.tsx
+++ b/src/components/ValidateAccountMessage.tsx
@@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import * as Session from '@userActions/Session';
-import * as User from '@userActions/User';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import Icon from './Icon';
@@ -44,9 +43,6 @@ function ValidateAccountMessage({backTo}: ValidateAccountMessageProps) {
onPress={() => {
const loginName = loginNames?.at(0);
const login = loginList?.[loginName ?? ''] ?? {};
- if (!login?.validatedDate && !login?.validateCodeSent) {
- User.requestContactMethodValidateCode(loginName ?? '');
- }
Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(login?.partnerUserID ?? loginNames?.at(0) ?? '', backTo));
}}
diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
index 9207b9158051..02121ce26906 100644
--- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
+++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -232,7 +232,6 @@ function BaseValidateCodeForm({
onPress={validateAndSubmitForm}
style={[styles.mt4]}
success
- pressOnEnter
large
isLoading={account?.isLoading}
/>
diff --git a/src/components/VideoPlayer/IconButton.tsx b/src/components/VideoPlayer/IconButton.tsx
index e2b931bc256a..3066cc7620ef 100644
--- a/src/components/VideoPlayer/IconButton.tsx
+++ b/src/components/VideoPlayer/IconButton.tsx
@@ -4,6 +4,7 @@ import Icon from '@components/Icon';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Tooltip from '@components/Tooltip';
import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
type IconButtonProps = {
@@ -29,6 +30,7 @@ function IconButton({src, fill = 'white', onPress, style, hoverStyle, tooltipTex
onPress={onPress}
style={[styles.videoIconButton, style]}
hoverStyle={[styles.videoIconButtonHovered, hoverStyle]}
+ role={CONST.ROLE.BUTTON}
>
)}
-
- {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive, isDisabled}) => (
- DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
- onPressOut={() => ControlSelection.unblock()}
- onLongPress={(event) => {
- if (isDisabled) {
- return;
- }
- showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs));
- }}
- shouldUseHapticsOnLongPress
- >
-
-
-
-
- )}
-
+ {!isDeleted ? (
+
+ {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive, isDisabled}) => (
+ DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
+ onPressOut={() => ControlSelection.unblock()}
+ onLongPress={(event) => {
+ if (isDisabled) {
+ return;
+ }
+ showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs));
+ }}
+ shouldUseHapticsOnLongPress
+ >
+
+
+
+
+ )}
+
+ ) : (
+
+ )}
);
}
diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx
index 2ce65f08fc20..fb188e593949 100644
--- a/src/components/VideoPlayerPreview/index.tsx
+++ b/src/components/VideoPlayerPreview/index.tsx
@@ -38,9 +38,12 @@ type VideoPlayerPreviewProps = {
/** Callback executed when modal is pressed. */
onShowModalPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise;
+
+ /** Whether the video is deleted */
+ isDeleted?: boolean;
};
-function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDimensions, videoDuration, onShowModalPress}: VideoPlayerPreviewProps) {
+function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDimensions, videoDuration, onShowModalPress, isDeleted}: VideoPlayerPreviewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {currentlyPlayingURL, currentlyPlayingURLReportID, updateCurrentlyPlayingURL} = usePlaybackContext();
@@ -71,11 +74,12 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi
return (
- {shouldUseNarrowLayout || isThumbnail ? (
+ {shouldUseNarrowLayout || isThumbnail || isDeleted ? (
) : (
diff --git a/src/hooks/useCancellationType.ts b/src/hooks/useCancellationType.ts
index bc34f5feea6f..2a77bfd8ddc1 100644
--- a/src/hooks/useCancellationType.ts
+++ b/src/hooks/useCancellationType.ts
@@ -21,6 +21,7 @@ function useCancellationType(): CancellationType | undefined {
}
// There are no new items in the cancellation details NVP
+ // eslint-disable-next-line react-compiler/react-compiler
if (previousCancellationDetails.current?.length === cancellationDetails?.length) {
return;
}
diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts
index b5e3f333c44a..458949264ff0 100644
--- a/src/hooks/useDebounce.ts
+++ b/src/hooks/useDebounce.ts
@@ -42,5 +42,6 @@ export default function useDebounce(func: T, wait: nu
}
}, []);
+ // eslint-disable-next-line react-compiler/react-compiler
return debounceCallback as T;
}
diff --git a/src/hooks/useDebouncedState.ts b/src/hooks/useDebouncedState.ts
index 8d7d43cb6f9c..b004c308a375 100644
--- a/src/hooks/useDebouncedState.ts
+++ b/src/hooks/useDebouncedState.ts
@@ -20,6 +20,7 @@ import CONST from '@src/CONST';
function useDebouncedState(initialValue: T, delay: number = CONST.TIMING.USE_DEBOUNCED_STATE_DELAY): [T, T, (value: T) => void] {
const [value, setValue] = useState(initialValue);
const [debouncedValue, setDebouncedValue] = useState(initialValue);
+ // eslint-disable-next-line react-compiler/react-compiler
const debouncedSetDebouncedValue = useRef(debounce(setDebouncedValue, delay)).current;
useEffect(() => () => debouncedSetDebouncedValue.cancel(), [debouncedSetDebouncedValue]);
diff --git a/src/hooks/useDeepCompareRef.ts b/src/hooks/useDeepCompareRef.ts
index 7511c1516a1d..9a226da44767 100644
--- a/src/hooks/useDeepCompareRef.ts
+++ b/src/hooks/useDeepCompareRef.ts
@@ -17,8 +17,11 @@ import {useRef} from 'react';
*/
export default function useDeepCompareRef(value: T): T | undefined {
const ref = useRef();
+ // eslint-disable-next-line react-compiler/react-compiler
if (!isEqual(value, ref.current)) {
+ // eslint-disable-next-line react-compiler/react-compiler
ref.current = value;
}
+ // eslint-disable-next-line react-compiler/react-compiler
return ref.current;
}
diff --git a/src/hooks/useDeleteSavedSearch.tsx b/src/hooks/useDeleteSavedSearch.tsx
index 668f9048e7fb..19e5def4601d 100644
--- a/src/hooks/useDeleteSavedSearch.tsx
+++ b/src/hooks/useDeleteSavedSearch.tsx
@@ -1,7 +1,7 @@
import React, {useState} from 'react';
import ConfirmModal from '@components/ConfirmModal';
import Navigation from '@libs/Navigation/Navigation';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import * as SearchActions from '@userActions/Search';
import ROUTES from '@src/ROUTES';
import useLocalize from './useLocalize';
@@ -22,7 +22,7 @@ export default function useDeleteSavedSearch() {
SearchActions.clearAdvancedFilters();
Navigation.navigate(
ROUTES.SEARCH_CENTRAL_PANE.getRoute({
- query: SearchUtils.buildCannedSearchQuery(),
+ query: SearchQueryUtils.buildCannedSearchQuery(),
}),
);
};
diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts
index 950d0592b59c..69aaebc415a5 100644
--- a/src/hooks/useNetwork.ts
+++ b/src/hooks/useNetwork.ts
@@ -10,6 +10,7 @@ type UseNetwork = {isOffline: boolean};
export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = {}): UseNetwork {
const callback = useRef(onReconnect);
+ // eslint-disable-next-line react-compiler/react-compiler
callback.current = onReconnect;
const {isOffline, networkStatus} = useContext(NetworkContext) ?? {...CONST.DEFAULT_NETWORK_DATA, networkStatus: CONST.NETWORK.NETWORK_STATUS.UNKNOWN};
diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts
index 279e8e4a3bf4..e5db9bffd39c 100644
--- a/src/hooks/usePrevious.ts
+++ b/src/hooks/usePrevious.ts
@@ -8,5 +8,6 @@ export default function usePrevious(value: T): T {
useEffect(() => {
ref.current = value;
}, [value]);
+ // eslint-disable-next-line react-compiler/react-compiler
return ref.current;
}
diff --git a/src/hooks/useSingleExecution/index.native.ts b/src/hooks/useSingleExecution/index.native.ts
index 16a98152def1..736a79ab1810 100644
--- a/src/hooks/useSingleExecution/index.native.ts
+++ b/src/hooks/useSingleExecution/index.native.ts
@@ -10,6 +10,7 @@ export default function useSingleExecution() {
const [isExecuting, setIsExecuting] = useState(false);
const isExecutingRef = useRef();
+ // eslint-disable-next-line react-compiler/react-compiler
isExecutingRef.current = isExecuting;
const singleExecution = useCallback(
diff --git a/src/hooks/useSubStep/index.ts b/src/hooks/useSubStep/index.ts
index eb4a30037ab0..e59e18cf85b5 100644
--- a/src/hooks/useSubStep/index.ts
+++ b/src/hooks/useSubStep/index.ts
@@ -59,9 +59,11 @@ export default function useSubStep({bodyContent, on
setScreenIndex(bodyContent.length - 1);
}, [bodyContent]);
+ // eslint-disable-next-line react-compiler/react-compiler
return {
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
componentToRender: bodyContent.at(screenIndex) as ComponentType,
+ // eslint-disable-next-line react-compiler/react-compiler
isEditing: isEditing.current,
screenIndex,
prevScreen,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 8b9569dc1267..5daecbc98e5f 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -259,6 +259,7 @@ const translations = {
firstName: 'First name',
lastName: 'Last name',
addCardTermsOfService: 'Expensify Terms of Service',
+ perPerson: 'per person',
phone: 'Phone',
phoneNumber: 'Phone number',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
@@ -963,7 +964,7 @@ const translations = {
genericHoldExpenseFailureMessage: 'Unexpected error holding this expense. Please try again later.',
genericUnholdExpenseFailureMessage: 'Unexpected error taking this expense off hold. Please try again later.',
receiptDeleteFailureError: 'Unexpected error deleting this receipt. Please try again later.',
- receiptFailureMessage: "The receipt didn't upload.",
+ receiptFailureMessage: "The receipt didn't upload. ",
// eslint-disable-next-line rulesdir/use-periods-for-error-messages
saveFileMessage: 'Download the file ',
loseFileMessage: 'or dismiss this error and lose it.',
@@ -974,6 +975,7 @@ const translations = {
atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses.',
splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.',
invalidMerchant: 'Please enter a correct merchant.',
+ atLeastOneAttendee: 'At least one attendee must be selected',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`,
enableWallet: 'Enable wallet',
@@ -1029,6 +1031,7 @@ const translations = {
bookingPendingDescription: "This booking is pending because it hasn't been paid yet.",
bookingArchived: 'This booking is archived',
bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.',
+ attendees: 'Attendees',
paymentComplete: 'Payment complete',
justTrackIt: 'Just track it (don’t submit it)',
},
@@ -2428,12 +2431,9 @@ const translations = {
"We'll create an itemized vendor bill for each Expensify report and add it to the account below. If this period is closed, we'll post to the 1st of the next open period.",
deepDiveExpensifyCard: 'Expensify Card transactions will automatically export to an "Expensify Card Liability Account" created with',
deepDiveExpensifyCardIntegration: 'our integration.',
- outOfPocketLocationEnabledDescription:
- 'QuickBooks Desktop doesn’t support locations on vendor bills or checks. As you have locations enabled on your workspace, these export options are unavailable.',
outOfPocketTaxEnabledDescription:
"QuickBooks Desktop doesn't support taxes on journal entry exports. As you have taxes enabled on your workspace, this export option is unavailable.",
outOfPocketTaxEnabledError: 'Journal entries are unavailable when taxes are enabled. Please choose a different export option.',
- outOfPocketLocationEnabledError: 'Vendor bills are unavailable when locations are enabled. Please choose a different export option.',
accounts: {
[CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD]: 'Credit card',
[CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL]: 'Vendor bill',
@@ -2501,10 +2501,8 @@ const translations = {
customersDescription: 'Choose how to handle QuickBooks Online customers/projects in Expensify.',
locationsDescription: 'Choose how to handle QuickBooks Online locations in Expensify.',
taxesDescription: 'Choose how to handle QuickBooks Online taxes in Expensify.',
- locationsAdditionalDescription:
- 'QuickBooks Online doesn’t support locations on vendor bills or checks. As you have locations enabled on your workspace, these export options are unavailable.',
- outOfPocketLocationEnabledDescription:
- 'QuickBooks Online doesn’t support locations on vendor bills or checks. As you have locations enabled on your workspace, these export options are unavailable.',
+ locationsLineItemsRestrictionDescription:
+ "QuickBooks Online does not support Locations at the line-level for Checks or Vendor Bills. If you'd like to have locations at the line-level, make sure you are using Journal Entries and Credit/Debit Card expenses.",
taxesJournalEntrySwitchNote: "QuickBooks Online doesn't support taxes on journal entries. Please change your export option to vendor bill or check.",
exportDescription: 'Configure how Expensify data exports to QuickBooks Online.',
date: 'Export date',
@@ -2554,7 +2552,6 @@ const translations = {
outOfPocketTaxEnabledDescription:
"QuickBooks Online doesn't support taxes on journal entry exports. As you have taxes enabled on your workspace, this export option is unavailable.",
outOfPocketTaxEnabledError: 'Journal entries are unavailable when taxes are enabled. Please choose a different export option.',
- outOfPocketLocationEnabledError: 'Vendor bills are unavailable when locations are enabled. Please choose a different export option.',
advancedConfig: {
autoSyncDescription: 'Expensify will automatically sync with QuickBooks Online every day.',
inviteEmployees: 'Invite employees',
@@ -2609,8 +2606,9 @@ const translations = {
notImported: 'Not imported',
notConfigured: 'Not configured',
trackingCategoriesOptions: {
- default: 'Xero contact default',
- tag: 'Tags',
+ [CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS.DEFAULT]: 'Xero contact default',
+ [CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS.TAG]: 'Tags',
+ [CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS.REPORT_FIELD]: 'Report fields',
},
exportDescription: 'Configure how Expensify data exports to Xero.',
purchaseBill: 'Purchase bill',
@@ -3827,14 +3825,6 @@ const translations = {
notReadyDescription: 'Draft or pending expense reports cannot be exported to the accounting system. Please approve or pay these expenses before exporting them.',
},
invoices: {
- invoiceClientsAndCustomers: 'Invoice clients and customers',
- invoiceFirstSectionCopy: 'Send beautiful, professional invoices directly to your clients and customers right from the Expensify app.',
- viewAllInvoices: 'View all invoices',
- unlockOnlineInvoiceCollection: 'Unlock online invoice collection',
- unlockNoVBACopy: 'Connect your bank account to accept online invoice payments by ACH or credit card.',
- moneyBackInAFlash: 'Money back, in a flash!',
- unlockVBACopy: "You're all set to accept payments by ACH or credit card!",
- viewUnpaidInvoices: 'View unpaid invoices',
sendInvoice: 'Send invoice',
sendFrom: 'Send from',
invoicingDetails: 'Invoicing details',
@@ -3850,8 +3840,8 @@ const translations = {
payingAsBusiness: 'Paying as a business',
},
invoiceBalance: 'Invoice balance',
- invoiceBalanceSubtitle: 'Here’s your current balance from collecting payments on invoices.',
- bankAccountsSubtitle: 'Add a bank account to receive invoice payments.',
+ invoiceBalanceSubtitle: "This is your current balance from collecting invoice payments. It'll transfer to your bank account automatically if you've added one.",
+ bankAccountsSubtitle: 'Add a bank account to make and receive invoice payments.',
},
invite: {
member: 'Invite member',
@@ -4057,7 +4047,7 @@ const translations = {
upgradeToUnlock: 'Unlock this feature',
completed: {
headline: `You've upgraded your workspace!`,
- successMessage: ({policyName}: ReportPolicyNameParams) => `You've successfully upgraded your ${policyName} workspace to the Control plan!`,
+ successMessage: ({policyName}: ReportPolicyNameParams) => `You've successfully upgraded ${policyName} to the Control plan!`,
viewSubscription: 'View your subscription',
moreDetails: 'for more details.',
gotIt: 'Got it, thanks',
@@ -4303,7 +4293,7 @@ const translations = {
searchResults: {
emptyResults: {
title: 'Nothing to show',
- subtitle: 'Try creating something using the green + button.',
+ subtitle: 'Try creating something with the green + button.',
},
emptyExpenseResults: {
title: "You haven't created any expenses yet",
diff --git a/src/languages/es.ts b/src/languages/es.ts
index b7f66ef2bec0..c38e9052bd60 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -243,6 +243,7 @@ const translations = {
not: 'No',
privacyPolicy: 'la Política de Privacidad de Expensify',
addCardTermsOfService: 'Términos de Servicio',
+ perPerson: 'por persona',
signIn: 'Conectarse',
signInWithGoogle: 'Iniciar sesión con Google',
signInWithApple: 'Iniciar sesión con Apple',
@@ -960,7 +961,7 @@ const translations = {
genericCreateFailureMessage: 'Error inesperado al enviar este gasto. Por favor, inténtalo más tarde.',
genericCreateInvoiceFailureMessage: 'Error inesperado al enviar la factura. Por favor, inténtalo de nuevo más tarde.',
receiptDeleteFailureError: 'Error inesperado al borrar este recibo. Por favor, vuelve a intentarlo más tarde.',
- receiptFailureMessage: 'El recibo no se subió.',
+ receiptFailureMessage: 'El recibo no se subió. ',
// eslint-disable-next-line rulesdir/use-periods-for-error-messages
saveFileMessage: 'Guarda el archivo ',
loseFileMessage: 'o descarta este error y piérdelo.',
@@ -971,6 +972,7 @@ const translations = {
atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes.',
splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con miembros individuales. Por favor, actualiza tu selección.',
invalidMerchant: 'Por favor, introduce un comerciante correcto.',
+ atLeastOneAttendee: 'Debe seleccionarse al menos un asistente',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su billetera`,
enableWallet: 'Habilitar billetera',
@@ -1026,6 +1028,7 @@ const translations = {
bookingPendingDescription: 'Esta reserva está pendiente porque aún no se ha pagado.',
bookingArchived: 'Esta reserva está archivada',
bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.',
+ attendees: 'Asistentes',
paymentComplete: 'Pago completo',
justTrackIt: 'Solo guardarlo (no enviarlo)',
},
@@ -2451,12 +2454,9 @@ const translations = {
'Crearemos una factura de proveedor desglosada para cada informe de Expensify y la añadiremos a la cuenta a continuación. Si este periodo está cerrado, lo contabilizaremos el 1º del siguiente periodo abierto.',
deepDiveExpensifyCard: 'Las transacciones de la Tarjeta Expensify se exportarán automáticamente a una "Cuenta de Responsabilidad de la Tarjeta Expensify" creada con',
deepDiveExpensifyCardIntegration: 'nuestra integración.',
- outOfPocketLocationEnabledDescription:
- 'QuickBooks Desktop no permite lugares en facturas de proveedores o cheques. Como tienes activadas los lugares en tu espacio de trabajo, estas opciones de exportación no están disponibles.',
outOfPocketTaxEnabledDescription:
'QuickBooks Desktop no admite impuestos en las exportaciones de asientos contables. Como tienes impuestos habilitados en tu espacio de trabajo, esta opción de exportación no está disponible.',
outOfPocketTaxEnabledError: 'Los asientos contables no están disponibles cuando los impuestos están habilitados. Por favor, selecciona otra opción de exportación.',
- outOfPocketLocationEnabledError: 'Las facturas de proveedores no están disponibles cuando las ubicaciones están habilitadas. Por favor, selecciona otra opción de exportación.',
accounts: {
[CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD]: 'Tarjeta de crédito',
[CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL]: 'Factura del proveedor',
@@ -2525,10 +2525,10 @@ const translations = {
classesDescription: 'Elige cómo gestionar las clases de QuickBooks Online en Expensify.',
customersDescription: 'Elige cómo gestionar los clientes/proyectos de QuickBooks Online en Expensify.',
locationsDescription: 'Elige cómo gestionar los lugares de QuickBooks Online en Expensify.',
+ locationsLineItemsRestrictionDescription:
+ 'QuickBooks Online no admite Ubicaciones a nivel de línea para cheques o facturas de proveedores. Si deseas tener ubicaciones a nivel de línea, asegúrate de estar usando asientos contables y gastos con tarjetas de crédito/débito.',
taxesDescription: 'Elige cómo gestionar los impuestos de QuickBooks Online en Expensify.',
taxesJournalEntrySwitchNote: 'QuickBooks Online no permite impuestos en los asientos contables. Por favor, cambia la opción de exportación a factura de proveedor o cheque.',
- locationsAdditionalDescription:
- 'QuickBooks Online no permite lugares en facturas de proveedores o cheques. Como tienes activadas los lugares en tu espacio de trabajo, estas opciones de exportación no están disponibles.',
exportInvoices: 'Exportar facturas a',
exportDescription: 'Configura cómo se exportan los datos de Expensify a QuickBooks Online.',
date: 'Fecha de exportación',
@@ -2577,10 +2577,6 @@ const translations = {
outOfPocketTaxEnabledDescription:
'QuickBooks Online no permite impuestos en las exportaciones de entradas a los asientos contables. Como tienes los impuestos activados en tu espacio de trabajo, esta opción de exportación no está disponible.',
outOfPocketTaxEnabledError: 'La anotacion en el diario no está disponible cuando los impuestos están activados. Por favor, selecciona otra opción de exportación diferente.',
- outOfPocketLocationEnabledError:
- 'Las facturas de proveedores no están disponibles cuando las ubicaciones están activadas. Por favor, selecciona otra opción de exportación diferente.',
- outOfPocketLocationEnabledDescription:
- 'QuickBooks Online no permite lugares en facturas de proveedores o cheques. Como tienes activadas los lugares en tu espacio de trabajo, estas opciones de exportación no están disponibles.',
advancedConfig: {
autoSyncDescription: 'Expensify se sincronizará automáticamente con QuickBooks Online todos los días.',
@@ -2641,8 +2637,9 @@ const translations = {
notImported: 'No importado',
notConfigured: 'No configurado',
trackingCategoriesOptions: {
- default: 'Contacto de Xero por defecto',
- tag: 'Etiquetas',
+ [CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS.DEFAULT]: 'Contacto de Xero por defecto',
+ [CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS.TAG]: 'Etiquetas',
+ [CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS.REPORT_FIELD]: 'Campos de informes',
},
exportDescription: 'Configura cómo se exportan los datos de Expensify a Xero.',
purchaseBill: 'Factura de compra',
@@ -3872,14 +3869,6 @@ const translations = {
'Los borradores o informes de gastos pendientes no se pueden exportar al sistema contabilidad. Por favor, apruebe o pague estos gastos antes de exportarlos.',
},
invoices: {
- invoiceClientsAndCustomers: 'Emite facturas a tus clientes',
- invoiceFirstSectionCopy: 'Envía facturas detalladas y profesionales directamente a tus clientes desde la app de Expensify.',
- viewAllInvoices: 'Ver facturas emitidas',
- unlockOnlineInvoiceCollection: 'Desbloquea el cobro de facturas online',
- unlockNoVBACopy: 'Conecta tu cuenta bancaria para recibir pagos de facturas online por transferencia o con tarjeta.',
- moneyBackInAFlash: '¡Tu dinero de vuelta en un momento!',
- unlockVBACopy: '¡Todo listo para recibir pagos por transferencia o con tarjeta!',
- viewUnpaidInvoices: 'Ver facturas emitidas pendientes',
sendInvoice: 'Enviar factura',
sendFrom: 'Enviar desde',
invoicingDetails: 'Detalles de facturación',
@@ -3895,8 +3884,8 @@ const translations = {
payingAsBusiness: 'Pagar como una empresa',
},
invoiceBalance: 'Saldo de la factura',
- invoiceBalanceSubtitle: 'Aquí está su saldo actual de la recaudación de pagos en las facturas.',
- bankAccountsSubtitle: 'Agrega una cuenta bancaria para recibir pagos de facturas.',
+ invoiceBalanceSubtitle: 'Este es tu saldo actual de la recaudación de pagos de facturas. Se transferirá automáticamente a tu cuenta bancaria si has agregado una.',
+ bankAccountsSubtitle: 'Agrega una cuenta bancaria para hacer y recibir pagos de facturas.',
},
invite: {
member: 'Invitar miembros',
@@ -4104,7 +4093,7 @@ const translations = {
upgradeToUnlock: 'Desbloquear esta función',
completed: {
headline: 'Has mejorado tu espacio de trabajo.',
- successMessage: ({policyName}: ReportPolicyNameParams) => `Ha mejorado correctamente su espacio de trabajo ${policyName} al plan Control.`,
+ successMessage: ({policyName}: ReportPolicyNameParams) => `Has actualizado con éxito ${policyName} al plan Control.`,
viewSubscription: 'Ver su suscripción',
moreDetails: 'para obtener más información.',
gotIt: 'Entendido, gracias.',
@@ -4351,7 +4340,7 @@ const translations = {
searchResults: {
emptyResults: {
title: 'No hay nada que ver aquí',
- subtitle: 'Por favor intenta crear algo usando el botón verde.',
+ subtitle: 'Por favor intenta crear algo con el botón verde.',
},
emptyExpenseResults: {
title: 'Aún no has creado ningún gasto',
diff --git a/src/libs/API/parameters/RequestMoneyParams.ts b/src/libs/API/parameters/RequestMoneyParams.ts
index 27e1032d82a9..e3e600a4e367 100644
--- a/src/libs/API/parameters/RequestMoneyParams.ts
+++ b/src/libs/API/parameters/RequestMoneyParams.ts
@@ -28,7 +28,6 @@ type RequestMoneyParams = {
transactionThreadReportID: string;
createdReportActionIDForThread: string;
reimbursible?: boolean;
- policyID?: string;
};
export default RequestMoneyParams;
diff --git a/src/libs/API/parameters/SetMissingPersonalDetailsAndShipExpensifyCardParams.ts b/src/libs/API/parameters/SetPersonalDetailsAndShipExpensifyCardsParams.ts
similarity index 63%
rename from src/libs/API/parameters/SetMissingPersonalDetailsAndShipExpensifyCardParams.ts
rename to src/libs/API/parameters/SetPersonalDetailsAndShipExpensifyCardsParams.ts
index 54d73aec5df7..0ab82ba6b755 100644
--- a/src/libs/API/parameters/SetMissingPersonalDetailsAndShipExpensifyCardParams.ts
+++ b/src/libs/API/parameters/SetPersonalDetailsAndShipExpensifyCardsParams.ts
@@ -1,4 +1,4 @@
-type SetMissingPersonalDetailsAndShipExpensifyCardParams = {
+type SetPersonalDetailsAndShipExpensifyCardsParams = {
legalFirstName: string;
legalLastName: string;
phoneNumber: string;
@@ -9,7 +9,6 @@ type SetMissingPersonalDetailsAndShipExpensifyCardParams = {
addressCountry: string;
addressState: string;
dob: string;
- cardID: number;
};
-export default SetMissingPersonalDetailsAndShipExpensifyCardParams;
+export default SetPersonalDetailsAndShipExpensifyCardsParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 32a1e01ff3da..9f07049736ed 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -335,8 +335,8 @@ export type {default as UnassignCompanyCard} from './UnassignCompanyCard';
export type {default as UpdateCompanyCard} from './UpdateCompanyCard';
export type {default as UpdateCompanyCardNameParams} from './UpdateCompanyCardNameParams';
export type {default as SetCompanyCardExportAccountParams} from './SetCompanyCardExportAccountParams';
+export type {default as SetPersonalDetailsAndShipExpensifyCardsParams} from './SetPersonalDetailsAndShipExpensifyCardsParams';
export type {default as RequestFeedSetupParams} from './RequestFeedSetupParams';
-export type {default as SetMissingPersonalDetailsAndShipExpensifyCardParams} from './SetMissingPersonalDetailsAndShipExpensifyCardParams';
export type {default as SetInvoicingTransferBankAccountParams} from './SetInvoicingTransferBankAccountParams';
export type {default as ConnectPolicyToQuickBooksDesktopParams} from './ConnectPolicyToQuickBooksDesktopParams';
export type {default as UpdateQuickbooksDesktopExpensesExportDestinationTypeParams} from './UpdateQuickbooksDesktopExpensesExportDestinationTypeParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 929e709559b7..063be53a2eda 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -175,6 +175,7 @@ const WRITE_COMMANDS = {
DELETE_REPORT_FIELD: 'RemoveReportField',
SET_REPORT_NAME: 'RenameReport',
COMPLETE_SPLIT_BILL: 'CompleteSplitBill',
+ UPDATE_MONEY_REQUEST_ATTENDEES: 'UpdateMoneyRequestAttendees',
UPDATE_MONEY_REQUEST_DATE: 'UpdateMoneyRequestDate',
UPDATE_MONEY_REQUEST_BILLABLE: 'UpdateMoneyRequestBillable',
UPDATE_MONEY_REQUEST_MERCHANT: 'UpdateMoneyRequestMerchant',
@@ -427,7 +428,7 @@ const WRITE_COMMANDS = {
UPDATE_COMPANY_CARD: 'SyncCard',
UPDATE_COMPANY_CARD_NAME: 'SetCardName',
SET_CARD_EXPORT_ACCOUNT: 'SetCardExportAccount',
- SET_MISSING_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARD: 'SetMissingPersonalDetailsAndShipExpensifyCard',
+ SET_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARDS: 'SetPersonalDetailsAndShipExpensifyCards',
SET_INVOICING_TRANSFER_BANK_ACCOUNT: 'SetInvoicingTransferBankAccount',
} as const;
@@ -592,6 +593,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams;
[WRITE_COMMANDS.DELETE_REPORT_FIELD]: Parameters.DeleteReportFieldParams;
[WRITE_COMMANDS.COMPLETE_SPLIT_BILL]: Parameters.CompleteSplitBillParams;
+ [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_BILLABLE]: Parameters.UpdateMoneyRequestParams;
@@ -848,7 +850,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.DELETE_SAVED_SEARCH]: Parameters.DeleteSavedSearchParams;
[WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY]: Parameters.UpdateCardSettlementFrequencyParams;
[WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_ACCOUNT]: Parameters.UpdateCardSettlementAccountParams;
- [WRITE_COMMANDS.SET_MISSING_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARD]: Parameters.SetMissingPersonalDetailsAndShipExpensifyCardParams;
+ [WRITE_COMMANDS.SET_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARDS]: Parameters.SetPersonalDetailsAndShipExpensifyCardsParams;
// Xero API
[WRITE_COMMANDS.UPDATE_XERO_TENANT_ID]: Parameters.UpdateXeroGenericTypeParams;
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index c52827cec964..286f952b3484 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -40,7 +40,7 @@ function getMileageRates(policy: OnyxInputOrEntry, includeDisabledRates
return mileageRates;
}
- const distanceUnit = PolicyUtils.getCustomUnit(policy);
+ const distanceUnit = PolicyUtils.getDistanceRateCustomUnit(policy);
if (!distanceUnit?.rates) {
return mileageRates;
}
@@ -78,7 +78,7 @@ function getDefaultMileageRate(policy: OnyxInputOrEntry): MileageRate |
return undefined;
}
- const distanceUnit = PolicyUtils.getCustomUnit(policy);
+ const distanceUnit = PolicyUtils.getDistanceRateCustomUnit(policy);
if (!distanceUnit?.rates) {
return;
}
@@ -302,8 +302,8 @@ function getCustomUnitRateID(reportID: string, shouldUseDefault?: boolean) {
* Get taxable amount from a specific distance rate, taking into consideration the tax claimable amount configured for the distance rate
*/
function getTaxableAmount(policy: OnyxEntry, customUnitRateID: string, distance: number) {
- const distanceUnit = PolicyUtils.getCustomUnit(policy);
- const customUnitRate = PolicyUtils.getCustomUnitRate(policy, customUnitRateID);
+ const distanceUnit = PolicyUtils.getDistanceRateCustomUnit(policy);
+ const customUnitRate = PolicyUtils.getDistanceRateCustomUnitRate(policy, customUnitRateID);
if (!distanceUnit || !distanceUnit?.customUnitID || !customUnitRate) {
return 0;
}
diff --git a/src/libs/E2E/tests/appStartTimeTest.e2e.ts b/src/libs/E2E/tests/appStartTimeTest.e2e.ts
index 188dd65c85e9..ccd781e08514 100644
--- a/src/libs/E2E/tests/appStartTimeTest.e2e.ts
+++ b/src/libs/E2E/tests/appStartTimeTest.e2e.ts
@@ -1,11 +1,14 @@
import Config from 'react-native-config';
+import type {NativeConfig} from 'react-native-config';
import type {PerformanceEntry} from 'react-native-performance';
import E2ELogin from '@libs/E2E/actions/e2eLogin';
import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import E2EClient from '@libs/E2E/client';
+import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow';
import Performance from '@libs/Performance';
-const test = () => {
+const test = (config: NativeConfig) => {
+ const name = getConfigValueOrThrow('name', config);
// check for login (if already logged in the action will simply resolve)
E2ELogin().then((neededLogin) => {
if (neededLogin) {
@@ -25,7 +28,7 @@ const test = () => {
metrics.map((metric) =>
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: `App start ${metric.name}`,
+ name: `${name} ${metric.name}`,
metric: metric.duration,
unit: 'ms',
}),
diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts
index 8e2a0a81da7d..cf0c4889aa69 100644
--- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts
+++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts
@@ -15,6 +15,7 @@ const test = (config: NativeConfig) => {
console.debug('[E2E] Logging in for chat opening');
const reportID = getConfigValueOrThrow('reportID', config);
+ const name = getConfigValueOrThrow('name', config);
E2ELogin().then((neededLogin) => {
if (neededLogin) {
@@ -48,7 +49,7 @@ const test = (config: NativeConfig) => {
if (entry.name === CONST.TIMING.CHAT_RENDER) {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: 'Chat opening',
+ name: `${name} Chat opening`,
metric: entry.duration,
unit: 'ms',
})
@@ -64,7 +65,7 @@ const test = (config: NativeConfig) => {
if (entry.name === CONST.TIMING.OPEN_REPORT) {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: 'Chat TTI',
+ name: `${name} Chat TTI`,
metric: entry.duration,
unit: 'ms',
})
diff --git a/src/libs/E2E/tests/linkingTest.e2e.ts b/src/libs/E2E/tests/linkingTest.e2e.ts
index c4d580e8c57b..18ba438c2ca6 100644
--- a/src/libs/E2E/tests/linkingTest.e2e.ts
+++ b/src/libs/E2E/tests/linkingTest.e2e.ts
@@ -24,6 +24,7 @@ const test = (config: NativeConfig) => {
const reportID = getConfigValueOrThrow('reportID', config);
const linkedReportID = getConfigValueOrThrow('linkedReportID', config);
const linkedReportActionID = getConfigValueOrThrow('linkedReportActionID', config);
+ const name = getConfigValueOrThrow('name', config);
E2ELogin().then((neededLogin) => {
if (neededLogin) {
@@ -74,7 +75,7 @@ const test = (config: NativeConfig) => {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: 'Comment linking',
+ name,
metric: entry.duration,
unit: 'ms',
});
diff --git a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts
index 48278aee536a..de9464c9c286 100644
--- a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts
+++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts
@@ -1,16 +1,20 @@
import Config from 'react-native-config';
+import type {NativeConfig} from 'react-native-config';
import * as E2EGenericPressableWrapper from '@components/Pressable/GenericPressable/index.e2e';
import E2ELogin from '@libs/E2E/actions/e2eLogin';
import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import E2EClient from '@libs/E2E/client';
+import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow';
import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve';
import Performance from '@libs/Performance';
import CONST from '@src/CONST';
-const test = () => {
+const test = (config: NativeConfig) => {
// check for login (if already logged in the action will simply resolve)
console.debug('[E2E] Logging in for new search router');
+ const name = getConfigValueOrThrow('name', config);
+
E2ELogin().then((neededLogin: boolean): Promise | undefined => {
if (neededLogin) {
return waitForAppLoaded().then(() =>
@@ -39,7 +43,7 @@ const test = () => {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
error: 'Search button not found',
- name: 'Open Search Router TTI',
+ name: `${name} Open Search Router TTI`,
}).then(() => E2EClient.submitTestDone());
return;
}
@@ -48,7 +52,7 @@ const test = () => {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
error: 'Search button found but onPress prop was not present',
- name: 'Open Search Router TTI',
+ name: `${name} Open Search Router TTI`,
}).then(() => E2EClient.submitTestDone());
return;
}
@@ -59,7 +63,7 @@ const test = () => {
if (entry.name === CONST.TIMING.SEARCH_ROUTER_RENDER) {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: 'Open Search Router TTI',
+ name: `${name} Open Search Router TTI`,
metric: entry.duration,
unit: 'ms',
})
@@ -75,7 +79,7 @@ const test = () => {
if (entry.name === CONST.TIMING.LOAD_SEARCH_OPTIONS) {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: 'Load Search Options',
+ name: `${name} Load Search Options`,
metric: entry.duration,
unit: 'ms',
})
diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts
index efe1c380dfd0..e042a688c37d 100644
--- a/src/libs/E2E/tests/reportTypingTest.e2e.ts
+++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts
@@ -21,6 +21,7 @@ const test = (config: NativeConfig) => {
const reportID = getConfigValueOrThrow('reportID', config);
const message = getConfigValueOrThrow('message', config);
+ const name = getConfigValueOrThrow('name', config);
E2ELogin().then((neededLogin) => {
if (neededLogin) {
@@ -45,7 +46,7 @@ const test = (config: NativeConfig) => {
if (entry.name === CONST.TIMING.MESSAGE_SENT) {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: 'Message sent',
+ name: `${name} Message sent`,
metric: entry.duration,
unit: 'ms',
}).then(messageSentResolve);
@@ -77,7 +78,7 @@ const test = (config: NativeConfig) => {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
- name: 'Composer typing rerender count',
+ name: `${name} Composer typing rerender count`,
metric: rerenderCount,
unit: 'renders',
})
diff --git a/src/libs/FastSearch.ts b/src/libs/FastSearch.ts
new file mode 100644
index 000000000000..59d28dedd449
--- /dev/null
+++ b/src/libs/FastSearch.ts
@@ -0,0 +1,140 @@
+/* eslint-disable rulesdir/prefer-at */
+import CONST from '@src/CONST';
+import Timing from './actions/Timing';
+import SuffixUkkonenTree from './SuffixUkkonenTree';
+
+type SearchableData = {
+ /**
+ * The data that should be searchable
+ */
+ data: T[];
+ /**
+ * A function that generates a string from a data entry. The string's value is used for searching.
+ * If you have multiple fields that should be searchable, simply concat them to the string and return it.
+ */
+ toSearchableString: (data: T) => string;
+};
+
+// There are certain characters appear very often in our search data (email addresses), which we don't need to search for.
+const charSetToSkip = new Set(['@', '.', '#', '$', '%', '&', '*', '+', '-', '/', ':', ';', '<', '=', '>', '?', '_', '~', '!', ' ']);
+
+/**
+ * Creates a new "FastSearch" instance. "FastSearch" uses a suffix tree to search for substrings in a list of strings.
+ * You can provide multiple datasets. The search results will be returned for each dataset.
+ *
+ * Note: Creating a FastSearch instance with a lot of data is computationally expensive. You should create an instance once and reuse it.
+ * Searches will be very fast though, even with a lot of data.
+ */
+function createFastSearch(dataSets: Array>) {
+ Timing.start(CONST.TIMING.SEARCH_CONVERT_SEARCH_VALUES);
+ const maxNumericListSize = 400_000;
+ // The user might provide multiple data sets, but internally, the search values will be stored in this one list:
+ let concatenatedNumericList = new Uint8Array(maxNumericListSize);
+ // Here we store the index of the data item in the original data list, so we can map the found occurrences back to the original data:
+ const occurrenceToIndex = new Uint32Array(maxNumericListSize * 4);
+ // As we are working with ArrayBuffers, we need to keep track of the current offset:
+ const offset = {value: 1};
+ // We store the last offset for a dataSet, so we can map the found occurrences to the correct dataSet:
+ const listOffsets: number[] = [];
+
+ for (const {data, toSearchableString} of dataSets) {
+ // Performance critical: the array parameters are passed by reference, so we don't have to create new arrays every time:
+ dataToNumericRepresentation(concatenatedNumericList, occurrenceToIndex, offset, {data, toSearchableString});
+ listOffsets.push(offset.value);
+ }
+ concatenatedNumericList[offset.value++] = SuffixUkkonenTree.END_CHAR_CODE;
+ listOffsets[listOffsets.length - 1] = offset.value;
+ Timing.end(CONST.TIMING.SEARCH_CONVERT_SEARCH_VALUES);
+
+ // The list might be larger than necessary, so we clamp it to the actual size:
+ concatenatedNumericList = concatenatedNumericList.slice(0, offset.value);
+
+ // Create & build the suffix tree:
+ Timing.start(CONST.TIMING.SEARCH_MAKE_TREE);
+ const tree = SuffixUkkonenTree.makeTree(concatenatedNumericList);
+ Timing.end(CONST.TIMING.SEARCH_MAKE_TREE);
+
+ Timing.start(CONST.TIMING.SEARCH_BUILD_TREE);
+ tree.build();
+ Timing.end(CONST.TIMING.SEARCH_BUILD_TREE);
+
+ /**
+ * Searches for the given input and returns results for each dataset.
+ */
+ function search(searchInput: string): T[][] {
+ const cleanedSearchString = cleanString(searchInput);
+ const {numeric} = SuffixUkkonenTree.stringToNumeric(cleanedSearchString, {
+ charSetToSkip,
+ // stringToNumeric might return a list that is larger than necessary, so we clamp it to the actual size
+ // (otherwise the search could fail as we include in our search empty array values):
+ clamp: true,
+ });
+ const result = tree.findSubstring(Array.from(numeric));
+
+ const resultsByDataSet = Array.from({length: dataSets.length}, () => new Set());
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
+ for (let i = 0; i < result.length; i++) {
+ const occurrenceIndex = result[i];
+ const itemIndexInDataSet = occurrenceToIndex[occurrenceIndex];
+ const dataSetIndex = listOffsets.findIndex((listOffset) => occurrenceIndex < listOffset);
+
+ if (dataSetIndex === -1) {
+ throw new Error(`[FastSearch] The occurrence index ${occurrenceIndex} is not in any dataset`);
+ }
+ const item = dataSets[dataSetIndex].data[itemIndexInDataSet];
+ if (!item) {
+ throw new Error(`[FastSearch] The item with index ${itemIndexInDataSet} in dataset ${dataSetIndex} is not defined`);
+ }
+ resultsByDataSet[dataSetIndex].add(item);
+ }
+
+ return resultsByDataSet.map((set) => Array.from(set));
+ }
+
+ return {
+ search,
+ };
+}
+
+/**
+ * The suffix tree can only store string like values, and internally stores those as numbers.
+ * This function converts the user data (which are most likely objects) to a numeric representation.
+ * Additionally a list of the original data and their index position in the numeric list is created, which is used to map the found occurrences back to the original data.
+ */
+function dataToNumericRepresentation(concatenatedNumericList: Uint8Array, occurrenceToIndex: Uint32Array, offset: {value: number}, {data, toSearchableString}: SearchableData): void {
+ data.forEach((option, index) => {
+ const searchStringForTree = toSearchableString(option);
+ const cleanedSearchStringForTree = cleanString(searchStringForTree);
+
+ if (cleanedSearchStringForTree.length === 0) {
+ return;
+ }
+
+ SuffixUkkonenTree.stringToNumeric(cleanedSearchStringForTree, {
+ charSetToSkip,
+ out: {
+ outArray: concatenatedNumericList,
+ offset,
+ outOccurrenceToIndex: occurrenceToIndex,
+ index,
+ },
+ });
+ // eslint-disable-next-line no-param-reassign
+ occurrenceToIndex[offset.value] = index;
+ // eslint-disable-next-line no-param-reassign
+ concatenatedNumericList[offset.value++] = SuffixUkkonenTree.DELIMITER_CHAR_CODE;
+ });
+}
+
+/**
+ * Everything in the tree is treated as lowercase.
+ */
+function cleanString(input: string) {
+ return input.toLowerCase();
+}
+
+const FastSearch = {
+ createFastSearch,
+};
+
+export default FastSearch;
diff --git a/src/libs/Firebase/utils.ts b/src/libs/Firebase/utils.ts
index 0235953bfcd3..4f7718e691ed 100644
--- a/src/libs/Firebase/utils.ts
+++ b/src/libs/Firebase/utils.ts
@@ -26,8 +26,8 @@ function getAttributes(): FirebaseAttributes {
transactionViolationsLength,
policiesLength,
transactionsLength,
- policyType: policy?.type ?? '',
- policyRole: policy?.role ?? '',
+ policyType: policy?.type ?? 'N/A',
+ policyRole: policy?.role ?? 'N/A',
};
}
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index dcfdd4bbc73a..e0fd37db5b3b 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -3,7 +3,8 @@ import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {OnyxInputOrEntry, Report, Transaction} from '@src/types/onyx';
+import type {OnyxInputOrEntry, PersonalDetails, Report, Transaction} from '@src/types/onyx';
+import type {Attendee} from '@src/types/onyx/IOU';
import type {IOURequestType} from './actions/IOU';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
@@ -162,6 +163,24 @@ function shouldUseTransactionDraft(action: IOUAction | undefined) {
return action === CONST.IOU.ACTION.CREATE || isMovingTransactionFromTrackExpense(action);
}
+function formatCurrentUserToAttendee(currentUser?: PersonalDetails, reportID?: string) {
+ if (!currentUser) {
+ return;
+ }
+ const initialAttendee: Attendee = {
+ email: currentUser?.login,
+ login: currentUser?.login,
+ displayName: currentUser.displayName,
+ avatarUrl: currentUser.avatar?.toString(),
+ accountID: currentUser.accountID,
+ text: currentUser.login,
+ selected: true,
+ reportID,
+ };
+
+ return [initialAttendee];
+}
+
function shouldStartLocationPermissionFlow() {
return (
!lastLocationPermissionPrompt ||
@@ -179,5 +198,6 @@ export {
isValidMoneyRequestType,
navigateToStartMoneyRequestStep,
updateIOUOwnerAndTotal,
+ formatCurrentUserToAttendee,
shouldStartLocationPermissionFlow,
};
diff --git a/src/libs/Log.ts b/src/libs/Log.ts
index 72673b8d3f79..2ccbd1d37882 100644
--- a/src/libs/Log.ts
+++ b/src/libs/Log.ts
@@ -3,6 +3,7 @@
/* eslint-disable rulesdir/no-api-in-views */
import {Logger} from 'expensify-common';
+import AppLogs from 'react-native-app-logs';
import Onyx from 'react-native-onyx';
import type {Merge} from 'type-fest';
import CONST from '@src/CONST';
@@ -82,4 +83,21 @@ const Log = new Logger({
});
timeout = setTimeout(() => Log.info('Flushing logs older than 10 minutes', true, {}, true), 10 * 60 * 1000);
+AppLogs.configure({appGroupName: 'group.com.expensify.new', interval: -1});
+AppLogs.registerHandler({
+ filter: '[NotificationService]',
+ handler: ({filter, logs}) => {
+ logs.forEach((log) => {
+ // Both native and JS logs are captured by the filter so we replace the filter before logging to avoid an infinite loop
+ const message = `[PushNotification] ${log.message.replace(filter, 'NotificationService -')}`;
+
+ if (log.level === 'error') {
+ Log.hmmm(message);
+ } else {
+ Log.info(message);
+ }
+ });
+ },
+});
+
export default Log;
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 20c5f6d7dce9..f5109cbea74b 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -302,6 +302,19 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
);
}
+ const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'attendees' in reportActionOriginalMessage;
+ if (hasModifiedAttendees) {
+ buildMessageFragmentForValue(
+ reportActionOriginalMessage.oldAttendees ?? '',
+ reportActionOriginalMessage.attendees ?? '',
+ Localize.translateLocal('iou.attendees'),
+ false,
+ setFragments,
+ removalFragments,
+ changeFragments,
+ );
+ }
+
const message =
getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) +
getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) +
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 7b8589c81e7f..85c027f06d95 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -29,7 +29,7 @@ import onyxSubscribe from '@libs/onyxSubscribe';
import * as Pusher from '@libs/Pusher/pusher';
import PusherConnectionManager from '@libs/PusherConnectionManager';
import * as ReportUtils from '@libs/ReportUtils';
-import {buildSearchQueryString} from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import * as SessionUtils from '@libs/SessionUtils';
import ConnectionCompletePage from '@pages/ConnectionCompletePage';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
@@ -94,7 +94,7 @@ const loadWorkspaceJoinUser = () => require('@pages/worksp
function getCentralPaneScreenInitialParams(screenName: CentralPaneName, initialReportID?: string): Partial> {
if (screenName === SCREENS.SEARCH.CENTRAL_PANE) {
// Generate default query string with buildSearchQueryString without argument.
- return {q: buildSearchQueryString()};
+ return {q: SearchQueryUtils.buildSearchQueryString()};
}
if (screenName === SCREENS.REPORT) {
@@ -225,6 +225,8 @@ const modalScreenListenersWithCancelSearch = {
function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDAppliedToClient}: AuthScreensProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ // We need to use isSmallScreenWidth for the root stack navigator
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, onboardingIsMediumOrLargerScreenWidth, isSmallScreenWidth} = useResponsiveLayout();
const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils);
const {canUseDefaultRooms} = usePermissions();
@@ -242,6 +244,8 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
let initialReportID: string | undefined;
const isInitialRender = useRef(true);
+
+ // eslint-disable-next-line react-compiler/react-compiler
if (isInitialRender.current) {
Timing.start(CONST.TIMING.HOMEPAGE_INITIAL_RENDER);
@@ -255,6 +259,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
initialReportID = initialReport?.reportID ?? '';
}
+ // eslint-disable-next-line react-compiler/react-compiler
isInitialRender.current = false;
}
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index b15c5235ae75..35f67e0253c6 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -87,7 +87,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepMerchant').default,
[SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: () => require('../../../../pages/iou/request/step/IOURequestStepParticipants').default,
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default,
- [SCREENS.SETTINGS_TAGS_ROOT]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default,
[SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../../pages/iou/request/step/IOURequestStepScan').default,
[SCREENS.MONEY_REQUEST.STEP_TAG]: () => require('../../../../pages/iou/request/step/IOURequestStepTag').default,
[SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: () => require('../../../../pages/iou/request/step/IOURequestStepWaypoint').default,
@@ -99,6 +99,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/AddDebitCardPage').default,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePaymentsPage').default,
[SCREENS.MONEY_REQUEST.STATE_SELECTOR]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default,
+ [SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: () => require('../../../../pages/iou/request/step/IOURequestStepAttendees').default,
});
const TravelModalStackNavigator = createModalStackNavigator({
@@ -142,6 +143,23 @@ const CategoriesModalStackNavigator = createModalStackNavigator({
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_CREATE]: () => require('../../../../pages/workspace/categories/CreateCategoryPage').default,
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_EDIT]: () => require('../../../../pages/workspace/categories/EditCategoryPage').default,
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_SETTINGS]: () => require('../../../../pages/workspace/categories/CategorySettingsPage').default,
+ [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_IMPORT]: () => require('../../../../pages/workspace/categories/ImportCategoriesPage').default,
+ [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_IMPORTED]: () => require('../../../../pages/workspace/categories/ImportedCategoriesPage').default,
+ [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_PAYROLL_CODE]: () => require('../../../../pages/workspace/categories/CategoryPayrollCodePage').default,
+ [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_GL_CODE]: () => require('../../../../pages/workspace/categories/CategoryGLCodePage').default,
+});
+
+const TagsModalStackNavigator = createModalStackNavigator({
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_SETTINGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_EDIT]: () => require('../../../../pages/workspace/tags/WorkspaceEditTagsPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_IMPORT]: () => require('../../../../pages/workspace/tags/ImportTagsPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_IMPORTED]: () => require('../../../../pages/workspace/tags/ImportedTagsPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_SETTINGS]: () => require('../../../../pages/workspace/tags/TagSettingsPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_LIST_VIEW]: () => require('../../../../pages/workspace/tags/WorkspaceViewTagsPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_CREATE]: () => require('../../../../pages/workspace/tags/WorkspaceCreateTagPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_EDIT]: () => require('../../../../pages/workspace/tags/EditTagPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_APPROVER]: () => require('../../../../pages/workspace/tags/TagApproverPage').default,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_GL_CODE]: () => require('../../../../pages/workspace/tags/TagGLCodePage').default,
});
const ExpensifyCardModalStackNavigator = createModalStackNavigator({
@@ -363,6 +381,12 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/qbo/import/QuickbooksTaxesPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS]: () => require('../../../../pages/workspace/accounting/qbo/import/QuickbooksLocationsPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES]: () => require('../../../../pages/workspace/accounting/qbo/import/QuickbooksClassesPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS]: () =>
+ require('../../../../pages/workspace/accounting/qbo/import/QuickbooksClassesDisplayedAsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS]: () =>
+ require('../../../../pages/workspace/accounting/qbo/import/QuickbooksCustomersDisplayedAsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS]: () =>
+ require('../../../../pages/workspace/accounting/qbo/import/QuickbooksLocationsDisplayedAsPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ADVANCED]: () => require('../../../../pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR]: () =>
require('../../../../pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage').default,
@@ -662,6 +686,7 @@ export {
SettingsModalStackNavigator,
SignInModalStackNavigator,
CategoriesModalStackNavigator,
+ TagsModalStackNavigator,
ExpensifyCardModalStackNavigator,
DomainCardModalStackNavigator,
SplitDetailsModalStackNavigator,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index 0df3ae3f4b92..da1ce32bf747 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -103,6 +103,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.SETTINGS_CATEGORIES}
component={ModalStackNavigators.CategoriesModalStackNavigator}
/>
+
{
- Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}));
+ Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchQueryUtils.buildCannedSearchQuery()}));
}}
>
{translate('common.cancel')}
diff --git a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/BottomTabBar.tsx
index cbcfa4b84677..3bf029012b36 100644
--- a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/BottomTabBar.tsx
@@ -17,7 +17,7 @@ import Navigation from '@libs/Navigation/Navigation';
import type {AuthScreensParamList} from '@libs/Navigation/types';
import {isCentralPaneName} from '@libs/NavigationUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
import navigationRef from '@navigation/navigationRef';
@@ -47,7 +47,7 @@ type BottomTabBarProps = {
* Otherwise policyID will be inserted into query
*/
function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: string): SearchQueryString {
- const queryJSON = SearchUtils.buildSearchQueryJSON(query);
+ const queryJSON = SearchQueryUtils.buildSearchQueryJSON(query);
if (!queryJSON) {
return query;
}
@@ -62,7 +62,7 @@ function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: stri
queryJSON.policyID = policyID;
}
- return SearchUtils.buildSearchQueryString(queryJSON);
+ return SearchQueryUtils.buildSearchQueryString(queryJSON);
}
function BottomTabBar({selectedTab}: BottomTabBarProps) {
@@ -130,7 +130,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
return;
}
- const defaultCannedQuery = SearchUtils.buildCannedSearchQuery();
+ const defaultCannedQuery = SearchQueryUtils.buildCannedSearchQuery();
// when navigating to search we might have an activePolicyID set from workspace switcher
const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery;
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
diff --git a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx
index 8967486165f8..eba7a7448ad0 100644
--- a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx
@@ -10,7 +10,7 @@ import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import SignInButton from '@pages/home/sidebar/SignInButton';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
@@ -61,7 +61,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true,
accessibilityLabel={translate('common.cancel')}
style={[styles.textBlue]}
onPress={() => {
- Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}));
+ Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchQueryUtils.buildCannedSearchQuery()}));
}}
>
{translate('common.cancel')}
diff --git a/src/libs/Navigation/extractPolicyIDFromQuery.ts b/src/libs/Navigation/extractPolicyIDFromQuery.ts
index bd0464f4aab6..f091690c16f2 100644
--- a/src/libs/Navigation/extractPolicyIDFromQuery.ts
+++ b/src/libs/Navigation/extractPolicyIDFromQuery.ts
@@ -1,4 +1,4 @@
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import type {NavigationPartialRoute} from './types';
function extractPolicyIDFromQuery(route?: NavigationPartialRoute) {
@@ -11,12 +11,12 @@ function extractPolicyIDFromQuery(route?: NavigationPartialRoute) {
}
const queryString = route.params.q as string;
- const queryJSON = SearchUtils.buildSearchQueryJSON(queryString);
+ const queryJSON = SearchQueryUtils.buildSearchQueryJSON(queryString);
if (!queryJSON) {
return undefined;
}
- return SearchUtils.getPolicyIDFromSearchQuery(queryJSON);
+ return SearchQueryUtils.getPolicyIDFromSearchQuery(queryJSON);
}
export default extractPolicyIDFromQuery;
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 39fa05cf87d4..60cb6f53f697 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -45,6 +45,9 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ADVANCED,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 72e5f398c1d8..6f551d7cc41c 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -383,6 +383,15 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR]: {
path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR.route,
},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS]: {
+ path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS]: {
+ path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS]: {
+ path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS.route,
+ },
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT]: {
path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT.route,
},
@@ -973,6 +982,80 @@ const config: LinkingOptions['config'] = {
categoryName: (categoryName: string) => decodeURIComponent(categoryName),
},
},
+ [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_IMPORT]: {
+ path: ROUTES.SETTINGS_CATEGORIES_IMPORT.route,
+ },
+ [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_IMPORTED]: {
+ path: ROUTES.SETTINGS_CATEGORIES_IMPORTED.route,
+ },
+ [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_PAYROLL_CODE]: {
+ path: ROUTES.SETTINGS_CATEGORY_PAYROLL_CODE.route,
+ parse: {
+ categoryName: (categoryName: string) => decodeURIComponent(categoryName),
+ },
+ },
+ [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_GL_CODE]: {
+ path: ROUTES.SETTINGS_CATEGORY_GL_CODE.route,
+ parse: {
+ categoryName: (categoryName: string) => decodeURIComponent(categoryName),
+ },
+ },
+ },
+ },
+ [SCREENS.RIGHT_MODAL.SETTINGS_TAGS]: {
+ screens: {
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_SETTINGS]: {
+ path: ROUTES.SETTINGS_TAGS_SETTINGS.route,
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_EDIT]: {
+ path: ROUTES.SETTINGS_TAGS_EDIT.route,
+ parse: {
+ orderWeight: Number,
+ },
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_CREATE]: {
+ path: ROUTES.SETTINGS_TAG_CREATE.route,
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_EDIT]: {
+ path: ROUTES.SETTINGS_TAG_EDIT.route,
+ parse: {
+ orderWeight: Number,
+ tagName: (tagName: string) => decodeURIComponent(tagName),
+ },
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_SETTINGS]: {
+ path: ROUTES.SETTINGS_TAG_SETTINGS.route,
+ parse: {
+ orderWeight: Number,
+ tagName: (tagName: string) => decodeURIComponent(tagName),
+ },
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_APPROVER]: {
+ path: ROUTES.SETTINGS_TAG_APPROVER.route,
+ parse: {
+ orderWeight: Number,
+ tagName: (tagName: string) => decodeURIComponent(tagName),
+ },
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_LIST_VIEW]: {
+ path: ROUTES.SETTINGS_TAG_LIST_VIEW.route,
+ parse: {
+ orderWeight: Number,
+ },
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAG_GL_CODE]: {
+ path: ROUTES.SETTINGS_TAG_GL_CODE.route,
+ parse: {
+ orderWeight: Number,
+ tagName: (tagName: string) => decodeURIComponent(tagName),
+ },
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_IMPORT]: {
+ path: ROUTES.SETTINGS_TAGS_IMPORT.route,
+ },
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_IMPORTED]: {
+ path: ROUTES.SETTINGS_TAGS_IMPORTED.route,
+ },
},
},
[SCREENS.RIGHT_MODAL.EXPENSIFY_CARD]: {
@@ -1091,7 +1174,7 @@ const config: LinkingOptions['config'] = {
},
},
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: ROUTES.SETTINGS_CATEGORIES_ROOT.route,
- [SCREENS.SETTINGS_TAGS_ROOT]: ROUTES.SETTINGS_TAGS_ROOT.route,
+ [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: ROUTES.SETTINGS_TAGS_ROOT.route,
[SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: ROUTES.MONEY_REQUEST_STEP_SEND_FROM.route,
[SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.route,
[SCREENS.MONEY_REQUEST.STEP_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_AMOUNT.route,
@@ -1112,6 +1195,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route,
[SCREENS.MONEY_REQUEST.STATE_SELECTOR]: {path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route, exact: true},
[SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.route,
+ [SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: ROUTES.MONEY_REQUEST_ATTENDEE.route,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS,
[SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT,
[SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD,
diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts
index 5ccc2da54418..16e705258e58 100644
--- a/src/libs/Navigation/switchPolicyID.ts
+++ b/src/libs/Navigation/switchPolicyID.ts
@@ -4,7 +4,7 @@ import {getPathFromState} from '@react-navigation/native';
import type {Writable} from 'type-fest';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import {isCentralPaneName} from '@libs/NavigationUtils';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
@@ -83,7 +83,7 @@ export default function switchPolicyID(navigation: NavigationContainerRef>;
const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config);
@@ -110,16 +110,16 @@ export default function switchPolicyID(navigation: NavigationContainerRef;
+ transactionID: string;
+ reportID: string;
+ backTo: Routes;
+ };
};
type NewTaskNavigatorParamList = {
@@ -1318,6 +1340,7 @@ type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.SETTINGS_CATEGORIES]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.SETTINGS_TAGS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.EXPENSIFY_CARD]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.DOMAIN_CARD]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: NavigatorScreenParams;
@@ -1409,6 +1432,7 @@ type FullScreenNavigatorParamList = {
};
[SCREENS.WORKSPACE.TAGS]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAXES]: {
policyID: string;
@@ -1431,6 +1455,15 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
policyID: string;
};
diff --git a/src/libs/Network/NetworkStore.ts b/src/libs/Network/NetworkStore.ts
index fe90aa87495e..467b62b8a82a 100644
--- a/src/libs/Network/NetworkStore.ts
+++ b/src/libs/Network/NetworkStore.ts
@@ -101,19 +101,30 @@ function isSupportRequest(command: string): boolean {
return [
WRITE_COMMANDS.OPEN_APP,
WRITE_COMMANDS.SEARCH,
+ WRITE_COMMANDS.UPDATE_NEWSLETTER_SUBSCRIPTION,
SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP,
SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT,
READ_COMMANDS.OPEN_CARD_DETAILS_PAGE,
+ READ_COMMANDS.GET_POLICY_CATEGORIES,
READ_COMMANDS.OPEN_POLICY_CATEGORIES_PAGE,
READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_PAGE,
+ READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED,
READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE,
+ READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE,
READ_COMMANDS.OPEN_POLICY_EXPENSIFY_CARDS_PAGE,
READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE,
READ_COMMANDS.OPEN_POLICY_PROFILE_PAGE,
READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE,
+ READ_COMMANDS.OPEN_POLICY_INITIAL_PAGE,
+ READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE,
READ_COMMANDS.OPEN_POLICY_TAGS_PAGE,
- READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE,
READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE,
+ READ_COMMANDS.OPEN_POLICY_TAXES_PAGE,
+ READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE,
+ READ_COMMANDS.OPEN_WORKSPACE_VIEW,
+ READ_COMMANDS.OPEN_PAYMENTS_PAGE,
+ READ_COMMANDS.OPEN_WORKSPACE_MEMBERS_PAGE,
+ READ_COMMANDS.SEARCH_FOR_REPORTS,
].some((cmd) => cmd === command);
}
diff --git a/src/libs/Network/enhanceParameters.ts b/src/libs/Network/enhanceParameters.ts
index 01d2185a34c6..1806726fdabb 100644
--- a/src/libs/Network/enhanceParameters.ts
+++ b/src/libs/Network/enhanceParameters.ts
@@ -20,6 +20,15 @@ Onyx.connect({
},
});
+// Check if the user is logged in as a delegate and send that if so
+let delegate = '';
+Onyx.connect({
+ key: ONYXKEYS.ACCOUNT,
+ callback: (val) => {
+ delegate = val?.delegatedAccess?.delegate ?? '';
+ },
+});
+
/**
* Does this command require an authToken?
*/
@@ -57,5 +66,9 @@ export default function enhanceParameters(command: string, parameters: Record = ReportUtils.OptionData & {
item: T;
- isOptimisticReportOption?: boolean;
};
type OptionList = {
@@ -180,8 +178,9 @@ type GetOptionsConfig = {
includeInvoiceRooms?: boolean;
includeDomainEmail?: boolean;
action?: IOUAction;
+ shouldAcceptName?: boolean;
+ recentAttendees?: Attendee[];
shouldBoldTitleByDefault?: boolean;
- includePoliciesWithoutExpenseChats?: boolean;
};
type GetUserToInviteConfig = {
@@ -191,6 +190,7 @@ type GetUserToInviteConfig = {
selectedOptions?: Array>;
reportActions?: ReportActions;
showChatPreviewLine?: boolean;
+ shouldAcceptName?: boolean;
};
type MemberForList = {
@@ -222,7 +222,10 @@ type Options = {
type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean};
-type FilterOptionsConfig = Pick & {
+type FilterOptionsConfig = Pick<
+ GetOptionsConfig,
+ 'sortByReportTypeInSearch' | 'canInviteUser' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' | 'shouldAcceptName'
+> & {
preferChatroomsOverThreads?: boolean;
preferPolicyExpenseChat?: boolean;
preferRecentExpenseReports?: boolean;
@@ -243,13 +246,6 @@ Onyx.connect({
},
});
-let allReportsDraft: OnyxCollection;
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
- waitForCollectionCallback: true,
- callback: (value) => (allReportsDraft = value),
-});
-
let loginList: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.LOGIN_LIST,
@@ -426,7 +422,7 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant
const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1];
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const login = detail?.login || participant.login || '';
- const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login));
+ const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login) || participant.text);
return {
keyForList: String(detail?.accountID),
@@ -1509,7 +1505,6 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions:
function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) {
const reportMapForAccountIDs: Record = {};
const allReportOptions: Array> = [];
- const policyToReportForPolicyExpenseChats: Record = {};
if (reports) {
Object.values(reports).forEach((report) => {
@@ -1525,10 +1520,6 @@ function createOptionList(personalDetails: OnyxEntry, repor
return;
}
- if (ReportUtils.isPolicyExpenseChat(report) && report.policyID) {
- policyToReportForPolicyExpenseChats[report.policyID] = report;
- }
-
// Save the report in the map if this is a single participant so we can associate the reportID with the
// personal detail option later. Individuals should not be associated with single participant
// policyExpenseChats or chatRooms since those are not people.
@@ -1543,46 +1534,6 @@ function createOptionList(personalDetails: OnyxEntry, repor
});
}
- const policiesWithoutExpenseChats = Object.values(policies ?? {}).filter((policy) => {
- if (policy?.type === CONST.POLICY.TYPE.PERSONAL) {
- return false;
- }
- return !policyToReportForPolicyExpenseChats[policy?.id ?? ''];
- });
-
- // go through each policy and create a optimistic report option for it
- if (policiesWithoutExpenseChats && policiesWithoutExpenseChats.length > 0) {
- policiesWithoutExpenseChats.forEach((policy) => {
- // check for draft report exist in allreportDrafts for the policy
- let draftReport = Object.values(allReportsDraft ?? {})?.find((reportDraft) => reportDraft?.policyID === policy?.id);
- if (!draftReport) {
- draftReport = ReportUtils.buildOptimisticChatReport(
- [currentUserAccountID ?? -1],
- '',
- CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
- policy?.id,
- currentUserAccountID,
- true,
- policy?.name,
- undefined,
- undefined,
- undefined,
- undefined,
- undefined,
- undefined,
- undefined,
- undefined,
- );
- createDraftReportForPolicyExpenseChat({...draftReport, isOptimisticReport: true});
- }
- const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(draftReport);
- allReportOptions.push({
- item: draftReport,
- isOptimisticReportOption: true,
- ...createOption(accountIDs, personalDetails, draftReport, {}),
- });
- });
- }
const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({
item: personalDetail,
...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}),
@@ -1679,6 +1630,7 @@ function canCreateOptimisticPersonalDetailOption({
* We create a new user option if the following conditions are satisfied:
* - There's no matching recent report and personal detail option
* - The searchValue is a valid email or phone number
+ * - If prop shouldAcceptName = true, the searchValue can be also a normal string
* - The searchValue isn't the current personal detail login
*/
function getUserToInviteOption({
@@ -1688,6 +1640,7 @@ function getUserToInviteOption({
selectedOptions = [],
reportActions = {},
showChatPreviewLine = false,
+ shouldAcceptName = false,
}: GetUserToInviteConfig): ReportUtils.OptionData | null {
const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchValue)));
const isCurrentUserLogin = isCurrentUser({login: searchValue} as PersonalDetails);
@@ -1697,7 +1650,7 @@ function getUserToInviteOption({
const isInOptionToExclude =
optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) !== -1;
- if (!searchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber) || isInOptionToExclude || excludeUnknownUsers) {
+ if (!searchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber && !shouldAcceptName) || isInOptionToExclude || excludeUnknownUsers) {
return null;
}
@@ -1714,7 +1667,7 @@ function getUserToInviteOption({
showChatPreviewLine,
});
userToInvite.isOptimisticAccount = true;
- userToInvite.login = searchValue;
+ userToInvite.login = isValidEmail || isValidPhoneNumber ? searchValue : '';
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
userToInvite.text = userToInvite.text || searchValue;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -1777,8 +1730,8 @@ function getOptions(
includeInvoiceRooms = false,
includeDomainEmail = false,
action,
+ recentAttendees,
shouldBoldTitleByDefault = true,
- includePoliciesWithoutExpenseChats = false,
}: GetOptionsConfig,
): Options {
if (includeCategories) {
@@ -1843,9 +1796,6 @@ function getOptions(
// Filter out all the reports that shouldn't be displayed
const filteredReportOptions = options.reports.filter((option) => {
- if (option.isOptimisticReportOption && !includePoliciesWithoutExpenseChats) {
- return;
- }
const report = option.item;
const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations);
@@ -1956,7 +1906,7 @@ function getOptions(
optionsToExclude.push({login});
});
- let recentReportOptions = [];
+ let recentReportOptions: ReportUtils.OptionData[] = [];
let personalDetailsOptions: ReportUtils.OptionData[] = [];
const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE;
@@ -2037,6 +1987,9 @@ function getOptions(
}
}
}
+ } else if (recentAttendees && recentAttendees?.length > 0) {
+ recentAttendees.filter((attendee) => attendee.login ?? attendee.displayName).forEach((a) => optionsToExclude.push({login: a.login ?? a.displayName}));
+ recentReportOptions = recentAttendees as ReportUtils.OptionData[];
}
const personalDetailsOptionsToExclude = [...optionsToExclude, {login: currentUserLogin}];
@@ -2195,7 +2148,6 @@ type FilteredOptionsParams = {
includeInvoiceRooms?: boolean;
action?: IOUAction;
sortByReportTypeInSearch?: boolean;
- includePoliciesWithoutExpenseChats?: boolean;
};
// It is not recommended to pass a search value to getFilteredOptions when passing reports and personalDetails.
@@ -2237,7 +2189,6 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue
includeInvoiceRooms = false,
action,
sortByReportTypeInSearch = false,
- includePoliciesWithoutExpenseChats = false,
} = params;
return getOptions(
{reports, personalDetails},
@@ -2267,7 +2218,52 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue
includeInvoiceRooms,
action,
sortByReportTypeInSearch,
- includePoliciesWithoutExpenseChats,
+ },
+ );
+}
+
+function getAttendeeOptions(
+ reports: Array>,
+ personalDetails: Array>,
+ betas: OnyxEntry,
+ attendees: Attendee[],
+ recentAttendees: Attendee[],
+ includeOwnedWorkspaceChats = false,
+ includeP2P = true,
+ canInviteUser = true,
+ includeInvoiceRooms = false,
+ action: IOUAction | undefined = undefined,
+ sortByReportTypeInSearch = false,
+) {
+ return getOptions(
+ {reports, personalDetails},
+ {
+ betas,
+ searchInputValue: '',
+ selectedOptions: attendees,
+ excludeLogins: CONST.EXPENSIFY_EMAILS,
+ includeOwnedWorkspaceChats,
+ includeRecentReports: false,
+ includeP2P,
+ includeCategories: false,
+ categories: {},
+ recentlyUsedCategories: [],
+ includeTags: false,
+ tags: {},
+ recentlyUsedTags: [],
+ canInviteUser,
+ includeSelectedOptions: false,
+ includeTaxRates: false,
+ maxRecentReportsToShow: 0,
+ taxRates: {} as TaxRatesWithDefault,
+ includeSelfDM: false,
+ includePolicyReportFieldOptions: false,
+ policyReportFieldOptions: [],
+ recentlyUsedPolicyReportFieldOptions: [],
+ includeInvoiceRooms,
+ action,
+ sortByReportTypeInSearch,
+ recentAttendees,
},
);
}
@@ -2481,6 +2477,31 @@ function getPersonalDetailSearchTerms(item: Partial) {
function getCurrentUserSearchTerms(item: ReportUtils.OptionData) {
return [item.text ?? '', item.login ?? '', item.login?.replace(CONST.EMAIL_SEARCH_REGEX, '') ?? ''];
}
+
+type PickUserToInviteParams = {
+ canInviteUser: boolean;
+ recentReports: ReportUtils.OptionData[];
+ personalDetails: ReportUtils.OptionData[];
+ searchValue: string;
+ config?: FilterOptionsConfig;
+ optionsToExclude: Option[];
+};
+
+const pickUserToInvite = ({canInviteUser, recentReports, personalDetails, searchValue, config, optionsToExclude}: PickUserToInviteParams) => {
+ let userToInvite = null;
+ if (canInviteUser) {
+ if (recentReports.length === 0 && personalDetails.length === 0) {
+ userToInvite = getUserToInviteOption({
+ searchValue,
+ selectedOptions: config?.selectedOptions,
+ optionsToExclude,
+ });
+ }
+ }
+
+ return userToInvite;
+};
+
/**
* Filters options based on the search input value
*/
@@ -2568,16 +2589,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
recentReports = orderOptions(recentReports, searchValue);
}
- let userToInvite = null;
- if (canInviteUser) {
- if (recentReports.length === 0 && personalDetails.length === 0) {
- userToInvite = getUserToInviteOption({
- searchValue,
- selectedOptions: config?.selectedOptions,
- optionsToExclude,
- });
- }
- }
+ const userToInvite = pickUserToInvite({canInviteUser, recentReports, personalDetails, searchValue, config, optionsToExclude});
if (maxRecentReportsToShow > 0 && recentReports.length > maxRecentReportsToShow) {
recentReports.splice(maxRecentReportsToShow);
@@ -2612,7 +2624,8 @@ function getEmptyOptions(): Options {
}
function shouldUseBoldText(report: ReportUtils.OptionData): boolean {
- return report.isUnread === true && ReportUtils.getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE;
+ const notificationPreference = ReportUtils.getReportNotificationPreference(report);
+ return report.isUnread === true && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
}
export {
@@ -2646,6 +2659,7 @@ export {
formatMemberForList,
formatSectionsFromSearchTerm,
getShareLogOptions,
+ orderOptions,
filterOptions,
createOptionList,
createOptionFromReport,
@@ -2658,7 +2672,9 @@ export {
getCurrentUserSearchTerms,
getEmptyOptions,
shouldUseBoldText,
+ getAttendeeOptions,
getAlternateText,
+ pickUserToInvite,
hasReportErrors,
};
diff --git a/src/libs/PolicyDistanceRatesUtils.ts b/src/libs/PolicyDistanceRatesUtils.ts
index 8e4d68f78b4c..11f65f0f07c0 100644
--- a/src/libs/PolicyDistanceRatesUtils.ts
+++ b/src/libs/PolicyDistanceRatesUtils.ts
@@ -26,10 +26,10 @@ function validateRateValue(values: FormOnyxValues, currency: stri
return errors;
}
-function validateTaxClaimableValue(values: FormOnyxValues, rate: Rate): FormInputErrors {
+function validateTaxClaimableValue(values: FormOnyxValues, rate: Rate | undefined): FormInputErrors {
const errors: FormInputErrors = {};
- if (rate.rate && Number(values.taxClaimableValue) > rate.rate / 100) {
+ if (rate?.rate && Number(values.taxClaimableValue) > rate.rate / 100) {
errors.taxClaimableValue = Localize.translateLocal('workspace.taxes.error.updateTaxClaimableFailureMessage');
}
return errors;
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index fae3d163b2e9..36e5ccef3308 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -132,15 +132,15 @@ function getNumericValue(value: number | string, toLocaleDigit: (arg: string) =>
/**
* Retrieves the distance custom unit object for the given policy
*/
-function getCustomUnit(policy: OnyxEntry): CustomUnit | undefined {
+function getDistanceRateCustomUnit(policy: OnyxEntry): CustomUnit | undefined {
return Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
}
/**
* Retrieves custom unit rate object from the given customUnitRateID
*/
-function getCustomUnitRate(policy: OnyxEntry, customUnitRateID: string): Rate | undefined {
- const distanceUnit = getCustomUnit(policy);
+function getDistanceRateCustomUnitRate(policy: OnyxEntry, customUnitRateID: string): Rate | undefined {
+ const distanceUnit = getDistanceRateCustomUnit(policy);
return distanceUnit?.rates[customUnitRateID];
}
@@ -382,7 +382,7 @@ function isControlPolicy(policy: OnyxEntry): boolean {
}
function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry, isDistanceRequest: boolean): boolean {
- const distanceUnit = getCustomUnit(policy);
+ const distanceUnit = getDistanceRateCustomUnit(policy);
const customUnitID = distanceUnit?.customUnitID ?? 0;
const isPolicyTaxTrackingEnabled = isPolicyExpenseChat && policy?.tax?.trackingEnabled;
const isTaxEnabledForDistance = isPolicyTaxTrackingEnabled && policy?.customUnits?.[customUnitID]?.attributes?.taxEnabled;
@@ -1148,8 +1148,8 @@ export {
getSageIntacctNonReimbursableActiveDefaultVendor,
getSageIntacctCreditCards,
getSageIntacctBankAccounts,
- getCustomUnit,
- getCustomUnitRate,
+ getDistanceRateCustomUnit,
+ getDistanceRateCustomUnitRate,
sortWorkspacesBySelected,
removePendingFieldsFromCustomUnit,
navigateWhenEnableFeature,
diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts
index 4d6a2b339809..450a6d7f5481 100644
--- a/src/libs/ReportActionComposeFocusManager.ts
+++ b/src/libs/ReportActionComposeFocusManager.ts
@@ -82,14 +82,6 @@ function isEditFocused(): boolean {
return !!editComposerRef.current?.isFocused();
}
-/**
- * Utility function to blur both main composer and edit composer.
- */
-function blurAll(): void {
- composerRef.current?.blur();
- editComposerRef.current?.blur();
-}
-
export default {
composerRef,
onComposerFocus,
@@ -98,5 +90,4 @@ export default {
isFocused,
editComposerRef,
isEditFocused,
- blurAll,
};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index ca4166f8a707..ade79ee327a2 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -1749,7 +1749,7 @@ function isCardIssuedAction(reportAction: OnyxEntry) {
);
}
-function getCardIssuedMessage(reportAction: OnyxEntry, shouldRenderHTML = false, policyID = '-1') {
+function getCardIssuedMessage(reportAction: OnyxEntry, shouldRenderHTML = false, policyID = '-1', shouldDisplayLinkToCard = false) {
const cardIssuedActionOriginalMessage = isActionOfType(
reportAction,
CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED,
@@ -1765,9 +1765,10 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende
const isPolicyAdmin = PolicyUtils.isPolicyAdmin(PolicyUtils.getPolicy(policyID));
const assignee = shouldRenderHTML ? ` ` : assigneeDetails?.firstName ?? assigneeDetails?.login ?? '';
const navigateRoute = isPolicyAdmin ? ROUTES.EXPENSIFY_CARD_DETAILS.getRoute(policyID, String(cardID)) : ROUTES.SETTINGS_DOMAINCARD_DETAIL.getRoute(String(cardID));
- const expensifyCardLink = shouldRenderHTML
- ? `${Localize.translateLocal('cardPage.expensifyCard')} `
- : Localize.translateLocal('cardPage.expensifyCard');
+ const expensifyCardLink =
+ shouldRenderHTML && shouldDisplayLinkToCard
+ ? `${Localize.translateLocal('cardPage.expensifyCard')} `
+ : Localize.translateLocal('cardPage.expensifyCard');
const companyCardLink = shouldRenderHTML
? `${Localize.translateLocal('workspace.companyCards.companyCard')} `
: Localize.translateLocal('workspace.companyCards.companyCard');
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 1ef60b626ac7..223e94e0bdde 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -13,7 +13,7 @@ import type {SvgProps} from 'react-native-svg';
import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage';
import type {TupleToUnion, ValueOf} from 'type-fest';
import type {FileObject} from '@components/AttachmentModal';
-import {FallbackAvatar, QBOCircle, XeroCircle} from '@components/Icon/Expensicons';
+import {FallbackAvatar, IntacctSquare, NetSuiteSquare, QBOSquare, XeroSquare} from '@components/Icon/Expensicons';
import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars';
import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars';
import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput';
@@ -44,7 +44,7 @@ import type {
TransactionViolation,
UserWallet,
} from '@src/types/onyx';
-import type {Participant} from '@src/types/onyx/IOU';
+import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction';
import type Onboarding from '@src/types/onyx/Onboarding';
@@ -300,7 +300,6 @@ type OptimisticChatReport = Pick<
| 'chatReportID'
| 'iouReportID'
| 'isOwnPolicyExpenseChat'
- | 'isPolicyExpenseChat'
| 'isPinned'
| 'lastActorAccountID'
| 'lastMessageTranslationKey'
@@ -418,6 +417,7 @@ type OptimisticTaskReport = Pick<
type TransactionDetails = {
created: string;
amount: number;
+ attendees: Attendee[];
taxAmount?: number;
taxCode?: string;
currency: string;
@@ -1290,6 +1290,16 @@ function isProcessingReport(report: OnyxEntry): boolean {
return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED;
}
+function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean {
+ if (!report) {
+ return false;
+ }
+
+ const submitsToAccountID = PolicyUtils.getSubmitToAccountID(getPolicy(report.policyID), report.ownerAccountID ?? -1);
+
+ return isProcessingReport(report) && submitsToAccountID === report.managerID;
+}
+
/**
* Check if the report is a single chat report that isn't a thread
* and personal detail of participant is optimistic data
@@ -1745,6 +1755,10 @@ function canAddOrDeleteTransactions(moneyRequestReport: OnyxEntry): bool
return false;
}
+ if (PolicyUtils.isInstantSubmitEnabled(policy) && isProcessingReport(moneyRequestReport)) {
+ return isAwaitingFirstLevelApproval(moneyRequestReport);
+ }
+
if (isReportApproved(moneyRequestReport) || isSettled(moneyRequestReport?.reportID)) {
return false;
}
@@ -2994,6 +3008,7 @@ function getTransactionDetails(transaction: OnyxInputOrEntry, creat
return {
created: TransactionUtils.getFormattedCreated(transaction, createdDateFormat),
amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
+ attendees: TransactionUtils.getAttendees(transaction),
taxAmount: TransactionUtils.getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
taxCode: TransactionUtils.getTaxCode(transaction),
currency: TransactionUtils.getCurrency(transaction),
@@ -3572,6 +3587,9 @@ function getModifiedExpenseOriginalMessage(
originalMessage.oldMerchant = TransactionUtils.getMerchant(oldTransaction);
originalMessage.merchant = transactionChanges?.merchant;
}
+ if ('attendees' in transactionChanges) {
+ [originalMessage.oldAttendees, originalMessage.attendees] = TransactionUtils.getFormattedAttendees(transactionChanges?.attendees, TransactionUtils.getAttendees(oldTransaction));
+ }
// The amount is always a combination of the currency and the number value so when one changes we need to store both
// to match how we handle the modified expense action in oldDot
@@ -4113,11 +4131,11 @@ function navigateBackAfterDeleteTransaction(backRoute: Route | undefined, isFrom
/**
* Go back to the previous page from the edit private page of a given report
*/
-function goBackFromPrivateNotes(report: OnyxEntry, session: OnyxEntry, backTo?: string) {
- if (isEmpty(report) || isEmpty(session) || !session.accountID) {
+function goBackFromPrivateNotes(report: OnyxEntry, accountID?: number, backTo?: string) {
+ if (isEmpty(report) || !accountID) {
return;
}
- const currentUserPrivateNote = report.privateNotes?.[session.accountID]?.note ?? '';
+ const currentUserPrivateNote = report.privateNotes?.[accountID]?.note ?? '';
if (isEmpty(currentUserPrivateNote)) {
const participantAccountIDs = getParticipantsAccountIDsForDisplay(report);
@@ -4504,6 +4522,9 @@ function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, re
[currentUserAccountID ?? -1]: {
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
},
+ [receiverAccountID]: {
+ notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ },
},
parentReportID: chatReportID,
lastVisibleActionCreated: DateUtils.getDBTime(),
@@ -5323,7 +5344,6 @@ function buildOptimisticChatReport(
chatType,
isOwnPolicyExpenseChat,
isPinned: isNewlyCreatedWorkspaceChat,
- isPolicyExpenseChat: chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
lastActorAccountID: 0,
lastMessageTranslationKey: '',
lastMessageHtml: '',
@@ -6126,7 +6146,7 @@ function isUnread(report: OnyxEntry): boolean {
return false;
}
- if (isEmptyReport(report) && !isSelfDM(report)) {
+ if (isEmptyReport(report)) {
return false;
}
// lastVisibleActionCreated and lastReadTime are both datetime strings and can be compared directly
@@ -8233,11 +8253,18 @@ function getSourceIDFromReportAction(reportAction: OnyxEntry): str
function getIntegrationIcon(connectionName?: ConnectionName) {
if (connectionName === CONST.POLICY.CONNECTIONS.NAME.XERO) {
- return XeroCircle;
+ return XeroSquare;
}
if (connectionName === CONST.POLICY.CONNECTIONS.NAME.QBO) {
- return QBOCircle;
+ return QBOSquare;
+ }
+ if (connectionName === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) {
+ return NetSuiteSquare;
}
+ if (connectionName === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT) {
+ return IntacctSquare;
+ }
+
return undefined;
}
@@ -8521,6 +8548,7 @@ export {
isPolicyExpenseChat,
isPolicyExpenseChatAdmin,
isProcessingReport,
+ isAwaitingFirstLevelApproval,
isPublicAnnounceRoom,
isPublicRoom,
isReportApproved,
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchQueryUtils.ts
similarity index 52%
rename from src/libs/SearchUtils.ts
rename to src/libs/SearchQueryUtils.ts
index cd5af621ef81..32c2eff72007 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchQueryUtils.ts
@@ -1,54 +1,25 @@
import cloneDeep from 'lodash/cloneDeep';
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
-import type {AdvancedFiltersKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SearchStatus, SortOrder} from '@components/Search/types';
-import ChatListItem from '@components/SelectionList/ChatListItem';
-import ReportListItem from '@components/SelectionList/Search/ReportListItem';
-import TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
-import type {ListItem, ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
-import * as Expensicons from '@src/components/Icon/Expensicons';
+import type {AdvancedFiltersKeys, ASTNode, QueryFilter, QueryFilters, SearchQueryJSON, SearchQueryString, SearchStatus} from '@components/Search/types';
import CONST from '@src/CONST';
-import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm';
import type * as OnyxTypes from '@src/types/onyx';
-import type SearchResults from '@src/types/onyx/SearchResults';
-import type {ListItemDataType, ListItemType, SearchDataTypes, SearchPersonalDetails, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
-import * as CurrencyUtils from './CurrencyUtils';
-import DateUtils from './DateUtils';
-import {translateLocal} from './Localize';
+import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import {validateAmount} from './MoneyRequestUtils';
-import Navigation from './Navigation/Navigation';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import {getTagNamesFromTagsLists} from './PolicyUtils';
-import * as ReportActionsUtils from './ReportActionsUtils';
import * as ReportUtils from './ReportUtils';
import * as searchParser from './SearchParser/searchParser';
-import * as TransactionUtils from './TransactionUtils';
import * as UserUtils from './UserUtils';
import * as ValidationUtils from './ValidationUtils';
type FilterKeys = keyof typeof CONST.SEARCH.SYNTAX_FILTER_KEYS;
-const columnNamesToSortingProperty = {
- [CONST.SEARCH.TABLE_COLUMNS.TO]: 'formattedTo' as const,
- [CONST.SEARCH.TABLE_COLUMNS.FROM]: 'formattedFrom' as const,
- [CONST.SEARCH.TABLE_COLUMNS.DATE]: 'date' as const,
- [CONST.SEARCH.TABLE_COLUMNS.TAG]: 'tag' as const,
- [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]: '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,
- [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: null,
-};
-
-// This map contains signs with spaces that match each operator
-const operatorToSignMap = {
+// This map contains chars that match each operator
+const operatorToCharMap = {
[CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO]: ':' as const,
[CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN]: '<' as const,
[CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO]: '<=' as const,
@@ -59,404 +30,21 @@ const operatorToSignMap = {
[CONST.SEARCH.SYNTAX_OPERATORS.OR]: ' ' as const,
};
-const emptyPersonalDetails = {
- accountID: CONST.REPORT.OWNER_ACCOUNT_ID_FAKE,
- avatar: '',
- displayName: undefined,
- login: undefined,
-};
-/* Search list and results related */
-
/**
* @private
+ * Returns string value wrapped in quotes "", if the value contains special characters.
*/
-function getTransactionItemCommonFormattedProperties(
- transactionItem: SearchTransaction,
- from: SearchPersonalDetails,
- to: SearchPersonalDetails,
-): Pick {
- const isExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE;
-
- const formattedFrom = from?.displayName ?? from?.login ?? '';
- const formattedTo = to?.displayName ?? to?.login ?? '';
- const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport);
- const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created;
- const merchant = TransactionUtils.getMerchant(transactionItem);
- const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant;
-
- return {
- formattedFrom,
- formattedTo,
- date,
- formattedTotal,
- formattedMerchant,
- };
-}
-
-type ReportKey = `${typeof ONYXKEYS.COLLECTION.REPORT}${string}`;
-
-type TransactionKey = `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`;
-
-type ReportActionKey = `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`;
-
-function isReportEntry(key: string): key is ReportKey {
- return key.startsWith(ONYXKEYS.COLLECTION.REPORT);
-}
-
-function isTransactionEntry(key: string): key is TransactionKey {
- return key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION);
-}
-
-function isReportActionEntry(key: string): key is ReportActionKey {
- return key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
-}
-
-function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean {
- return Object.keys(data).some((key) => {
- if (isTransactionEntry(key)) {
- const item = data[key];
- const merchant = item.modifiedMerchant ? item.modifiedMerchant : item.merchant ?? '';
- return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT;
- }
- return false;
- });
-}
-
-const currentYear = new Date().getFullYear();
-
-function isReportListItemType(item: ListItem): item is ReportListItemType {
- return 'transactions' in item;
-}
-
-function isTransactionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is TransactionListItemType {
- const transactionListItem = item as TransactionListItemType;
- return transactionListItem.transactionID !== undefined;
-}
-
-function isReportActionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is ReportActionListItemType {
- const reportActionListItem = item as ReportActionListItemType;
- return reportActionListItem.reportActionID !== undefined;
-}
-
-function shouldShowYear(data: TransactionListItemType[] | ReportListItemType[] | OnyxTypes.SearchResults['data']): boolean {
- if (Array.isArray(data)) {
- return data.some((item: TransactionListItemType | ReportListItemType) => {
- if (isReportListItemType(item)) {
- // If the item is a ReportListItemType, iterate over its transactions and check them
- return item.transactions.some((transaction) => {
- const transactionYear = new Date(TransactionUtils.getCreated(transaction)).getFullYear();
- return transactionYear !== currentYear;
- });
- }
-
- const createdYear = new Date(item?.modifiedCreated ? item.modifiedCreated : item?.created || '').getFullYear();
- return createdYear !== currentYear;
- });
- }
-
- for (const key in data) {
- if (isTransactionEntry(key)) {
- const item = data[key];
- const date = TransactionUtils.getCreated(item);
-
- if (DateUtils.doesDateBelongToAPastYear(date)) {
- return true;
- }
- } else if (isReportActionEntry(key)) {
- const item = data[key];
- for (const action of Object.values(item)) {
- const date = action.created;
-
- if (DateUtils.doesDateBelongToAPastYear(date)) {
- return true;
- }
- }
- }
- }
- return false;
-}
-
-function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']): TransactionListItemType[] {
- const shouldShowMerchant = getShouldShowMerchant(data);
-
- const doesDataContainAPastYearTransaction = shouldShowYear(data);
-
- return Object.keys(data)
- .filter(isTransactionEntry)
- .map((key) => {
- const transactionItem = data[key];
- const from = data.personalDetailsList?.[transactionItem.accountID];
- const to = transactionItem.managerID ? data.personalDetailsList?.[transactionItem.managerID] : emptyPersonalDetails;
-
- const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties(transactionItem, from, to);
-
- return {
- ...transactionItem,
- from,
- to,
- formattedFrom,
- formattedTo,
- formattedTotal,
- formattedMerchant,
- date,
- shouldShowMerchant,
- shouldShowCategory: metadata?.columnsToShow?.shouldShowCategoryColumn,
- shouldShowTag: metadata?.columnsToShow?.shouldShowTagColumn,
- shouldShowTax: metadata?.columnsToShow?.shouldShowTaxColumn,
- keyForList: transactionItem.transactionID,
- shouldShowYear: doesDataContainAPastYearTransaction,
- };
- });
-}
-
-function getReportActionsSections(data: OnyxTypes.SearchResults['data']): ReportActionListItemType[] {
- const reportActionItems: ReportActionListItemType[] = [];
- for (const key in data) {
- if (isReportActionEntry(key)) {
- const reportActions = data[key];
- for (const reportAction of Object.values(reportActions)) {
- const from = data.personalDetailsList?.[reportAction.accountID];
- if (ReportActionsUtils.isDeletedAction(reportAction)) {
- // eslint-disable-next-line no-continue
- continue;
- }
- reportActionItems.push({
- ...reportAction,
- from,
- formattedFrom: from?.displayName ?? from?.login ?? '',
- date: reportAction.created,
- keyForList: reportAction.reportActionID,
- });
- }
- }
- }
- return reportActionItems;
-}
-
-function getIOUReportName(data: OnyxTypes.SearchResults['data'], reportItem: SearchReport) {
- const payerPersonalDetails = reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails;
- const payerName = payerPersonalDetails?.displayName ?? payerPersonalDetails?.login ?? translateLocal('common.hidden');
- const formattedAmount = CurrencyUtils.convertToDisplayString(reportItem.total ?? 0, reportItem.currency ?? CONST.CURRENCY.USD);
- if (reportItem.action === CONST.SEARCH.ACTION_TYPES.VIEW) {
- return translateLocal('iou.payerOwesAmount', {
- payer: payerName,
- amount: formattedAmount,
- });
- }
-
- if (reportItem.action === CONST.SEARCH.ACTION_TYPES.PAID) {
- return translateLocal('iou.payerPaidAmount', {
- payer: payerName,
- amount: formattedAmount,
- });
- }
-
- return reportItem.reportName;
-}
-
-function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']): ReportListItemType[] {
- const shouldShowMerchant = getShouldShowMerchant(data);
-
- const doesDataContainAPastYearTransaction = shouldShowYear(data);
-
- const reportIDToTransactions: Record = {};
- for (const key in data) {
- if (isReportEntry(key)) {
- const reportItem = {...data[key]};
- const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`;
- const transactions = reportIDToTransactions[reportKey]?.transactions ?? [];
- const isIOUReport = reportItem.type === CONST.REPORT.TYPE.IOU;
-
- reportIDToTransactions[reportKey] = {
- ...reportItem,
- keyForList: reportItem.reportID,
- from: data.personalDetailsList?.[reportItem.accountID ?? -1],
- to: reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails,
- transactions,
- reportName: isIOUReport ? getIOUReportName(data, reportItem) : reportItem.reportName,
- };
- } else if (isTransactionEntry(key)) {
- const transactionItem = {...data[key]};
- const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`;
-
- const from = data.personalDetailsList?.[transactionItem.accountID];
- const to = transactionItem.managerID ? data.personalDetailsList?.[transactionItem.managerID] : emptyPersonalDetails;
-
- const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties(transactionItem, from, to);
-
- const transaction = {
- ...transactionItem,
- from,
- to,
- formattedFrom,
- formattedTo,
- formattedTotal,
- formattedMerchant,
- date,
- shouldShowMerchant,
- shouldShowCategory: metadata?.columnsToShow?.shouldShowCategoryColumn,
- shouldShowTag: metadata?.columnsToShow?.shouldShowTagColumn,
- shouldShowTax: metadata?.columnsToShow?.shouldShowTaxColumn,
- keyForList: transactionItem.transactionID,
- shouldShowYear: doesDataContainAPastYearTransaction,
- };
- if (reportIDToTransactions[reportKey]?.transactions) {
- reportIDToTransactions[reportKey].transactions.push(transaction);
- } else if (reportIDToTransactions[reportKey]) {
- reportIDToTransactions[reportKey].transactions = [transaction];
- }
- }
- }
-
- return Object.values(reportIDToTransactions);
-}
-
-function getListItem(type: SearchDataTypes, status: SearchStatus): ListItemType {
- if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
- return ChatListItem;
- }
- if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
- return TransactionListItem;
- }
- return ReportListItem;
-}
-
-function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) {
- if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
- return getReportActionsSections(data);
- }
- if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
- return getTransactionsSections(data, metadata);
- }
- return getReportSections(data, metadata);
-}
-
-function getSortedSections(type: SearchDataTypes, status: SearchStatus, data: ListItemDataType, sortBy?: SearchColumnType, sortOrder?: SortOrder) {
- if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
- return getSortedReportActionData(data as ReportActionListItemType[]);
- }
- if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
- return getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder);
- }
- return getSortedReportData(data as ReportListItemType[]);
-}
-
-function getSortedTransactionData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) {
- if (!sortBy || !sortOrder) {
- return data;
- }
-
- const sortingProperty = columnNamesToSortingProperty[sortBy];
-
- if (!sortingProperty) {
- return data;
- }
-
- return data.sort((a, b) => {
- const aValue = sortingProperty === 'comment' ? a.comment?.comment : a[sortingProperty];
- const bValue = sortingProperty === 'comment' ? b.comment?.comment : b[sortingProperty];
-
- if (aValue === undefined || bValue === undefined) {
- return 0;
- }
-
- // We are guaranteed that both a and b will be string or number at the same time
- if (typeof aValue === 'string' && typeof bValue === 'string') {
- return sortOrder === CONST.SEARCH.SORT_ORDER.ASC ? aValue.toLowerCase().localeCompare(bValue) : bValue.toLowerCase().localeCompare(aValue);
- }
-
- const aNum = aValue as number;
- const bNum = bValue as number;
-
- return sortOrder === CONST.SEARCH.SORT_ORDER.ASC ? aNum - bNum : bNum - aNum;
- });
-}
-
-function getReportNewestTransactionDate(report: ReportListItemType) {
- return report.transactions?.reduce((max, curr) => (curr.modifiedCreated ?? curr.created > (max?.created ?? '') ? curr : max), report.transactions.at(0))?.created;
-}
-
-function getSortedReportData(data: ReportListItemType[]) {
- return data.sort((a, b) => {
- const aNewestTransaction = getReportNewestTransactionDate(a);
- const bNewestTransaction = getReportNewestTransactionDate(b);
-
- if (!aNewestTransaction || !bNewestTransaction) {
- return 0;
- }
-
- return bNewestTransaction.toLowerCase().localeCompare(aNewestTransaction);
- });
-}
-
-function getSortedReportActionData(data: ReportActionListItemType[]) {
- return data.sort((a, b) => {
- const aValue = a?.created;
- const bValue = b?.created;
-
- if (aValue === undefined || bValue === undefined) {
- return 0;
- }
-
- return bValue.toLowerCase().localeCompare(aValue);
- });
-}
-
-function isSearchResultsEmpty(searchResults: SearchResults) {
- return !Object.keys(searchResults?.data).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION));
-}
-
-function getQueryHash(query: SearchQueryJSON): number {
- let orderedQuery = '';
- if (query.policyID) {
- orderedQuery += `${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${query.policyID} `;
- }
- orderedQuery += `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${query.type}`;
- orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${query.status}`;
- orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`;
- orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER}:${query.sortOrder}`;
-
- Object.keys(query.flatFilters)
- .sort()
- .forEach((key) => {
- const filterValues = query.flatFilters?.[key as AdvancedFiltersKeys];
- const sortedFilterValues = filterValues?.sort((queryFilter1, queryFilter2) => {
- if (queryFilter1.value > queryFilter2.value) {
- return 1;
- }
- return -1;
- });
- orderedQuery += ` ${buildFilterString(key, sortedFilterValues ?? [])}`;
- });
-
- return UserUtils.hashText(orderedQuery, 2 ** 32);
-}
-
-function getExpenseTypeTranslationKey(expenseType: ValueOf): TranslationPaths {
- // eslint-disable-next-line default-case
- switch (expenseType) {
- case CONST.SEARCH.TRANSACTION_TYPE.DISTANCE:
- return 'common.distance';
- case CONST.SEARCH.TRANSACTION_TYPE.CARD:
- return 'common.card';
- case CONST.SEARCH.TRANSACTION_TYPE.CASH:
- return 'iou.cash';
+function sanitizeSearchValue(str: string) {
+ const regexp = /[^A-Za-z0-9_@./#&+\-\\';,"]/g;
+ if (regexp.test(str)) {
+ return `"${str}"`;
}
-}
-
-/* Search query related */
-
-/**
- * Update string query with all the default params that are set by parser
- */
-function normalizeQuery(query: string) {
- const normalizedQueryJSON = buildSearchQueryJSON(query);
- return buildSearchQueryString(normalizedQueryJSON);
+ return str;
}
/**
* @private
- * returns Date filter query string part, which needs special logic
+ * Returns date filter value for QueryString.
*/
function buildDateFilterQuery(filterValues: Partial) {
const dateBefore = filterValues[FILTER_KEYS.DATE_BEFORE];
@@ -478,7 +66,7 @@ function buildDateFilterQuery(filterValues: Partial)
/**
* @private
- * returns Date filter query string part, which needs special logic
+ * Returns amount filter value for QueryString.
*/
function buildAmountFilterQuery(filterValues: Partial) {
const lessThan = filterValues[FILTER_KEYS.LESS_THAN];
@@ -498,17 +86,33 @@ function buildAmountFilterQuery(filterValues: Partial
return amountFilter;
}
-function sanitizeString(str: string) {
- const regexp = /[^A-Za-z0-9_@./#&+\-\\';,"]/g;
- if (regexp.test(str)) {
- return `"${str}"`;
- }
- return str;
+/**
+ * @private
+ * Returns string of correctly formatted filter values from QueryFilters object.
+ */
+function buildFilterValuesString(filterName: string, queryFilters: QueryFilter[]) {
+ const delimiter = filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD ? ' ' : ',';
+ let filterValueString = '';
+ queryFilters.forEach((queryFilter, index) => {
+ // If the previous queryFilter has the same operator (this rule applies only to eq and neq operators) then append the current value
+ if (
+ index !== 0 &&
+ ((queryFilter.operator === 'eq' && queryFilters?.at(index - 1)?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters.at(index - 1)?.operator === 'neq'))
+ ) {
+ filterValueString += `${delimiter}${sanitizeSearchValue(queryFilter.value.toString())}`;
+ } else if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) {
+ filterValueString += `${delimiter}${sanitizeSearchValue(queryFilter.value.toString())}`;
+ } else {
+ filterValueString += ` ${filterName}${operatorToCharMap[queryFilter.operator]}${sanitizeSearchValue(queryFilter.value.toString())}`;
+ }
+ });
+
+ return filterValueString;
}
/**
* @private
- * traverses the AST and returns filters as a QueryFilters object
+ * Traverses the AST and returns filters as a QueryFilters object.
*/
function getFilters(queryJSON: SearchQueryJSON) {
const filters = {} as QueryFilters;
@@ -560,6 +164,82 @@ function getFilters(queryJSON: SearchQueryJSON) {
return filters;
}
+/**
+ * @private
+ * Given a filter name and its value, this function returns the corresponding ID found in Onyx data.
+ */
+function findIDFromDisplayValue(filterName: ValueOf, filter: string | string[], cardList: OnyxTypes.CardList, taxRates: Record) {
+ if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
+ if (typeof filter === 'string') {
+ const email = filter;
+ return PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? filter;
+ }
+ const emails = filter;
+ return emails.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email);
+ }
+ if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) {
+ const names = Array.isArray(filter) ? filter : ([filter] as string[]);
+ return names.map((name) => taxRates[name] ?? name).flat();
+ }
+ if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) {
+ if (typeof filter === 'string') {
+ const bank = filter;
+ const ids =
+ Object.values(cardList)
+ .filter((card) => card.bank === bank)
+ .map((card) => card.cardID.toString()) ?? filter;
+ return ids.length > 0 ? ids : bank;
+ }
+ const banks = filter;
+ return banks
+ .map(
+ (bank) =>
+ Object.values(cardList)
+ .filter((card) => card.bank === bank)
+ .map((card) => card.cardID.toString()) ?? bank,
+ )
+ .flat();
+ }
+ return filter;
+}
+
+/**
+ * @private
+ * Computes and returns a numerical hash for a given queryJSON.
+ * Sorts the query keys and values to ensure that hashes stay consistent.
+ */
+function getQueryHash(query: SearchQueryJSON): number {
+ let orderedQuery = '';
+ if (query.policyID) {
+ orderedQuery += `${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${query.policyID} `;
+ }
+ orderedQuery += `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${query.type}`;
+ orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${query.status}`;
+ orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`;
+ orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER}:${query.sortOrder}`;
+
+ Object.keys(query.flatFilters)
+ .sort()
+ .forEach((key) => {
+ const filterValues = query.flatFilters?.[key as AdvancedFiltersKeys];
+ const sortedFilterValues = filterValues?.sort((queryFilter1, queryFilter2) => {
+ if (queryFilter1.value > queryFilter2.value) {
+ return 1;
+ }
+ return -1;
+ });
+ orderedQuery += ` ${buildFilterValuesString(key, sortedFilterValues ?? [])}`;
+ });
+
+ return UserUtils.hashText(orderedQuery, 2 ** 32);
+}
+
+/**
+ * Parses a given search query string into a structured `SearchQueryJSON` format.
+ * This format of query is most commonly shared between components and also sent to backend to retrieve search results.
+ *
+ * In a way this is the reverse of buildSearchQueryString()
+ */
function buildSearchQueryJSON(query: SearchQueryString) {
try {
const result = searchParser.parse(query) as SearchQueryJSON;
@@ -569,12 +249,19 @@ function buildSearchQueryJSON(query: SearchQueryString) {
result.inputQuery = query;
result.flatFilters = flatFilters;
result.hash = getQueryHash(result);
+
return result;
} catch (e) {
console.error(`Error when parsing SearchQuery: "${query}"`, e);
}
}
+/**
+ * Formats a given `SearchQueryJSON` object into the string version of query.
+ * This format of query is the most basic string format and is used as the query param `q` in search URLs.
+ *
+ * In a way this is the reverse of buildSearchQueryJSON()
+ */
function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
const queryParts: string[] = [];
const defaultQueryJSON = buildSearchQueryJSON('');
@@ -598,7 +285,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
const queryFilter = filters[filterKey];
if (queryFilter) {
- const filterValueString = buildFilterString(filterKey, queryFilter);
+ const filterValueString = buildFilterValuesString(filterKey, queryFilter);
queryParts.push(filterValueString);
}
}
@@ -607,7 +294,10 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
}
/**
- * Given object with chosen search filters builds correct query string from them
+ * Formats a given object with search filter values into the string version of the query.
+ * Main usage is to consume data format that comes from AdvancedFilters Onyx Form Data, and generate query string.
+ *
+ * Reverse operation of buildFilterFormValuesFromQuery()
*/
function buildQueryStringFromFilterFormValues(filterValues: Partial) {
// We separate type and status filters from other filters to maintain hashes consistency for saved searches
@@ -618,17 +308,17 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
if (keyInCorrectForm) {
- return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${sanitizeString(filterValue as string)}`;
+ return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${sanitizeSearchValue(filterValue as string)}`;
}
}
if (filterKey === FILTER_KEYS.KEYWORD && filterValue) {
- const value = (filterValue as string).split(' ').map(sanitizeString).join(' ');
+ const value = (filterValue as string).split(' ').map(sanitizeSearchValue).join(' ');
return `${value}`;
}
@@ -662,7 +352,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial(filterValue)];
const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as FilterKeys[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
if (keyInCorrectForm) {
- return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`;
+ return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeSearchValue).join(',')}`;
}
}
@@ -677,11 +367,15 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial) {
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
// login can be an empty string
@@ -808,27 +506,13 @@ function getDisplayValue(filterName: string, filter: string, personalDetails: On
return filter;
}
-function buildFilterString(filterName: string, queryFilters: QueryFilter[]) {
- const delimiter = filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD ? ' ' : ',';
- let filterValueString = '';
- queryFilters.forEach((queryFilter, index) => {
- // If the previous queryFilter has the same operator (this rule applies only to eq and neq operators) then append the current value
- if (
- index !== 0 &&
- ((queryFilter.operator === 'eq' && queryFilters?.at(index - 1)?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters.at(index - 1)?.operator === 'neq'))
- ) {
- filterValueString += `${delimiter}${sanitizeString(queryFilter.value.toString())}`;
- } else if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) {
- filterValueString += `${delimiter}${sanitizeString(queryFilter.value.toString())}`;
- } else {
- filterValueString += ` ${filterName}${operatorToSignMap[queryFilter.operator]}${sanitizeString(queryFilter.value.toString())}`;
- }
- });
-
- return filterValueString;
-}
-
-function getSearchHeaderTitle(
+/**
+ * Formats a given `SearchQueryJSON` object into the human-readable string version of query.
+ * This format of query is the one which we want to display to users.
+ * We try to replace every numeric id value with a display version of this value,
+ * So: user IDs get turned into emails, report ids into report names etc.
+ */
+function buildUserReadableQueryString(
queryJSON: SearchQueryJSON,
PersonalDetails: OnyxTypes.PersonalDetailsList,
cardList: OnyxTypes.CardList,
@@ -864,12 +548,15 @@ function getSearchHeaderTitle(
value: getDisplayValue(key, filter.value.toString(), PersonalDetails, cardList, reports),
}));
}
- title += buildFilterString(key, displayQueryFilters);
+ title += buildFilterValuesString(key, displayQueryFilters);
});
return title;
}
+/**
+ * Returns properly built QueryString for a canned query, with the optional policyID.
+ */
function buildCannedSearchQuery({
type = CONST.SEARCH.DATA_TYPES.EXPENSE,
status = CONST.SEARCH.STATUS.EXPENSE.ALL,
@@ -881,73 +568,20 @@ function buildCannedSearchQuery({
} = {}): SearchQueryString {
const queryString = policyID ? `type:${type} status:${status} policyID:${policyID}` : `type:${type} status:${status}`;
- return normalizeQuery(queryString);
-}
-
-function getOverflowMenu(itemName: string, hash: number, inputQuery: string, showDeleteModal: (hash: number) => void, isMobileMenu?: boolean, closeMenu?: () => void) {
- return [
- {
- text: translateLocal('common.rename'),
- onSelected: () => {
- if (isMobileMenu && closeMenu) {
- closeMenu();
- }
- Navigation.navigate(ROUTES.SEARCH_SAVED_SEARCH_RENAME.getRoute({name: encodeURIComponent(itemName), jsonQuery: inputQuery}));
- },
- icon: Expensicons.Pencil,
- shouldShowRightIcon: false,
- shouldShowRightComponent: false,
- shouldCallAfterModalHide: true,
- },
- {
- text: translateLocal('common.delete'),
- onSelected: () => showDeleteModal(hash),
- icon: Expensicons.Trashcan,
- shouldShowRightIcon: false,
- shouldShowRightComponent: false,
- shouldCallAfterModalHide: true,
- shouldCloseAllModals: true,
- },
- ];
+ // Parse the query to fill all default query fields with values
+ const normalizedQueryJSON = buildSearchQueryJSON(queryString);
+ return buildSearchQueryString(normalizedQueryJSON);
}
/**
- * @private
- * Given a filter name and its value, this function will try to find the corresponding ID.
+ * Returns whether a given search query is a Canned query.
+ *
+ * Canned queries are simple predefined queries, that are defined only using type and status and no additional filters.
+ * In addition, they can contain an optional policyID.
+ * For example: "type:trip status:all" is a canned query.
*/
-function findIDFromDisplayValue(filterName: ValueOf, filter: string | string[], cardList: OnyxTypes.CardList, taxRates: Record) {
- if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
- if (typeof filter === 'string') {
- const email = filter;
- return PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? filter;
- }
- const emails = filter;
- return emails.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email);
- }
- if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) {
- const names = Array.isArray(filter) ? filter : ([filter] as string[]);
- return names.map((name) => taxRates[name] ?? name).flat();
- }
- if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) {
- if (typeof filter === 'string') {
- const bank = filter;
- const ids =
- Object.values(cardList)
- .filter((card) => card.bank === bank)
- .map((card) => card.cardID.toString()) ?? filter;
- return ids.length > 0 ? ids : bank;
- }
- const banks = filter;
- return banks
- .map(
- (bank) =>
- Object.values(cardList)
- .filter((card) => card.bank === bank)
- .map((card) => card.cardID.toString()) ?? bank,
- )
- .flat();
- }
- return filter;
+function isCannedSearchQuery(queryJSON: SearchQueryJSON) {
+ return !queryJSON.filters;
}
/**
@@ -981,46 +615,14 @@ function standardizeQueryJSON(queryJSON: SearchQueryJSON, cardList: OnyxTypes.Ca
return standardQuery;
}
-/**
- * Returns whether a given search query is a Canned query.
- *
- * Canned queries are simple predefined queries, that are defined only using type and status and no additional filters.
- * For example: "type:trip status:all" is a canned query.
- */
-function isCannedSearchQuery(queryJSON: SearchQueryJSON) {
- return !queryJSON.filters;
-}
-
-function getContextualSuggestionQuery(reportID: string) {
- return `type:chat in:${reportID}`;
-}
-
-function isCorrectSearchUserName(displayName?: string) {
- return displayName && displayName.toUpperCase() !== CONST.REPORT.OWNER_EMAIL_FAKE;
-}
-
export {
- getContextualSuggestionQuery,
- buildQueryStringFromFilterFormValues,
buildSearchQueryJSON,
buildSearchQueryString,
+ buildUserReadableQueryString,
+ buildQueryStringFromFilterFormValues,
buildFilterFormValuesFromQuery,
getPolicyIDFromSearchQuery,
- getListItem,
- getSections,
- getShouldShowMerchant,
- getSortedSections,
- isReportListItemType,
- isSearchResultsEmpty,
- isTransactionListItemType,
- isReportActionListItemType,
- getSearchHeaderTitle,
- normalizeQuery,
- shouldShowYear,
buildCannedSearchQuery,
isCannedSearchQuery,
- getExpenseTypeTranslationKey,
- getOverflowMenu,
- isCorrectSearchUserName,
standardizeQueryJSON,
};
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
new file mode 100644
index 000000000000..a7ce065a6d23
--- /dev/null
+++ b/src/libs/SearchUIUtils.ts
@@ -0,0 +1,524 @@
+import type {ValueOf} from 'type-fest';
+import type {SearchColumnType, SearchStatus, SortOrder} from '@components/Search/types';
+import ChatListItem from '@components/SelectionList/ChatListItem';
+import ReportListItem from '@components/SelectionList/Search/ReportListItem';
+import TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
+import type {ListItem, ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
+import * as Expensicons from '@src/components/Icon/Expensicons';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type SearchResults from '@src/types/onyx/SearchResults';
+import type {ListItemDataType, ListItemType, SearchDataTypes, SearchPersonalDetails, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
+import * as CurrencyUtils from './CurrencyUtils';
+import DateUtils from './DateUtils';
+import {translateLocal} from './Localize';
+import Navigation from './Navigation/Navigation';
+import * as ReportActionsUtils from './ReportActionsUtils';
+import * as TransactionUtils from './TransactionUtils';
+
+const columnNamesToSortingProperty = {
+ [CONST.SEARCH.TABLE_COLUMNS.TO]: 'formattedTo' as const,
+ [CONST.SEARCH.TABLE_COLUMNS.FROM]: 'formattedFrom' as const,
+ [CONST.SEARCH.TABLE_COLUMNS.DATE]: 'date' as const,
+ [CONST.SEARCH.TABLE_COLUMNS.TAG]: 'tag' as const,
+ [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]: '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,
+ [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: null,
+};
+
+const emptyPersonalDetails = {
+ accountID: CONST.REPORT.OWNER_ACCOUNT_ID_FAKE,
+ avatar: '',
+ displayName: undefined,
+ login: undefined,
+};
+
+type ReportKey = `${typeof ONYXKEYS.COLLECTION.REPORT}${string}`;
+
+type TransactionKey = `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`;
+
+type ReportActionKey = `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`;
+
+/**
+ * @private
+ *
+ * Returns a list of properties that are common to every Search ListItem
+ */
+function getTransactionItemCommonFormattedProperties(
+ transactionItem: SearchTransaction,
+ from: SearchPersonalDetails,
+ to: SearchPersonalDetails,
+): Pick {
+ const isExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE;
+
+ const formattedFrom = from?.displayName ?? from?.login ?? '';
+ const formattedTo = to?.displayName ?? to?.login ?? '';
+ const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport);
+ const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created;
+ const merchant = TransactionUtils.getMerchant(transactionItem);
+ const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant;
+
+ return {
+ formattedFrom,
+ formattedTo,
+ date,
+ formattedTotal,
+ formattedMerchant,
+ };
+}
+
+/**
+ * @private
+ */
+function isReportEntry(key: string): key is ReportKey {
+ return key.startsWith(ONYXKEYS.COLLECTION.REPORT);
+}
+
+/**
+ * @private
+ */
+function isTransactionEntry(key: string): key is TransactionKey {
+ return key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION);
+}
+
+/**
+ * @private
+ */
+function isReportActionEntry(key: string): key is ReportActionKey {
+ return key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
+}
+
+/**
+ * Determines whether to display the merchant field based on the transactions in the search results.
+ */
+function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean {
+ return Object.keys(data).some((key) => {
+ if (isTransactionEntry(key)) {
+ const item = data[key];
+ const merchant = item.modifiedMerchant ? item.modifiedMerchant : item.merchant ?? '';
+ return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT;
+ }
+ return false;
+ });
+}
+
+/**
+ * Type guard that checks if something is a ReportListItemType
+ */
+function isReportListItemType(item: ListItem): item is ReportListItemType {
+ return 'transactions' in item;
+}
+
+/**
+ * Type guard that checks if something is a TransactionListItemType
+ */
+function isTransactionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is TransactionListItemType {
+ const transactionListItem = item as TransactionListItemType;
+ return transactionListItem.transactionID !== undefined;
+}
+
+/**
+ * Type guard that checks if something is a ReportActionListItemType
+ */
+function isReportActionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is ReportActionListItemType {
+ const reportActionListItem = item as ReportActionListItemType;
+ return reportActionListItem.reportActionID !== undefined;
+}
+
+/**
+ * Checks if the date of transactions or reports indicate the need to display the year because they are from a past year.
+ */
+function shouldShowYear(data: TransactionListItemType[] | ReportListItemType[] | OnyxTypes.SearchResults['data']) {
+ const currentYear = new Date().getFullYear();
+
+ if (Array.isArray(data)) {
+ return data.some((item: TransactionListItemType | ReportListItemType) => {
+ if (isReportListItemType(item)) {
+ // If the item is a ReportListItemType, iterate over its transactions and check them
+ return item.transactions.some((transaction) => {
+ const transactionYear = new Date(TransactionUtils.getCreated(transaction)).getFullYear();
+ return transactionYear !== currentYear;
+ });
+ }
+
+ const createdYear = new Date(item?.modifiedCreated ? item.modifiedCreated : item?.created || '').getFullYear();
+ return createdYear !== currentYear;
+ });
+ }
+
+ for (const key in data) {
+ if (isTransactionEntry(key)) {
+ const item = data[key];
+ const date = TransactionUtils.getCreated(item);
+
+ if (DateUtils.doesDateBelongToAPastYear(date)) {
+ return true;
+ }
+ } else if (isReportActionEntry(key)) {
+ const item = data[key];
+ for (const action of Object.values(item)) {
+ const date = action.created;
+
+ if (DateUtils.doesDateBelongToAPastYear(date)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * @private
+ * Generates a display name for IOU reports considering the personal details of the payer and the transaction details.
+ */
+function getIOUReportName(data: OnyxTypes.SearchResults['data'], reportItem: SearchReport) {
+ const payerPersonalDetails = reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails;
+ const payerName = payerPersonalDetails?.displayName ?? payerPersonalDetails?.login ?? translateLocal('common.hidden');
+ const formattedAmount = CurrencyUtils.convertToDisplayString(reportItem.total ?? 0, reportItem.currency ?? CONST.CURRENCY.USD);
+ if (reportItem.action === CONST.SEARCH.ACTION_TYPES.VIEW) {
+ return translateLocal('iou.payerOwesAmount', {
+ payer: payerName,
+ amount: formattedAmount,
+ });
+ }
+
+ if (reportItem.action === CONST.SEARCH.ACTION_TYPES.PAID) {
+ return translateLocal('iou.payerPaidAmount', {
+ payer: payerName,
+ amount: formattedAmount,
+ });
+ }
+
+ return reportItem.reportName;
+}
+
+/**
+ * @private
+ * Organizes data into List Sections for display, for the TransactionListItemType of Search Results.
+ *
+ * Do not use directly, use only via `getSections()` facade.
+ */
+function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']): TransactionListItemType[] {
+ const shouldShowMerchant = getShouldShowMerchant(data);
+
+ const doesDataContainAPastYearTransaction = shouldShowYear(data);
+
+ return Object.keys(data)
+ .filter(isTransactionEntry)
+ .map((key) => {
+ const transactionItem = data[key];
+ const from = data.personalDetailsList?.[transactionItem.accountID];
+ const to = transactionItem.managerID ? data.personalDetailsList?.[transactionItem.managerID] : emptyPersonalDetails;
+
+ const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties(transactionItem, from, to);
+
+ return {
+ ...transactionItem,
+ from,
+ to,
+ formattedFrom,
+ formattedTo,
+ formattedTotal,
+ formattedMerchant,
+ date,
+ shouldShowMerchant,
+ shouldShowCategory: metadata?.columnsToShow?.shouldShowCategoryColumn,
+ shouldShowTag: metadata?.columnsToShow?.shouldShowTagColumn,
+ shouldShowTax: metadata?.columnsToShow?.shouldShowTaxColumn,
+ keyForList: transactionItem.transactionID,
+ shouldShowYear: doesDataContainAPastYearTransaction,
+ };
+ });
+}
+
+/**
+ * @private
+ * Organizes data into List Sections for display, for the ReportActionListItemType of Search Results.
+ *
+ * Do not use directly, use only via `getSections()` facade.
+ */
+function getReportActionsSections(data: OnyxTypes.SearchResults['data']): ReportActionListItemType[] {
+ const reportActionItems: ReportActionListItemType[] = [];
+ for (const key in data) {
+ if (isReportActionEntry(key)) {
+ const reportActions = data[key];
+ for (const reportAction of Object.values(reportActions)) {
+ const from = data.personalDetailsList?.[reportAction.accountID];
+ if (ReportActionsUtils.isDeletedAction(reportAction)) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+ reportActionItems.push({
+ ...reportAction,
+ from,
+ formattedFrom: from?.displayName ?? from?.login ?? '',
+ date: reportAction.created,
+ keyForList: reportAction.reportActionID,
+ });
+ }
+ }
+ }
+ return reportActionItems;
+}
+
+/**
+ * @private
+ * Organizes data into List Sections for display, for the ReportListItemType of Search Results.
+ *
+ * Do not use directly, use only via `getSections()` facade.
+ */
+function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']): ReportListItemType[] {
+ const shouldShowMerchant = getShouldShowMerchant(data);
+
+ const doesDataContainAPastYearTransaction = shouldShowYear(data);
+
+ const reportIDToTransactions: Record = {};
+ for (const key in data) {
+ if (isReportEntry(key)) {
+ const reportItem = {...data[key]};
+ const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`;
+ const transactions = reportIDToTransactions[reportKey]?.transactions ?? [];
+ const isIOUReport = reportItem.type === CONST.REPORT.TYPE.IOU;
+
+ reportIDToTransactions[reportKey] = {
+ ...reportItem,
+ keyForList: reportItem.reportID,
+ from: data.personalDetailsList?.[reportItem.accountID ?? -1],
+ to: reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails,
+ transactions,
+ reportName: isIOUReport ? getIOUReportName(data, reportItem) : reportItem.reportName,
+ };
+ } else if (isTransactionEntry(key)) {
+ const transactionItem = {...data[key]};
+ const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`;
+
+ const from = data.personalDetailsList?.[transactionItem.accountID];
+ const to = transactionItem.managerID ? data.personalDetailsList?.[transactionItem.managerID] : emptyPersonalDetails;
+
+ const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties(transactionItem, from, to);
+
+ const transaction = {
+ ...transactionItem,
+ from,
+ to,
+ formattedFrom,
+ formattedTo,
+ formattedTotal,
+ formattedMerchant,
+ date,
+ shouldShowMerchant,
+ shouldShowCategory: metadata?.columnsToShow?.shouldShowCategoryColumn,
+ shouldShowTag: metadata?.columnsToShow?.shouldShowTagColumn,
+ shouldShowTax: metadata?.columnsToShow?.shouldShowTaxColumn,
+ keyForList: transactionItem.transactionID,
+ shouldShowYear: doesDataContainAPastYearTransaction,
+ };
+ if (reportIDToTransactions[reportKey]?.transactions) {
+ reportIDToTransactions[reportKey].transactions.push(transaction);
+ } else if (reportIDToTransactions[reportKey]) {
+ reportIDToTransactions[reportKey].transactions = [transaction];
+ }
+ }
+ }
+
+ return Object.values(reportIDToTransactions);
+}
+
+/**
+ * Returns the appropriate list item component based on the type and status of the search data.
+ */
+function getListItem(type: SearchDataTypes, status: SearchStatus): ListItemType {
+ if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
+ return ChatListItem;
+ }
+ if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
+ return TransactionListItem;
+ }
+ return ReportListItem;
+}
+
+/**
+ * Organizes data into appropriate list sections for display based on the type of search results.
+ */
+function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) {
+ if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
+ return getReportActionsSections(data);
+ }
+ if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
+ return getTransactionsSections(data, metadata);
+ }
+ return getReportSections(data, metadata);
+}
+
+/**
+ * Sorts sections of data based on a specified column and sort order for displaying sorted results.
+ */
+function getSortedSections(type: SearchDataTypes, status: SearchStatus, data: ListItemDataType, sortBy?: SearchColumnType, sortOrder?: SortOrder) {
+ if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
+ return getSortedReportActionData(data as ReportActionListItemType[]);
+ }
+ if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
+ return getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder);
+ }
+ return getSortedReportData(data as ReportListItemType[]);
+}
+
+/**
+ * @private
+ * Sorts transaction sections based on a specified column and sort order.
+ */
+function getSortedTransactionData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) {
+ if (!sortBy || !sortOrder) {
+ return data;
+ }
+
+ const sortingProperty = columnNamesToSortingProperty[sortBy];
+
+ if (!sortingProperty) {
+ return data;
+ }
+
+ return data.sort((a, b) => {
+ const aValue = sortingProperty === 'comment' ? a.comment?.comment : a[sortingProperty];
+ const bValue = sortingProperty === 'comment' ? b.comment?.comment : b[sortingProperty];
+
+ if (aValue === undefined || bValue === undefined) {
+ return 0;
+ }
+
+ // We are guaranteed that both a and b will be string or number at the same time
+ if (typeof aValue === 'string' && typeof bValue === 'string') {
+ return sortOrder === CONST.SEARCH.SORT_ORDER.ASC ? aValue.toLowerCase().localeCompare(bValue) : bValue.toLowerCase().localeCompare(aValue);
+ }
+
+ const aNum = aValue as number;
+ const bNum = bValue as number;
+
+ return sortOrder === CONST.SEARCH.SORT_ORDER.ASC ? aNum - bNum : bNum - aNum;
+ });
+}
+
+/**
+ * @private
+ * Determines the date of the newest transaction within a report for sorting purposes.
+ */
+function getReportNewestTransactionDate(report: ReportListItemType) {
+ return report.transactions?.reduce((max, curr) => (curr.modifiedCreated ?? curr.created > (max?.created ?? '') ? curr : max), report.transactions.at(0))?.created;
+}
+
+/**
+ * @private
+ * Sorts report sections based on a specified column and sort order.
+ */
+function getSortedReportData(data: ReportListItemType[]) {
+ return data.sort((a, b) => {
+ const aNewestTransaction = getReportNewestTransactionDate(a);
+ const bNewestTransaction = getReportNewestTransactionDate(b);
+
+ if (!aNewestTransaction || !bNewestTransaction) {
+ return 0;
+ }
+
+ return bNewestTransaction.toLowerCase().localeCompare(aNewestTransaction);
+ });
+}
+
+/**
+ * @private
+ * Sorts report actions sections based on a specified column and sort order.
+ */
+function getSortedReportActionData(data: ReportActionListItemType[]) {
+ return data.sort((a, b) => {
+ const aValue = a?.created;
+ const bValue = b?.created;
+
+ if (aValue === undefined || bValue === undefined) {
+ return 0;
+ }
+
+ return bValue.toLowerCase().localeCompare(aValue);
+ });
+}
+
+/**
+ * Checks if the search results contain any data, useful for determining if the search results are empty.
+ */
+function isSearchResultsEmpty(searchResults: SearchResults) {
+ return !Object.keys(searchResults?.data).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION));
+}
+
+/**
+ * Returns the corresponding translation key for expense type
+ */
+function getExpenseTypeTranslationKey(expenseType: ValueOf): TranslationPaths {
+ // eslint-disable-next-line default-case
+ switch (expenseType) {
+ case CONST.SEARCH.TRANSACTION_TYPE.DISTANCE:
+ return 'common.distance';
+ case CONST.SEARCH.TRANSACTION_TYPE.CARD:
+ return 'common.card';
+ case CONST.SEARCH.TRANSACTION_TYPE.CASH:
+ return 'iou.cash';
+ }
+}
+
+/**
+ * Constructs and configures the overflow menu for search items, handling interactions such as renaming or deleting items.
+ */
+function getOverflowMenu(itemName: string, hash: number, inputQuery: string, showDeleteModal: (hash: number) => void, isMobileMenu?: boolean, closeMenu?: () => void) {
+ return [
+ {
+ text: translateLocal('common.rename'),
+ onSelected: () => {
+ if (isMobileMenu && closeMenu) {
+ closeMenu();
+ }
+ Navigation.navigate(ROUTES.SEARCH_SAVED_SEARCH_RENAME.getRoute({name: encodeURIComponent(itemName), jsonQuery: inputQuery}));
+ },
+ icon: Expensicons.Pencil,
+ shouldShowRightIcon: false,
+ shouldShowRightComponent: false,
+ shouldCallAfterModalHide: true,
+ },
+ {
+ text: translateLocal('common.delete'),
+ onSelected: () => showDeleteModal(hash),
+ icon: Expensicons.Trashcan,
+ shouldShowRightIcon: false,
+ shouldShowRightComponent: false,
+ shouldCallAfterModalHide: true,
+ shouldCloseAllModals: true,
+ },
+ ];
+}
+
+/**
+ * Checks if the passed username is a correct standard username, and not a placeholder
+ */
+function isCorrectSearchUserName(displayName?: string) {
+ return displayName && displayName.toUpperCase() !== CONST.REPORT.OWNER_EMAIL_FAKE;
+}
+
+export {
+ getListItem,
+ getSections,
+ getShouldShowMerchant,
+ getSortedSections,
+ isReportListItemType,
+ isSearchResultsEmpty,
+ isTransactionListItemType,
+ isReportActionListItemType,
+ shouldShowYear,
+ getExpenseTypeTranslationKey,
+ getOverflowMenu,
+ isCorrectSearchUserName,
+};
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index a9cb79923a59..7be24d4ee691 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -385,6 +385,7 @@ function getOptionData({
result.hasOutstandingChildTask = report.hasOutstandingChildTask;
result.hasParentAccess = report.hasParentAccess;
result.isConciergeChat = ReportUtils.isConciergeChatReport(report);
+ result.participants = report.participants;
const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || ReportUtils.isExpenseReport(report);
const subtitle = ReportUtils.getChatRoomSubtitle(report);
diff --git a/src/libs/SuffixUkkonenTree/index.ts b/src/libs/SuffixUkkonenTree/index.ts
new file mode 100644
index 000000000000..bcefd1008493
--- /dev/null
+++ b/src/libs/SuffixUkkonenTree/index.ts
@@ -0,0 +1,211 @@
+/* eslint-disable rulesdir/prefer-at */
+// .at() has a performance overhead we explicitly want to avoid here
+
+/* eslint-disable no-continue */
+import {ALPHABET_SIZE, DELIMITER_CHAR_CODE, END_CHAR_CODE, SPECIAL_CHAR_CODE, stringToNumeric} from './utils';
+
+/**
+ * This implements a suffix tree using Ukkonen's algorithm.
+ * A good visualization to learn about the algorithm can be found here: https://brenden.github.io/ukkonen-animation/
+ * A good video explaining Ukkonen's algorithm can be found here: https://www.youtube.com/watch?v=ALEV0Hc5dDk
+ * Note: This implementation is optimized for performance, not necessarily for readability.
+ *
+ * You probably don't want to use this directly, but rather use @libs/FastSearch.ts as a easy to use wrapper around this.
+ */
+
+/**
+ * Creates a new tree instance that can be used to build a suffix tree and search in it.
+ * The input is a numeric representation of the search string, which can be created using {@link stringToNumeric}.
+ * Separate search values must be separated by the {@link DELIMITER_CHAR_CODE}. The search string must end with the {@link END_CHAR_CODE}.
+ *
+ * The tree will be built using the Ukkonen's algorithm: https://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf
+ */
+function makeTree(numericSearchValues: Uint8Array) {
+ // Every leaf represents a suffix. There can't be more than n suffixes.
+ // Every internal node has to have at least 2 children. So the total size of ukkonen tree is not bigger than 2n - 1.
+ // + 1 is because an extra character at the beginning to offset the 1-based indexing.
+ const maxNodes = 2 * numericSearchValues.length + 1;
+ /*
+ This array represents all internal nodes in the suffix tree.
+ When building this tree, we'll be given a character in the string, and we need to be able to lookup in constant time
+ if there's any edge connected to a node starting with that character. For example, given a tree like this:
+
+ root
+ / | \
+ a b c
+
+ and the next character in our string is 'd', we need to be able do check if any of the edges from the root node
+ start with the letter 'd', without looping through all the edges.
+
+ To accomplish this, each node gets an array matching the alphabet size.
+ So you can imagine if our alphabet was just [a,b,c,d], then each node would get an array like [0,0,0,0].
+ If we add an edge starting with 'a', then the root node would be [1,0,0,0]
+ So given an arbitrary letter such as 'd', then we can take the position of that letter in its alphabet (position 3 in our example)
+ and check whether that index in the array is 0 or 1. If it's a 1, then there's an edge starting with the letter 'd'.
+
+ Note that for efficiency, all nodes are stored in a single flat array. That's how we end up with (maxNodes * alphabet_size).
+ In the example of a 4-character alphabet, we'd have an array like this:
+
+ root root.left root.right last possible node
+ / \ / \ / \ / \
+ [0,0,0,0, 0,0,0,0, 0,0,0,0, ................. 0,0,0,0]
+ */
+ const transitionNodes = new Uint32Array(maxNodes * ALPHABET_SIZE);
+
+ // Storing the range of the original string that each node represents:
+ const rangeStart = new Uint32Array(maxNodes);
+ const rangeEnd = new Uint32Array(maxNodes);
+
+ const parent = new Uint32Array(maxNodes);
+ const suffixLink = new Uint32Array(maxNodes);
+
+ let currentNode = 1;
+ let currentPosition = 1;
+ let nodeCounter = 3;
+ let currentIndex = 1;
+
+ function initializeTree() {
+ rangeEnd.fill(numericSearchValues.length);
+ rangeEnd[1] = 0;
+ rangeEnd[2] = 0;
+ suffixLink[1] = 2;
+ for (let i = 0; i < ALPHABET_SIZE; ++i) {
+ transitionNodes[ALPHABET_SIZE * 2 + i] = 1;
+ }
+ }
+
+ function processCharacter(char: number) {
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (rangeEnd[currentNode] < currentPosition) {
+ if (transitionNodes[currentNode * ALPHABET_SIZE + char] === 0) {
+ createNewLeaf(char);
+ continue;
+ }
+ currentNode = transitionNodes[currentNode * ALPHABET_SIZE + char];
+ currentPosition = rangeStart[currentNode];
+ }
+ if (currentPosition === 0 || char === numericSearchValues[currentPosition]) {
+ currentPosition++;
+ } else {
+ splitEdge(char);
+ continue;
+ }
+ break;
+ }
+ }
+
+ function createNewLeaf(c: number) {
+ transitionNodes[currentNode * ALPHABET_SIZE + c] = nodeCounter;
+ rangeStart[nodeCounter] = currentIndex;
+ parent[nodeCounter++] = currentNode;
+ currentNode = suffixLink[currentNode];
+
+ currentPosition = rangeEnd[currentNode] + 1;
+ }
+
+ function splitEdge(c: number) {
+ rangeStart[nodeCounter] = rangeStart[currentNode];
+ rangeEnd[nodeCounter] = currentPosition - 1;
+ parent[nodeCounter] = parent[currentNode];
+
+ transitionNodes[nodeCounter * ALPHABET_SIZE + numericSearchValues[currentPosition]] = currentNode;
+ transitionNodes[nodeCounter * ALPHABET_SIZE + c] = nodeCounter + 1;
+ rangeStart[nodeCounter + 1] = currentIndex;
+ parent[nodeCounter + 1] = nodeCounter;
+ rangeStart[currentNode] = currentPosition;
+ parent[currentNode] = nodeCounter;
+
+ transitionNodes[parent[nodeCounter] * ALPHABET_SIZE + numericSearchValues[rangeStart[nodeCounter]]] = nodeCounter;
+ nodeCounter += 2;
+ handleDescent(nodeCounter);
+ }
+
+ function handleDescent(latestNodeIndex: number) {
+ currentNode = suffixLink[parent[latestNodeIndex - 2]];
+ currentPosition = rangeStart[latestNodeIndex - 2];
+ while (currentPosition <= rangeEnd[latestNodeIndex - 2]) {
+ currentNode = transitionNodes[currentNode * ALPHABET_SIZE + numericSearchValues[currentPosition]];
+ currentPosition += rangeEnd[currentNode] - rangeStart[currentNode] + 1;
+ }
+ if (currentPosition === rangeEnd[latestNodeIndex - 2] + 1) {
+ suffixLink[latestNodeIndex - 2] = currentNode;
+ } else {
+ suffixLink[latestNodeIndex - 2] = latestNodeIndex;
+ }
+ currentPosition = rangeEnd[currentNode] - (currentPosition - rangeEnd[latestNodeIndex - 2]) + 2;
+ }
+
+ function build() {
+ initializeTree();
+ for (currentIndex = 1; currentIndex < numericSearchValues.length; ++currentIndex) {
+ const c = numericSearchValues[currentIndex];
+ processCharacter(c);
+ }
+ }
+
+ /**
+ * Returns all occurrences of the given (sub)string in the input string.
+ *
+ * You can think of the tree that we create as a big string that looks like this:
+ *
+ * "banana$pancake$apple|"
+ * The example delimiter character '$' is used to separate the different strings.
+ * The end character '|' is used to indicate the end of our search string.
+ *
+ * This function will return the index(es) of found occurrences within this big string.
+ * So, when searching for "an", it would return [1, 3, 8].
+ */
+ function findSubstring(searchValue: number[]) {
+ const occurrences: number[] = [];
+
+ function dfs(node: number, depth: number) {
+ const leftRange = rangeStart[node];
+ const rightRange = rangeEnd[node];
+ const rangeLen = node === 1 ? 0 : rightRange - leftRange + 1;
+
+ for (let i = 0; i < rangeLen && depth + i < searchValue.length && leftRange + i < numericSearchValues.length; i++) {
+ if (searchValue[depth + i] !== numericSearchValues[leftRange + i]) {
+ return;
+ }
+ }
+
+ let isLeaf = true;
+ for (let i = 0; i < ALPHABET_SIZE; ++i) {
+ const tNode = transitionNodes[node * ALPHABET_SIZE + i];
+
+ // Search speed optimization: don't go through the edge if it's different than the next char:
+ const correctChar = depth + rangeLen >= searchValue.length || i === searchValue[depth + rangeLen];
+
+ if (tNode !== 0 && tNode !== 1 && correctChar) {
+ isLeaf = false;
+ dfs(tNode, depth + rangeLen);
+ }
+ }
+
+ if (isLeaf && depth + rangeLen >= searchValue.length) {
+ occurrences.push(numericSearchValues.length - (depth + rangeLen) + 1);
+ }
+ }
+
+ dfs(1, 0);
+ return occurrences;
+ }
+
+ return {
+ build,
+ findSubstring,
+ };
+}
+
+const SuffixUkkonenTree = {
+ makeTree,
+
+ // Re-exported from utils:
+ DELIMITER_CHAR_CODE,
+ SPECIAL_CHAR_CODE,
+ END_CHAR_CODE,
+ stringToNumeric,
+};
+
+export default SuffixUkkonenTree;
diff --git a/src/libs/SuffixUkkonenTree/utils.ts b/src/libs/SuffixUkkonenTree/utils.ts
new file mode 100644
index 000000000000..96ee35b15796
--- /dev/null
+++ b/src/libs/SuffixUkkonenTree/utils.ts
@@ -0,0 +1,115 @@
+/* eslint-disable rulesdir/prefer-at */ // .at() has a performance overhead we explicitly want to avoid here
+/* eslint-disable no-continue */
+
+const CHAR_CODE_A = 'a'.charCodeAt(0);
+const ALPHABET = 'abcdefghijklmnopqrstuvwxyz';
+const LETTER_ALPHABET_SIZE = ALPHABET.length;
+const ALPHABET_SIZE = LETTER_ALPHABET_SIZE + 3; // +3: special char, delimiter char, end char
+const SPECIAL_CHAR_CODE = ALPHABET_SIZE - 3;
+const DELIMITER_CHAR_CODE = ALPHABET_SIZE - 2;
+const END_CHAR_CODE = ALPHABET_SIZE - 1;
+
+// Store the results for a char code in a lookup table to avoid recalculating the same values (performance optimization)
+const base26LookupTable = new Array();
+
+/**
+ * Converts a number to a base26 representation.
+ */
+function convertToBase26(num: number): number[] {
+ if (base26LookupTable[num]) {
+ return base26LookupTable[num];
+ }
+ if (num < 0) {
+ throw new Error('convertToBase26: Input must be a non-negative integer');
+ }
+
+ const result: number[] = [];
+
+ do {
+ // eslint-disable-next-line no-param-reassign
+ num--;
+ result.unshift(num % 26);
+ // eslint-disable-next-line no-bitwise, no-param-reassign
+ num >>= 5; // Equivalent to Math.floor(num / 26), but faster
+ } while (num > 0);
+
+ base26LookupTable[num] = result;
+ return result;
+}
+
+/**
+ * Converts a string to an array of numbers representing the characters of the string.
+ * Every number in the array is in the range [0, ALPHABET_SIZE-1] (0-28).
+ *
+ * The numbers are offset by the character code of 'a' (97).
+ * - This is so that the numbers from a-z are in the range 0-28.
+ * - 26 is for encoding special characters. Character numbers that are not within the range of a-z will be encoded as "specialCharacter + base26(charCode)"
+ * - 27 is for the delimiter character
+ * - 28 is for the end character
+ *
+ * Note: The string should be converted to lowercase first (otherwise uppercase letters get base26'ed taking more space than necessary).
+ */
+function stringToNumeric(
+ // The string we want to convert to a numeric representation
+ input: string,
+ options?: {
+ // A set of characters that should be skipped and not included in the numeric representation
+ charSetToSkip?: Set;
+ // When out is provided, the function will write the result to the provided arrays instead of creating new ones (performance)
+ out?: {
+ outArray: Uint8Array;
+ // As outArray is a ArrayBuffer we need to keep track of the current offset
+ offset: {value: number};
+ // A map of to map the found occurrences to the correct data set
+ // As the search string can be very long for high traffic accounts (500k+), this has to be big enough, thus its a Uint32Array
+ outOccurrenceToIndex?: Uint32Array;
+ // The index that will be used in the outOccurrenceToIndex array (this is the index of your original data position)
+ index?: number;
+ };
+ // By default false. By default the outArray may be larger than necessary. If clamp is set to true the outArray will be clamped to the actual size.
+ clamp?: boolean;
+ },
+): {
+ numeric: Uint8Array;
+ occurrenceToIndex: Uint32Array;
+ offset: {value: number};
+} {
+ // The out array might be longer than our input string length, because we encode special characters as multiple numbers using the base26 encoding.
+ // * 6 is because the upper limit of encoding any char in UTF-8 to base26 is at max 6 numbers.
+ const outArray = options?.out?.outArray ?? new Uint8Array(input.length * 6);
+ const offset = options?.out?.offset ?? {value: 0};
+ const occurrenceToIndex = options?.out?.outOccurrenceToIndex ?? new Uint32Array(input.length * 16 * 4);
+ const index = options?.out?.index ?? 0;
+
+ for (let i = 0; i < input.length; i++) {
+ const char = input[i];
+
+ if (options?.charSetToSkip?.has(char)) {
+ continue;
+ }
+
+ if (char >= 'a' && char <= 'z') {
+ // char is an alphabet character
+ occurrenceToIndex[offset.value] = index;
+ outArray[offset.value++] = char.charCodeAt(0) - CHAR_CODE_A;
+ } else {
+ const charCode = input.charCodeAt(i);
+ occurrenceToIndex[offset.value] = index;
+ outArray[offset.value++] = SPECIAL_CHAR_CODE;
+ const asBase26Numeric = convertToBase26(charCode);
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
+ for (let j = 0; j < asBase26Numeric.length; j++) {
+ occurrenceToIndex[offset.value] = index;
+ outArray[offset.value++] = asBase26Numeric[j];
+ }
+ }
+ }
+
+ return {
+ numeric: options?.clamp ? outArray.slice(0, offset.value) : outArray,
+ occurrenceToIndex,
+ offset,
+ };
+}
+
+export {stringToNumeric, ALPHABET, ALPHABET_SIZE, SPECIAL_CHAR_CODE, DELIMITER_CHAR_CODE, END_CHAR_CODE};
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 7bc4b7ca8c24..bf5073aba27c 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -12,7 +12,7 @@ import {toLocaleDigit} from '@libs/LocaleDigitUtils';
import * as Localize from '@libs/Localize';
import * as NumberUtils from '@libs/NumberUtils';
import Permissions from '@libs/Permissions';
-import {getCleanedTagName, getCustomUnitRate} from '@libs/PolicyUtils';
+import {getCleanedTagName, getDistanceRateCustomUnitRate} from '@libs/PolicyUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
// eslint-disable-next-line import/no-cycle
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -20,8 +20,10 @@ import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import type {IOURequestType} from '@userActions/IOU';
import CONST from '@src/CONST';
+import type {IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
+import type {Attendee} from '@src/types/onyx/IOU';
import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -126,6 +128,7 @@ function buildOptimisticTransaction(
currency: string,
reportID: string,
comment = '',
+ attendees: Attendee[] = [],
created = '',
source = '',
originalTransactionID = '',
@@ -179,6 +182,7 @@ function buildOptimisticTransaction(
taxAmount,
billable,
reimbursable,
+ attendees,
};
}
@@ -207,6 +211,13 @@ function isMerchantMissing(transaction: OnyxEntry) {
return isMerchantEmpty;
}
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function shouldShowAttendees(iouType: IOUType, policy: OnyxEntry): boolean {
+ return false;
+ // To be renabled once feature is complete: https://github.com/Expensify/App/issues/44725
+ // return iouType === CONST.IOU.TYPE.SUBMIT && !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM);
+}
+
/**
* Check if the merchant is partial i.e. `(none)`
*/
@@ -312,6 +323,10 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
updatedTransaction.tag = transactionChanges.tag;
}
+ if (Object.hasOwn(transactionChanges, 'attendees')) {
+ updatedTransaction.modifiedAttendees = transactionChanges?.attendees;
+ }
+
if (
shouldUpdateReceiptState &&
shouldStopSmartscan &&
@@ -335,6 +350,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'taxAmount') && {taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'taxCode') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
+ ...(Object.hasOwn(transactionChanges, 'attendees') && {attendees: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
};
return updatedTransaction;
@@ -440,6 +456,22 @@ function getMerchantOrDescription(transaction: OnyxEntry) {
return !isMerchantMissing(transaction) ? getMerchant(transaction) : getDescription(transaction);
}
+/**
+ * Return the list of modified attendees if present otherwise list of attendees
+ */
+function getAttendees(transaction: OnyxInputOrEntry): Attendee[] {
+ return transaction?.modifiedAttendees ? transaction.modifiedAttendees : transaction?.attendees ?? [];
+}
+
+/**
+ * Return the list of attendees as a string and modified list of attendees as a string if present.
+ */
+function getFormattedAttendees(modifiedAttendees?: Attendee[], attendees?: Attendee[]): [string, string] {
+ const oldAttendees = modifiedAttendees ?? [];
+ const newAttendees = attendees ?? [];
+ return [oldAttendees.map((item) => item.displayName ?? item.login).join(', '), newAttendees.map((item) => item.displayName ?? item.login).join(', ')];
+}
+
/**
* Return the reimbursable value. Defaults to true to match BE logic.
*/
@@ -903,7 +935,7 @@ function getRateID(transaction: OnyxInputOrEntry): string | undefin
function getDefaultTaxCode(policy: OnyxEntry, transaction: OnyxEntry, currency?: string | undefined): string | undefined {
if (isDistanceRequest(transaction)) {
const customUnitRateID = getRateID(transaction) ?? '';
- const customUnitRate = getCustomUnitRate(policy, customUnitRateID);
+ const customUnitRate = getDistanceRateCustomUnitRate(policy, customUnitRateID);
return customUnitRate?.attributes?.taxRateExternalID ?? '';
}
const defaultExternalID = policy?.taxRates?.defaultExternalID;
@@ -1140,6 +1172,7 @@ export {
isManualRequest,
isScanRequest,
getAmount,
+ getAttendees,
getTaxAmount,
getTaxCode,
getCurrency,
@@ -1147,6 +1180,7 @@ export {
getCardID,
getOriginalCurrency,
getOriginalAmount,
+ getFormattedAttendees,
getMerchant,
getMerchantOrDescription,
getMCCGroup,
@@ -1206,6 +1240,7 @@ export {
removeSettledAndApprovedTransactions,
getCardName,
hasReceiptSource,
+ shouldShowAttendees,
};
export type {TransactionChanges};
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index 98ea0cc4c677..b49a7be66b31 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -2,7 +2,7 @@ import reject from 'lodash/reject';
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
-import {getCustomUnitRate, getSortedTagKeys} from '@libs/PolicyUtils';
+import {getDistanceRateCustomUnitRate, getSortedTagKeys} from '@libs/PolicyUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -218,7 +218,7 @@ const ViolationsUtils = {
: getTagViolationsForMultiLevelTags(updatedTransaction, newTransactionViolations, policyTagList, hasDependentTags);
}
- if (updatedTransaction?.comment?.customUnit?.customUnitRateID && !!getCustomUnitRate(policy, updatedTransaction?.comment?.customUnit?.customUnitRateID)) {
+ if (updatedTransaction?.comment?.customUnit?.customUnitRateID && !!getDistanceRateCustomUnitRate(policy, updatedTransaction?.comment?.customUnit?.customUnitRateID)) {
newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY});
}
@@ -343,6 +343,11 @@ const ViolationsUtils = {
return violation.name as never;
}
},
+
+ // We have to use regex, because Violation limit is given in a inconvenient form: "$2,000.00"
+ getViolationAmountLimit(violation: TransactionViolation): number {
+ return Number(violation.data?.formattedLimit?.replace(CONST.VIOLATION_LIMIT_REGEX, ''));
+ },
};
export default ViolationsUtils;
diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts
index 67655e0f472b..3a43c0bc4eca 100644
--- a/src/libs/WorkflowUtils.ts
+++ b/src/libs/WorkflowUtils.ts
@@ -125,6 +125,7 @@ function convertPolicyEmployeesToApprovalWorkflows({employees, defaultApprover,
email,
avatar: personalDetailsByEmail[email]?.avatar,
displayName: personalDetailsByEmail[email]?.displayName ?? email,
+ pendingFields: employee.pendingFields,
};
if (!approvalWorkflows[submitsTo]) {
@@ -234,6 +235,9 @@ function convertApprovalWorkflowToPolicyEmployees({
email: approver.email,
forwardsTo,
pendingAction,
+ pendingFields: {
+ forwardsTo: pendingAction,
+ },
};
});
@@ -250,6 +254,9 @@ function convertApprovalWorkflowToPolicyEmployees({
...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}),
submitsTo,
pendingAction,
+ pendingFields: {
+ submitsTo: pendingAction,
+ },
};
});
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 77427b5f42cc..5a594a19e15a 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -28,7 +28,7 @@ import type {OnyxData} from '@src/types/onyx/Request';
import {setShouldForceOffline} from './Network';
import * as PersistedRequests from './PersistedRequests';
import * as Policy from './Policy/Policy';
-import resolveDuplicationConflictAction from './RequestConflictUtils';
+import {resolveDuplicationConflictAction} from './RequestConflictUtils';
import * as Session from './Session';
import Timing from './Timing';
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 0d9a4887bd1d..7ce9b9dfb272 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -61,7 +61,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
-import type {Participant, Split} from '@src/types/onyx/IOU';
+import type {Attendee, Participant, Split} from '@src/types/onyx/IOU';
import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type ReportAction from '@src/types/onyx/ReportAction';
@@ -359,6 +359,7 @@ function initMoneyRequest(
// Use set() here so that there is no way that data will be leaked between objects when it gets reset
Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${newTransactionID}`, {
amount: 0,
+ attendees: IOUUtils.formatCurrentUserToAttendee(currentUserPersonalDetails, reportID),
comment,
created,
currency,
@@ -426,6 +427,10 @@ function setMoneyRequestMerchant(transactionID: string, merchant: string, isDraf
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {merchant});
}
+function setMoneyRequestAttendees(transactionID: string, attendees: Attendee[], isDraft: boolean) {
+ Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {attendees});
+}
+
function setMoneyRequestPendingFields(transactionID: string, pendingFields: OnyxTypes.Transaction['pendingFields']) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields});
}
@@ -1910,6 +1915,7 @@ function getSendInvoiceInformation(
currency,
optimisticInvoiceReport.reportID,
trimmedComment,
+ [],
created,
'',
'',
@@ -2031,6 +2037,7 @@ function getMoneyRequestInformation(
payeeEmail = currentUserEmail,
moneyRequestReportID = '',
linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction,
+ attendees?: Attendee[],
existingTransaction: OnyxEntry | undefined = undefined,
): MoneyRequestInformation {
const payerEmail = PhoneNumber.addSMSDomainIfPhoneNumber(participant.login ?? '');
@@ -2090,6 +2097,7 @@ function getMoneyRequestInformation(
currency,
iouReport.reportID,
comment,
+ attendees,
created,
'',
'',
@@ -2330,6 +2338,7 @@ function getTrackExpenseInformation(
currency,
shouldUseMoneyReport && iouReport ? iouReport.reportID : '-1',
comment,
+ [],
created,
'',
'',
@@ -2459,10 +2468,7 @@ function calculateDiffAmount(
// Add the diff to the total if we change the currency from a different currency to the currency of the IOU report
return updatedAmount;
}
- if (updatedCurrency !== iouReport?.currency && currentCurrency === iouReport?.currency) {
- // Subtract the diff from the total if we change the currency from the currency of IOU report to a different currency
- return -updatedAmount;
- }
+
if (updatedCurrency === iouReport?.currency && updatedAmount !== currentAmount) {
// Calculate the diff between the updated amount and the current amount if we change the amount and the currency of the transaction is the currency of the report
return updatedAmount - currentAmount;
@@ -2492,6 +2498,7 @@ function getUpdateMoneyRequestParams(
policyTagList: OnyxTypes.OnyxInputOrEntry