diff --git a/.eslintignore b/.eslintignore
index 162cc816ea80..26ecb1ae7cc7 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -12,4 +12,5 @@ docs/assets/**
web/gtm.js
**/.expo/**
src/libs/SearchParser/searchParser.js
+src/libs/SearchParser/autocompleteParser.js
help/_scripts/**
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 4031d6c0c119..dc4de9ec31a7 100644
--- a/.github/workflows/cla.yml
+++ b/.github/workflows/cla.yml
@@ -8,35 +8,5 @@ on:
jobs:
CLA:
- runs-on: ubuntu-latest
- # This job only runs for pull request comments or pull request target events (not issue comments)
- # It does not run for pull requests created by OSBotify
- if: ${{ github.event.issue.pull_request || (github.event_name == 'pull_request_target' && github.event.pull_request.user.login != 'OSBotify' && github.event.pull_request.user.login != 'imgbot[bot]') }}
- steps:
- - name: CLA comment check
- uses: actions-ecosystem/action-regex-match@9c35fe9ac1840239939c59e5db8839422eed8a73
- id: sign
- with:
- text: ${{ github.event.comment.body }}
- regex: '\s*I have read the CLA Document and I hereby sign the CLA\s*'
- - name: CLA comment re-check
- uses: actions-ecosystem/action-regex-match@9c35fe9ac1840239939c59e5db8839422eed8a73
- id: recheck
- with:
- text: ${{ github.event.comment.body }}
- regex: '\s*recheck\s*'
- - name: CLA Assistant
- if: ${{ steps.recheck.outputs.match != '' || steps.sign.outputs.match != '' || github.event_name == 'pull_request_target' }}
- # Version: 2.1.2-beta
- uses: cla-assistant/github-action@948230deb0d44dd38957592f08c6bd934d96d0cf
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- PERSONAL_ACCESS_TOKEN : ${{ secrets.CLA_BOTIFY_TOKEN }}
- with:
- path-to-signatures: '${{ github.repository }}/cla.json'
- path-to-document: 'https://github.com/${{ github.repository }}/blob/main/contributingGuides/CLA.md'
- branch: 'main'
- remote-organization-name: 'Expensify'
- remote-repository-name: 'CLA'
- lock-pullrequest-aftermerge: false
- allowlist: OSBotify,snyk-bot
+ uses: Expensify/GitHub-Actions/.github/workflows/cla.yml@main
+ secrets: inherit
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..b48c7b2175eb 100644
--- a/.github/workflows/e2ePerformanceTests.yml
+++ b/.github/workflows/e2ePerformanceTests.yml
@@ -24,33 +24,21 @@ 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 }}
-
- - 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
+ fetch-depth: 0 # Fetches the entire history
- - 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'
+ - name: Determine "baseline ref" (prev merge commit)
+ id: getBaselineRef
+ run: |
+ previous_merge=$(git rev-list --merges HEAD~1 | head -n 1)
+ git checkout "$previous_merge"
+ echo "$previous_merge"
+ echo "BASELINE_REF=$previous_merge" >> "$GITHUB_OUTPUT"
- name: Get pull request details
id: getPullRequestDetails
@@ -84,15 +72,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 +114,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 98d06e8c5f71..b428978a1563 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -18,8 +18,7 @@ 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
+src/libs/SearchParser/autocompleteParser.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 b07de7a4da33..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 1009005001
- versionName "9.0.50-1"
+ 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/assets/images/companyCards/pending-bank.svg b/assets/images/companyCards/pending-bank.svg
new file mode 100644
index 000000000000..dc265466d53f
--- /dev/null
+++ b/assets/images/companyCards/pending-bank.svg
@@ -0,0 +1,263 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/images/gallery-not-found.svg b/assets/images/gallery-not-found.svg
new file mode 100644
index 000000000000..25da973ce9cb
--- /dev/null
+++ b/assets/images/gallery-not-found.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts
index 91fc4b1bf528..2d8e27fd453e 100644
--- a/config/webpack/webpack.common.ts
+++ b/config/webpack/webpack.common.ts
@@ -227,8 +227,6 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment):
'react-native-config': 'react-web-config',
// eslint-disable-next-line @typescript-eslint/naming-convention
'react-native$': 'react-native-web',
- // eslint-disable-next-line @typescript-eslint/naming-convention
- 'react-native-sound': 'react-native-web-sound',
// Module alias for web & desktop
// https://webpack.js.org/configuration/resolve/#resolvealias
// eslint-disable-next-line @typescript-eslint/naming-convention
diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md
index 82e368214223..be71cd4e115a 100644
--- a/contributingGuides/CONTRIBUTING.md
+++ b/contributingGuides/CONTRIBUTING.md
@@ -32,9 +32,9 @@ This project and everyone participating in it is governed by the Expensify [Code
At this time, we are not hiring contractors in Crimea, North Korea, Russia, Iran, Cuba, or Syria.
## Slack channels
-All contributors should be a member of a shared Slack channel called [#expensify-open-source](https://expensify.slack.com/archives/C01GTK53T8Q) -- this channel is used to ask **general questions**, facilitate **discussions**, and make **feature requests**.
+We have a shared Slack channel called #expensify-open-source — this channel is used to ask general questions, facilitate discussions, and make feature requests.
-Before requesting an invite to Slack, please ensure your Upwork account is active, since we only pay via Upwork (see [below](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#payment-for-contributions)). To request an invite to Slack, email contributors@expensify.com with the subject `Slack Channel Invites`. We'll send you an invite!
+That said, we have a small issue with adding users at the moment and we’re working with Slack to try and get this resolved. If you would like to join, [fill out this form](https://forms.gle/Q7hnhUJPnQCK7Fe56) with your email and Upwork profile link. Once resolved, we’ll add you.
Note: Do not send direct messages to the Expensify team in Slack or Expensify Chat, they will not be able to respond.
diff --git a/contributingGuides/OFFLINE_UX.md b/contributingGuides/OFFLINE_UX.md
index 47b2cf117a06..48d52af6f796 100644
--- a/contributingGuides/OFFLINE_UX.md
+++ b/contributingGuides/OFFLINE_UX.md
@@ -85,7 +85,7 @@ When the user is offline:
- In the event that `successData` and `failureData` are the same, you can use a single object `finallyData` in place of both.
**Handling errors:**
-- The [OfflineWithFeedback component](https://github.com/Expensify/App/blob/main/src/components/OfflineWithFeedback.js) already handles showing errors too, as long as you pass the error field in the [errors prop](https://github.com/Expensify/App/blob/128ea378f2e1418140325c02f0b894ee60a8e53f/src/components/OfflineWithFeedback.js#L29-L31)
+- The [OfflineWithFeedback component](https://github.com/Expensify/App/blob/main/src/components/OfflineWithFeedback.tsx) already handles showing errors too, as long as you pass the error field in the [errors prop](https://github.com/Expensify/App/blob/128ea378f2e1418140325c02f0b894ee60a8e53f/src/components/OfflineWithFeedback.js#L29-L31)
- The behavior for when something fails is:
- If you were adding new data, the failed to add data is displayed greyed out and with the button to dismiss the error
- If you were deleting data, the failed data is displayed regularly with the button to dismiss the error
diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md
index 304811332916..e6660d848129 100644
--- a/contributingGuides/STYLE.md
+++ b/contributingGuides/STYLE.md
@@ -24,6 +24,7 @@
- [Refs](#refs)
- [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript)
- [Default value for inexistent IDs](#default-value-for-inexistent-IDs)
+ - [Extract complex types](#extract-complex-types)
- [Naming Conventions](#naming-conventions)
- [Type names](#type-names)
- [Prop callbacks](#prop-callbacks)
@@ -492,6 +493,30 @@ const foo = report?.reportID ?? '-1';
report ? report.reportID : '-1';
```
+### Extract complex types
+
+Advanced data types, such as objects within function parameters, should be separated into their own type definitions. Callbacks in function parameters should be extracted if there's a possibility they could be reused somewhere else.
+
+```ts
+// BAD
+function foo(param1: string, param2: {id: string}) {...};
+
+// BAD
+function foo(param1: string, param2: (value: string) => void) {...};
+
+// GOOD
+type Data = {
+ id: string;
+};
+
+function foo(param1: string, param2: Data) {...};
+
+// GOOD
+type Callback = (value: string) => void
+
+function foo(param1: string, param2: Callback) {...};
+```
+
## Naming Conventions
### Type names
diff --git a/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md b/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md
deleted file mode 100644
index 7dcc8e5e9c29..000000000000
--- a/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md
+++ /dev/null
@@ -1,73 +0,0 @@
----
-title: Importing Receipts from Various Platforms to Expensify
-description: Detailed guide on how to import receipts from multiple travel platforms into Expensify.
----
-
-# Overview
-You can automatically import receipts from many travel platforms into Expensify, to make tracking expenses while traveling for business a breeze. Read on to learn how to import receipts from Bolt Work, Spot Hero, Trainline, Grab, HotelTonight, and Kayak for Business.
-
-## How to Connect to Bolt Work
-
-### Set Up Bolt Work Profile
-- Open the Bolt app, go to the side navigation menu, and select Payment.
-- At the bottom, select Set up work profile and follow the instructions, entering your work email for verification.
-
-### Link to Expensify
-- In the Bolt app, go to Work Rides.
-- Select Add expense provider, choose Expensify, and enter the associated email to receive a verification link.
-- Ensure you select your work ride profile as the payment method before booking.
-
-## How to Connect to SpotHero
-
-### Set up a Business Profile
-- Open the SpotHero app, click the hamburger icon, and go to Account Settings.
-- Click Set up Business Profile.
-- Specify the email connected to Expensify and set up your payment method.
-- Upon checkout, choose between Business and Personal Profiles in the "Payment Details" section.
-- If you want, you can set a weekly or monthly cadence for consolidated SpotHero expense reports in your Business Profile settings. This will batch all of your SpotHero expenses to import into Expensify at that cadence.
-
-## How to Connect to Trainline
-- To send a ticket receipt to Expensify:
- - In the Trainline app, navigate to the My Tickets tab.
- - Tap Manage my booking > Expense receipt > Send to Expensify.
-- That’s it!
-
-## How to Connect to Grab
-- In the Grab app, tap on your name, go to “Profiles”, and “Add a business profile”.
-- Follow instructions and enter your work email for verification.
-- In your profile, tap on Business > Expense Solution > Expensify > Save.
-- Before booking, select your Business profile and confirm.
-
-## How to Connect to HotelTonight
-- In HotelTonight, go to the Bookings tab and select your booking.
-- Select Receipt > Expensify, enter your Expensify email, and send.
-
-## How to Connect to Kayak for Business
-
-### Admin Setup
-- Admins should go to “Company Settings” and click on “Connect to Expensify”.
-- Bookings made by employees will automatically be sent to Expensify.
-
-### Traveler Setup
-- From your account settings, choose whether expenses should be sent to Expensify automatically or manually.
-- We recommend sending them automatically, so you can travel without even thinking about your expense reports.
-
-{% include faq-begin.md %}
-
-**Q: What if I don’t have the option for Send to Expensify in Trainline?**
-
-A: This can happen if the native iOS Mail app is not installed on an Apple device. However, you can still use the native iOS share to Expensify function for Trainline receipts.
-
-**Q: Why should I choose automatic mode in Kayak for Business?**
-
-A: Automatic mode is less effort as it’s easier to delete an expense in Expensify than to remember to forward a forgotten receipt.
-
-**Q: Can I receive consolidated reports from SpotHero?**
-
-A: Yes, you can set a weekly or monthly cadence for SpotHero expenses to be emailed in a consolidated report.
-
-**Q: Do I need to select a specific profile before booking in Bolt Work and Grab?**
-
-A: Yes, ensure you have selected your work or business profile as the payment method before booking.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md b/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md
new file mode 100644
index 000000000000..2e5b5065b3d5
--- /dev/null
+++ b/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md
@@ -0,0 +1,121 @@
+---
+title: Travel Receipt Integrations
+description: How to use pre-built or custom integrations to track travel expenses
+---
+
+Expensify’s receipt integrations allow a merchant to upload receipts directly to a user’s Expensify account. A merchant just has to email a receipt to an Expensify user and Cc receipts@expensify.com. This automatically creates a transaction in the Expensify account for the user whose email address is in the To field.
+
+You can set up a receipt integration by using one of our existing pre-built integrations, or by building your own receipt integration.
+
+## Use a pre-built travel integration
+
+You can use our pre-built integrations to automatically import travel receipts from Bolt Work, Spot Hero, Grab, and Kayak for Business.
+
+### Bolt Work
+
+1. In the Bolt app, tap the menu icon in the top left and tap **Work trips**.
+2. Tap **Create profile**.
+3. Enter the email address that you use for Expensify, then tap **Next**.
+4. Enter your company details, then tap **Next**.
+5. Choose a payment method. If you don’t want to use the existing payment methods, you can create a new one by tapping **Add Payment Method**. Then tap **Next**.
+6. Tap **Done**.
+7. Tap Add expense provider, then tap **Expensify**.
+8. Tap **Verify**.
+9. Tap the menu icon on the top left and tap **Work trips** once more.
+10. Tap **Add expense provider** and select **Expensify** again.
+
+When booking a trip with Bolt Work, select your work trip profile as the payment method before booking. Then the receipt details will be automatically sent to Expensify.
+
+### SpotHero
+
+1. In the SpotHero app, tap the menu icon in the top left and tap **Account Settings**.
+2. Tap **Set up Business Profile**.
+3. Tap **Create Business Profile**.
+4. Enter the email address you use for Expensify and tap **Next**.
+5. Tap **Add a Payment Method** and enter your payment account details. Then tap **Next**.
+6. Tap **Expensify**.
+
+When reserving parking with SpotHero, select your business profile in the Payment Details section. Then the receipt will be automatically sent to Expensify. In your SpotHero Business Profile settings, you can also set a weekly or monthly cadence for SpotHero to send a batch of expenses to Expensify.
+
+### Grab
+
+1. In the Grab app, tap your profile picture in the top left.
+2. Tap your user icon again at the top of the settings menu.
+3. Tap **Add a business profile**.
+4. Tap Next twice, then tap **Let’s Get Started**.
+5. Enter the email address you use for Expensify and tap the next arrow in the bottom right.
+6. Check your email and copy the verification code you receive from Grab.
+7. Tap **Manage My Business Profile**.
+8. Under Preferences, tap **Expense Solution**.
+9. Tap **Expensify**, then tap **Save**.
+
+When booking a trip with Grab, tap **personal** and select **business** to ensure your business profile is selected. Then the receipt will be automatically sent to Expensify.
+
+### KAYAK for Business
+
+**Admin Setup**
+
+This process must be completed by a KAYAK for Business admin.
+
+1. On your KAYAK for Business homepage, click **Company Settings**.
+2. Click **Connect to Expensify**.
+
+KAYAK for Business will now forward bookings made by each employee into Expensify.
+
+**Traveler Setup**
+
+1. On your KAYAK for Business homepage, click **Profile Account Settings**.
+2. Enable the Expensify toggle to have your expenses automatically sent to Expensify. You also have the option to send them manually.
+
+## Build your own receipt integration
+
+1. Email receiptintegration@expensify.com and include:
+ - **Subject**: Use “Receipt Integration Request" as the subject line
+ - **Body**: List all email addresses the merchant sends email receipts from
+2. Once you receive your email confirmation (within approximately 2 weeks) that the email addresses have been whitelisted, you’ll then be able to Cc receipts@expensify.com on receipt emails to users, and transactions will be created in the users’ Expensify account.
+3. Test the integration by sending a receipt email to the email address you used to create your Expensify account and Cc receipts@expensify.com. Wait for the receipt to be SmartScanned. Then you will see the merchant, date, and amount added to the transaction.
+
+### Using the integration
+
+When sending an emailed receipt:
+
+- Attachments on an email (that are not an .ics file) will be SmartScanned. We recommend including the receipt as the only attachment.
+- You can only include one email address in the To field. In the Cc field, include only receipts@expensify.com.
+- Reservations for hotels and car rentals cannot be sent to Expensify as an expense because they are paid at the end of usage. You can only send transaction data for purchases that have already been made.
+- Use standardized three-letter currency codes (ISO 4217) where applicable.
+
+{% include faq-begin.md %}
+
+**In Trainline, what if I don’t have the option for Send to Expensify?**
+
+This can happen if the native iOS Mail app is not installed on an Apple device. However, you can still use the native iOS Share to Expensify function for Trainline receipts.
+
+**Why does it take 2 weeks to set up a custom integration?**
+
+Receipt integrations require our engineers to manually set them up on the backend. For that reason, it can take up to 2 weeks to set it up.
+
+**Is there a way to connect via API?**
+
+No, at this time there are no API receipt integrations. All receipt integrations are managed via receipt emails.
+
+**What is your Open API?**
+
+Our Open API is a self-serve tool meant to pull information out of Expensify. Typically, this tool is used to build integrations with accounting solutions that we don’t directly integrate with. If you wish to push data into Expensify, the only way to integrate is via the receipt integration options listed above in this article.
+
+**Are you able to split one email into separate receipts?**
+
+The receipt integration is unable to automatically split one email into separate receipts. However, once the receipt is SmartScanned, users can [split the expense](https://help.expensify.com/articles/expensify-classic/expenses/Split-an-expense) in their Expensify account.
+
+**Can we set up a (co-marketing) partnership?**
+
+We currently do not offer any co-marketing partnerships.
+
+**Can we announce or advertise our custom integration with Expensify?**
+
+Absolutely! You can promote the integration across your social media channels (tag @expensify and use the #expensify hashtag) and you can even create your own dedicated landing page on your website for your integration. At a minimum, we recommend including a brief overview of how the integration works, the benefits of using it, an integration setup guide, and guidance for how someone can contact you for support or integration setup if necessary.
+
+**How can I get help?**
+
+You can contact Concierge for ongoing support any time by clicking the green chat icon in the mobile or web app, or by emailing concierge@expensify.com. Concierge is a global team of highly trained product specialists focused on making our product as easy to use as possible and answering all your questions.
+
+{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
index f926792ffd1f..aecf21acfc3f 100644
--- a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
+++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
@@ -2,70 +2,62 @@
title: Configure Netsuite
description: Configure NetSuite's export, coding, and advanced settings.
---
-By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient.
+Correctly configuring NetSuite settings in Expensify ensures seamless integration between your expense management and accounting processes, saving time and reducing manual errors. Aligning your workspace settings with NetSuite’s financial structure can automate data syncs, simplify reporting, and improve overall financial accuracy.
+
+# Best Practices Using NetSuite
+A connection to NetSuite lets you combine the power of Expensify’s expense management features with NetSuite’s accounting capabilities.
+
+By following the recommended best practices below, your finances will be automatically categorized and accounted for in NetSuite:
+- Configure your setup immediately after making the connection, and review each settings tab thoroughly.
+- Keep Auto Sync enabled:
+ - The daily sync will update Expensify with any changes to your chart of accounts, customers/projects, or bank accounts in NetSuite.
+ - Finalized reports will be exported to NetSuite automatically, saving your admin team time with every report.
+- Set your preferred exporter to someone who is both a workspace and domain admin.
+- Configure your coding settings and enforce them by [requiring categories and tags on expenses](https://help.expensify.com/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses).
# Step 1: Configure Export Settings
There are numerous options for exporting Expensify reports to NetSuite. Let's explore how to configure these settings to align with your business needs.
-To access these settings, head to **Settings > Workspace > Group > Connections** and select the **Configure** button.
+To access these settings, go to **Settings > Workspace > Group > Connections** and select the **Configure** button.
-## Export Options
-
-### Subsidiary
+## Subsidiary
The subsidiary selection will only appear if you use NetSuite OneWorld and have multiple subsidiaries active. If you add a new subsidiary to NetSuite, sync the workspace connection, and the new subsidiary should appear in the dropdown list under **Settings > Workspaces > _[Workspace Name]_ > Connections**.
-### Preferred Exporter
+## Preferred Exporter
This option allows any admin to export, but the preferred exporter will receive notifications in Expensify regarding the status of exports.
-### Date
+## Date
The three options for the date your report will export with are:
- Date of last expense: This will use the date of the previous expense on the report
- Submitted date: The date the employee submitted the report
- Exported date: The date you export the report to NetSuite
-## Reimbursable Expenses
-
-### Expense Reports
-
-Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite.
-
-### Vendor Bills
-
-Expensify transactions export as vendor bills in NetSuite and will be mapped to the subsidiary associated with the corresponding policy. Each report will be posted as payable to the vendor associated with the employee who submitted the report.
-You can also set an approval level in NetSuite for vendor bills.
+## Export Settings for Reimbursable Expenses
-### Journal Entries
+**Expense Reports:** Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite.
-Expensify transactions that are set to export as journal entries in NetSuite will be mapped to the subsidiary associated with this policy. All the transactions will be posted to the payable account specified in the policy.
+**Vendor Bills:** Expensify transactions export as vendor bills in NetSuite and will be mapped to the subsidiary associated with the corresponding policy. Each report will be posted as payable to the vendor associated with the employee who submitted the report. You can also set an approval level in NetSuite for vendor bills.
-You can also set an approval level in NetSuite for the journal entries.
+**Journal Entries:** Expensify transactions that are set to export as journal entries in NetSuite will be mapped to the subsidiary associated with this policy. All the transactions will be posted to the payable account specified in the policy. You can also set an approval level in NetSuite for the journal entries.
-**Important Notes:**
- Journal entry forms by default do not contain a customer column, so it is not possible to export customers or projects with this export option
- The credit line and header level classifications are pulled from the employee record
-## Non-Reimbursable Expenses
+## Export Settings for Non-Reimbursable Expenses
-### Vendor Bills
+**Vendor Bills:** Non-reimbursable expenses will be posted as a vendor bill payable to the default vendor specified in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific vendor in NetSuite. You can also set an approval level in NetSuite for the bills.
-Non-reimbursable expenses will be posted as a vendor bill payable to the default vendor specified in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific vendor in NetSuite. You can also set an approval level in NetSuite for the bills.
+**Journal Entries:** Non-reimbursable expenses will be posted to the Journal Entries posting account selected in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in NetSuite.
-### Journal Entries
-
-Non-reimbursable expenses will be posted to the Journal Entries posting account selected in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in NetSuite.
-
-**Important Notes:**
- Expensify Card expenses will always export as Journal Entries, even if you have Expense Reports or Vendor Bills configured for non-reimbursable expenses on the Export tab
- Journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option
- The credit line and header level classifications are pulled from the employee record
-### Expense Reports
-
-To use the expense report option for your corporate card expenses, you will need to set up your default corporate cards in NetSuite.
+**Expense Reports:** To use the expense report option for your corporate card expenses, you will need to set up your default corporate cards in NetSuite.
To use a default corporate card for non-reimbursable expenses, you must select the correct card on the employee records (for individual accounts) or the subsidiary record (If you use a non-one world account, the default is found in your accounting preferences).
@@ -77,11 +69,11 @@ Add the corporate card option and corporate card main field to your expense repo
You can select the default account on your employee record to use individual corporate cards for each employee. Make sure you add this field to your employee entity form in NetSuite.
If you have multiple cards assigned to a single employee, you cannot export to each account. You can only have a single default per employee record.
-### Export Invoices
+## Export Invoices
Select the Accounts Receivable account you want your Invoice Reports to export. In NetSuite, the Invoices are linked to the customer, corresponding to the email address where the Invoice was sent.
-### Default Vendor Bills
+## Default Vendor Bills
When selecting the option to export non-reimbursable expenses as vendor bills, the list of vendors will be available in the dropdown menu.
@@ -169,7 +161,7 @@ From there, you should see the values for the Custom Segment under the Tag or Re
Don’t use the "Filtered by" feature available for Custom Segments. Expensify can’t make these dependent on other fields. If you do have a filter selected, we suggest switching that filter in NetSuite to "Subsidiary" and enabling all subsidiaries to ensure you don't receive any errors upon exporting reports.
-### Custom Records
+## Custom Records
Custom Records are added through the Custom Segments feature.
@@ -197,7 +189,7 @@ Lastly, head over to Expensify and do the following:
From there, you should see the values for the Custom Records under the Tag or Report Field settings in Expensify.
-### Custom Lists
+## Custom Lists
To add Custom Lists to your workspace, you’ll need to locate two fields in NetSuite:
- The name of the record
@@ -250,17 +242,11 @@ With this enabled, all submitters can add any newly imported Categories to an Ex
## Invite Employees & Set Approval Workflow
-### Invite Employees
-
-Use this option in Expensify to bring your employees from a specific NetSuite subsidiary into Expensify.
-Once imported, Expensify will send them an email letting them know they've been added to a workspace.
+**Invite Employees:** Use this option in Expensify to bring your employees from a specific NetSuite subsidiary into Expensify. Once imported, Expensify will send them an email letting them know they've been added to a workspace.
-### Set Approval Workflow
-
-Besides inviting employees, you can also establish an approval process in NetSuite.
-
-By doing this, the Approval Workflow in Expensify will automatically follow the same rules as NetSuite, typically starting with Manager Approval.
+**Set Approval Workflow:** In addition to inviting employees, you can establish an approval process in NetSuite. The Approval Workflow in Expensify will automatically follow the same rules as NetSuite, typically starting with Manager Approval.
+The available options are:
- **Basic Approval:** This is a single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page.
- **Manager Approval (default):** Two levels of approval route reports first to an employee's NetSuite expense approver or supervisor, and second to a workspace-wide Final Approver. By NetSuite convention, Expensify will map to the supervisor if no expense approver exists. The Final Approver defaults to the workspace owner but can be edited on the people page.
- **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured on the workspace's People settings page. If you enable this setting, it’s recommended you review the newly imported employees and managers on the **Settings > Workspaces > Group > _[Workspace Name]_ > People page**. You can set a user role for each new employee and enforce an approval workflow.
@@ -275,7 +261,7 @@ Using this feature allows you to send the original amount of the expense rather
## Cross-Subsidiary Customers/Projects
-This allows you to import Customers and Projects across all subsidiaries to a single group workspace. For this functionality, you must enable "Intercompany Time and Expense" in NetSuite.
+This allows you to import Customers and Projects across all subsidiaries to a single group workspace. To enable this functionality in NetSuite, you must enable "Intercompany Time and Expense."
That feature is found in NetSuite under _Setup > Company > Setup Tasks: Enable Features > Advanced Features_.
@@ -303,7 +289,7 @@ If you have Approval Routing selected in your accounting preference, this will o
If you do not wish to use Approval Routing in NetSuite, go to _Setup > Accounting > Accounting Preferences > Approval Routing_ and ensure Vendor Bills and Journal Entries are not selected.
-### Collection Account
+## Collection Account
When exporting invoices, once marked as Paid, the payment is marked against the account selected after enabling the Collection Account setting.
@@ -343,7 +329,7 @@ Add the corporate card option and the corporate card main field to configure you
If you prefer individual corporate cards for each employee, you can select the default account on the employee record. Add this field to your employee entity form in NetSuite (under _Customize > Customize Form_ from any employee record). Note that each employee can have only one corporate card account default.
-### Exporting Company Cards to GL Accounts in NetSuite
+## Exporting Company Cards to GL Accounts in NetSuite
If you need to export company card transactions to individual GL accounts, you can set that up at the domain level.
@@ -359,9 +345,7 @@ You’ll want to set up Tax Groups in Expensify if you're keeping track of taxes
Expensify can import "NetSuite Tax Groups" (not Tax Codes) from NetSuite. Tax Groups can contain one or more Tax Codes. If you have subsidiaries in the UK or Ireland, ensure your Tax Groups have only one Tax Code.
-You can locate these in NetSuite by setting up> Accounting > Tax Groups.
-
-You’ll want to name Tax Groups something that makes sense to your employees since both the name and the tax rate will appear in Expensify.
+You can locate these in NetSuite by setting up> Accounting > Tax Groups. Name the Tax Groups something that makes sense to your employees since both the name and the tax rate will appear in Expensify.
To bring NetSuite Tax Groups into Expensify, here's what you need to do:
1. Create your Tax Groups in NetSuite by going to _Setup > Accounting > Tax Groups_
@@ -386,7 +370,7 @@ Expensify. If you deactivate this group in NetSuite, it will lead to export erro
Additionally, some tax nexuses in NetSuite have specific settings that need to be configured in a certain way to work seamlessly with the Expensify integration:
- In the Tax Code Lists Include field, choose "Tax Groups" or "Tax Groups and Tax Codes." This setting determines how tax information is handled.
-- In the Tax Rounding Method field, select "Round Off." Although it won't cause connection errors, not using this setting can result in exported amounts differing from what NetSuite expects.
+- In the Tax Rounding Method field, select "Round Off." Although this setting won't cause connection errors, not using it can result in exported amounts differing from what NetSuite expects.
If your tax groups are importing into Expensify but not exporting to NetSuite, check that each tax group has the right subsidiaries enabled. That is crucial for proper data exchange.
@@ -408,7 +392,7 @@ Let's dive right in:
1. Access Configuration Settings: Go to **Settings > Workspace > Group > _[Workspace Name]_ > Connections > Configuration**
2. Choose Your Accounts Receivable Account: Scroll down to "Export Expenses to" and select the appropriate Accounts Receivable account from the dropdown list. If you don't see any options, try syncing your NetSuite connection by returning to the Connections page and clicking **Sync Now**
-### Exporting an Invoice to NetSuite
+## Exporting an Invoice to NetSuite
Invoices will be automatically sent to NetSuite when they are in the "Processing" or "Paid" status. This ensures you always have an up-to-date record of unpaid and paid invoices.
@@ -421,7 +405,7 @@ When exporting to NetSuite, we match the recipient's email address on the invoic
Once exported, the invoice will appear in the Accounts Receivable account you selected during your NetSuite Export configuration.
-### Updating the status of an invoice to "paid"
+## Updating the status of an invoice to "paid"
When you mark an invoice as "Paid" in Expensify, this status will automatically update in NetSuite. Similarly, if the invoice is marked as "Paid" in NetSuite, it will sync with Expensify. The payment will be reflected in the Collection account specified in your Advanced Settings Configuration.
diff --git a/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md b/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md
index 1f96d9b8a633..6cc69fccccc1 100644
--- a/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md
+++ b/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md
@@ -1,12 +1,11 @@
---
title: NetSuite
-description: Set up the direct connection from Expensify to NetSuite.
+description: Connect NetSuite to Expensify for streamlined expense reporting and accounting integration.
order: 1
---
-# Overview
-Expensify's integration with NetSuite allows you to automate report exports, tailor your coding preferences, and tap into NetSuite's array of advanced features. By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient.
+Expensify's direct integration with NetSuite allows you to automate report exports, tailor your coding preferences, and tap into NetSuite's array of advanced features.
-**Before connecting NetSuite to Expensify, a few things to note:**
+## Before connecting NetSuite to Expensify, review the following details:
- Token-based authentication works by ensuring that each request to NetSuite is accompanied by a signed token which is verified for authenticity
- You must be able to login to NetSuite as an administrator to initiate the connection
- You must have a Control Plan in Expensify to integrate with NetSuite
@@ -15,9 +14,7 @@ Expensify's integration with NetSuite allows you to automate report exports, tai
- Ensure that your workspace's report output currency setting matches the NetSuite Subsidiary default currency
- Make sure your page size is set to 1000 for importing your customers and vendors. You can check this in NetSuite under **Setup > Integration > Web Services Preferences > 'Search Page Size'**
-# Connect to NetSuite
-
-## Step 1: Install the Expensify Bundle in NetSuite
+# Step 1: Install the Expensify Bundle in NetSuite
1. While logged into NetSuite as an administrator, go to Customization > SuiteBundler > Search & Install Bundles, then search for "Expensify"
2. Click on the Expensify Connect bundle (Bundle ID 283395)
@@ -25,13 +22,13 @@ Expensify's integration with NetSuite allows you to automate report exports, tai
4. If you already have the Expensify Connect bundle installed, head to _Customization > SuiteBundler > Search & Install Bundles > List_ and update it to the latest version
5. Select **Show on Existing Custom Forms** for all available fields
-## Step 2: Enable Token-Based Authentication
+# Step 2: Enable Token-Based Authentication
1. Head to _Setup > Company > Enable Features > SuiteCloud > Manage Authentication_
2. Make sure “Token Based Authentication” is enabled
3. Click **Save**
-## Step 3: Add Expensify Integration Role to a User
+# Step 3: Add Expensify Integration Role to a User
The user you select must have access to at least the permissions included in the Expensify Integration Role, but they're not required to be an Admin.
1. In NetSuite, head to Lists > Employees, and find the user you want to add the Expensify Integration role to
@@ -40,7 +37,7 @@ The user you select must have access to at least the permissions included in the
Remember that Tokens are linked to a User and a Role, not solely to a User. It's important to note that you cannot establish a connection with tokens using one role and then switch to another role afterward. Once you've initiated a connection with tokens, you must continue using the same token/user/role combination for all subsequent sync or export actions.
-## Step 4: Create Access Tokens
+# Step 4: Create Access Tokens
1. Using Global Search in NetSuite, enter “page: tokens”
2. Click **New Access Token**
@@ -49,21 +46,20 @@ Remember that Tokens are linked to a User and a Role, not solely to a User. It's
5. Press **Save**
6. Copy and Paste the token and token ID to a saved location on your computer (this is the only time you will see these details)
-## Step 5: Confirm Expense Reports are Enabled in NetSuite.
+# Step 5: Confirm Expense Reports are Enabled in NetSuite.
Enabling Expense Reports is required as part of Expensify's integration with NetSuite:
1. Logged into NetSuite as an administrator, go to Setup > Company > Enable Features > Employees
2. Confirm the checkbox next to Expense Reports is checked
3. If not, click the checkbox and then Save to enable Expense Reports
-## Step 6: Confirm Expense Categories are set up in NetSuite.
+# Step 6: Confirm Expense Categories are set up in NetSuite.
Once Expense Reports are enabled, Expense Categories can be set up in NetSuite. Expense Categories are an alias for General Ledger accounts used to code expenses.
-
1. Logged into NetSuite as an administrator, go to Setup > Accounting > Expense Categories (a list of Expense Categories should show)
2. If no Expense Categories are visible, click **New** to create new ones
-## Step 7: Confirm Journal Entry Transaction Forms are Configured Properly
+# Step 7: Confirm Journal Entry Transaction Forms are Configured Properly
1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
2. Click **Customize** or **Edit** next to the Standard Journal Entry form
@@ -71,7 +67,7 @@ Once Expense Reports are enabled, Expense Categories can be set up in NetSuite.
4. Click the sub-header Lines and verify that the "Show" column for "Receipt URL" is checked
5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the journal type have this same configuration
-## Step 8: Confirm Expense Report Transaction Forms are Configured Properly
+# Step 8: Confirm Expense Report Transaction Forms are Configured Properly
1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
2. Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main**
@@ -79,7 +75,7 @@ Once Expense Reports are enabled, Expense Categories can be set up in NetSuite.
4. Click the second sub-header, Expenses, and verify that the 'Show' column for 'Receipt URL' is checked
5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the expense report type have this same configuration
-## Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly
+# Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly
1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_
2. Click **Customize** or **Edit** next to your preferred Vendor Bill form
@@ -87,20 +83,20 @@ Once Expense Reports are enabled, Expense Categories can be set up in NetSuite.
4. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class
5. Go to _Customization > Forms > Transaction Forms_ and provide all other transaction forms with the vendor bill type have this same configuration
-## Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly
+# Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly
1. While logged in as an administrator, go to _Customization > Forms > Transaction Forms_
2. Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked
3. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class
4. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the vendor credit type have this same configuration
-## Step 11: Set up Tax Groups (only applicable if tracking taxes)
+# Step 11: Set up Tax Groups (only applicable if tracking taxes)
Expensify imports NetSuite Tax Groups (not Tax Codes), which you can find in NetSuite under _Setup > Accounting > Tax Groups_.
Tax Groups are an alias for Tax Codes in NetSuite and can contain one or more Tax Codes (Please note: for UK and Ireland subsidiaries, please ensure your Tax Groups do not have more than one Tax Code). We recommend naming Tax Groups so your employees can easily understand them, as the name and rate will be displayed in Expensify.
-Before importing NetSuite Tax Groups into Expensify:
+## Before importing NetSuite Tax Groups into Expensify:
1. Create your Tax Groups in NetSuite by going to _Setup > Accounting > Tax Groups_
2. Click **New**
3. Select the country for your Tax Group
@@ -115,9 +111,9 @@ Ensure Tax Groups can be applied to expenses by going to _Setup > Accounting > S
If this field does not display, it’s not needed for that specific country.
-## Step 12: Connect Expensify and NetSuite
+# Step 12: Connect Expensify and NetSuite
-1. Log into Expensify as a Policy Admin and go to **Settings > Workspaces > _[Workspace Name]_ > Connections > NetSuite**
+1. Log into Expensify as a Workspace Admin and go to **Settings > Workspaces > _[Workspace Name]_ > Connections > NetSuite**
2. Click **Connect to NetSuite**
3. Enter your Account ID (Account ID can be found in NetSuite by going to _Setup > Integration > Web Services Preferences_)
4. Then, enter the token and token secret
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.
+{: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:
+{: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.
+{: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**.
+
+{:width="100%"}
+
+7. Select the Web Connector and click **Properties**.
+
+{: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”.
+
+{:width="100%"}
+
3. Check the Report Comments in Expensify to confirm that the report has been successfully exported to QuickBooks Desktop.
+{: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/billing-and-subscriptions/adding-payment-card-subscription-overview.md b/docs/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription.md
similarity index 63%
rename from docs/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview.md
rename to docs/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription.md
index d30fa06bc059..c181536d1174 100644
--- a/docs/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview.md
+++ b/docs/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription.md
@@ -1,15 +1,18 @@
-Subscription Management
+---
+title: Subscription Management
+description: How to manage your subscription
+---
Under the subscriptions section of your account, you can manage your payment card details, view your current plan, add a billing card, and adjust your subscription size and renewal date.
To view or manage your subscription in New Expensify:
-**Open the App**: Launch New Expensify on your device.
-**Go to Account Settings**: Click your profile icon in the bottom-left corner.
-**Find Workspaces**: Navigate to the Workspaces section.
-**Open Subscriptions**: Click Subscription under Workspaces to view your subscription.
+* **Open the App**: Launch New Expensify on your device.
+* **Go to Account Settings**: Click your profile icon in the bottom-left corner.
+* **Find Workspaces**: Navigate to the Workspaces section.
+* **Open Subscriptions**: Click Subscription under Workspaces to view your subscription.
## Add a Payment Card
Look for the option to **Add Payment Card**. Enter your payment card details securely to ensure uninterrupted service.
-[PLACEHOLDER for design image- default]
+{:width="100%"}
## Subscription Overview
This is where you can view your current subscription plan and see details like the number of seats, billing information, and the next renewal date.
@@ -19,13 +22,13 @@ This is where you can view your current subscription plan and see details like t
- **Auto-increase annual seats**: Here you can see how much you could save by automatically increasing seats to accommodate team members who exceed the current subscription size.
**Note**: This will extend your annual subscription end date.
-[PLACEHOLDER for design image- your plan]
+{:width="100%"}
## Early Cancellation Requests
If you need to cancel your subscription early, you can find the **Request Early Cancellation** option in the same Subscriptions section.
**Note**: Not all customers are eligible to cancel their subscription early.
-[PLACEHOLDER for design image- billing]
+{:width="100%"}
## Pricing Information
For more details on pricing plans, visit Billing Page [coming soon!]
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.
{: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/articles/new-expensify/expenses-&-payments/Track-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Track-expenses.md
index f6260b9f8f84..77256279b1d7 100644
--- a/docs/articles/new-expensify/expenses-&-payments/Track-expenses.md
+++ b/docs/articles/new-expensify/expenses-&-payments/Track-expenses.md
@@ -40,4 +40,6 @@ For an in-depth walkthrough on how to create an expense, check out the [create a
{% include end-selector.html %}
+{:width="100%"}
+
diff --git a/docs/articles/new-expensify/workspaces/Create-expense-categories.md b/docs/articles/new-expensify/workspaces/Create-expense-categories.md
index 56557d449908..a6874ac0a2ef 100644
--- a/docs/articles/new-expensify/workspaces/Create-expense-categories.md
+++ b/docs/articles/new-expensify/workspaces/Create-expense-categories.md
@@ -110,6 +110,7 @@ GL codes and payroll codes can be exported to a CSV export. They are not display
6. To add or edit a GL code, click the GL code field, make the desired change, then click **Save**
7. To add or edit a payroll code, click the payroll code field, make the desired change, then click **Save**
+{:width="100%"}
# Apply categories to expenses automatically
diff --git a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md
index 8f2cf0897ad0..df77ed3b5b01 100644
--- a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md
+++ b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md
@@ -29,6 +29,8 @@ To require workspace members to add tags and/or categories to their expenses,
{% include end-option.html %}
{% include end-selector.html %}
+
+{:width="100%"}
This will highlight the tag and/or category field as required on all expenses.
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/FAB_track_expense.png b/docs/assets/images/FAB_track_expense.png
new file mode 100644
index 000000000000..6ee0cf5abba4
Binary files /dev/null and b/docs/assets/images/FAB_track_expense.png differ
diff --git a/docs/assets/images/NetSuite_Configure_06.png b/docs/assets/images/NetSuite_Configure_06.png
new file mode 100644
index 000000000000..cddfe2fabcd6
Binary files /dev/null and b/docs/assets/images/NetSuite_Configure_06.png differ
diff --git a/docs/assets/images/NetSuite_Configure_08.png b/docs/assets/images/NetSuite_Configure_08.png
new file mode 100644
index 000000000000..77690a2c3aa1
Binary files /dev/null and b/docs/assets/images/NetSuite_Configure_08.png differ
diff --git a/docs/assets/images/NetSuite_Configure_09.png b/docs/assets/images/NetSuite_Configure_09.png
new file mode 100644
index 000000000000..8da56f22838d
Binary files /dev/null and b/docs/assets/images/NetSuite_Configure_09.png differ
diff --git a/docs/assets/images/NetSuite_Configure_Advanced_10.png b/docs/assets/images/NetSuite_Configure_Advanced_10.png
new file mode 100644
index 000000000000..23fe99498052
Binary files /dev/null and b/docs/assets/images/NetSuite_Configure_Advanced_10.png differ
diff --git a/docs/assets/images/NetSuite_Connect_Bundle_02.png b/docs/assets/images/NetSuite_Connect_Bundle_02.png
new file mode 100644
index 000000000000..c015178873ad
Binary files /dev/null and b/docs/assets/images/NetSuite_Connect_Bundle_02.png differ
diff --git a/docs/assets/images/NetSuite_Connect_Categories_05.png b/docs/assets/images/NetSuite_Connect_Categories_05.png
new file mode 100644
index 000000000000..e71341170129
Binary files /dev/null and b/docs/assets/images/NetSuite_Connect_Categories_05.png differ
diff --git a/docs/assets/images/NetSuite_Connect_Customization_01.png b/docs/assets/images/NetSuite_Connect_Customization_01.png
new file mode 100644
index 000000000000..8a0c53b45d7f
Binary files /dev/null and b/docs/assets/images/NetSuite_Connect_Customization_01.png differ
diff --git a/docs/assets/images/NetSuite_Connect_Expense_Reports_03.png b/docs/assets/images/NetSuite_Connect_Expense_Reports_03.png
new file mode 100644
index 000000000000..44c8fe6c993d
Binary files /dev/null and b/docs/assets/images/NetSuite_Connect_Expense_Reports_03.png differ
diff --git a/docs/assets/images/NetSuite_Expense_Categories_04.png b/docs/assets/images/NetSuite_Expense_Categories_04.png
new file mode 100644
index 000000000000..d13e9f95cfea
Binary files /dev/null and b/docs/assets/images/NetSuite_Expense_Categories_04.png differ
diff --git a/docs/assets/images/NetSuite_HelpScreenshot_07.png b/docs/assets/images/NetSuite_HelpScreenshot_07.png
new file mode 100644
index 000000000000..55cfe532f890
Binary files /dev/null and b/docs/assets/images/NetSuite_HelpScreenshot_07.png differ
diff --git a/docs/assets/images/Workspace_category_toggle.png b/docs/assets/images/Workspace_category_toggle.png
new file mode 100644
index 000000000000..c6af6fe183c0
Binary files /dev/null and b/docs/assets/images/Workspace_category_toggle.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/assets/images/workspace_gl_payroll_codes.png b/docs/assets/images/workspace_gl_payroll_codes.png
new file mode 100644
index 000000000000..6b7770dc01b0
Binary files /dev/null and b/docs/assets/images/workspace_gl_payroll_codes.png differ
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 90baeff59260..a7d4d94adb5d 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -571,6 +571,7 @@ https://community.expensify.com/discussion/4641/how-to-add-a-deposit-only-bank-a
https://community.expensify.com/discussion/5940/how-to-get-reimbursed-outside-the-us-with-wise-for-non-us-employees,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Get-reimbursed-faster-as-a-non-US-employee
https://help.expensify.com/articles/expensify-classic/spending-insights,https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates
https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-notifications,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications
+https://help.expensify.com/articles/expensify-classic/connections/Additional-Travel-Integrations,https://help.expensify.com/articles/expensify-classic/connections/Travel-receipt-integrations
https://help.expensify.com/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan,https://help.expensify.com/Hidden/Upgrade-to-a-Collect-Plan
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports
https://help.expensify.com/articles/new-expensify/expenses-&-payments/pay-an-invoice.html,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Pay-an-invoice
@@ -584,6 +585,7 @@ 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/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
+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
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 1cd1cdf3ee77..361113013ae4 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.0.50
+ 9.0.54
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.50.1
+ 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 7fd36223c03d..cb867d7af0b5 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.50
+ 9.0.54
CFBundleSignature
????
CFBundleVersion
- 9.0.50.1
+ 9.0.54.1
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 078041a521b9..c7c9879bb2ab 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.0.50
+ 9.0.54
CFBundleVersion
- 9.0.50.1
+ 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 b7e42c99e7d0..7202ef76ab66 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
{
"name": "new.expensify",
- "version": "9.0.50-1",
+ "version": "9.0.54-1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.50-1",
+ "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,13 +51,14 @@
"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",
"expo-image-manipulator": "12.0.5",
"fast-equals": "^4.0.3",
"focus-trap-react": "^10.2.3",
+ "howler": "^2.2.4",
"htmlparser2": "^7.2.0",
"idb-keyval": "^6.2.1",
"lodash-es": "4.17.21",
@@ -76,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",
@@ -116,7 +118,6 @@
"react-native-view-shot": "3.8.0",
"react-native-vision-camera": "4.0.0-beta.13",
"react-native-web": "^0.19.12",
- "react-native-web-sound": "^0.1.3",
"react-native-webview": "13.8.6",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
@@ -174,6 +175,7 @@
"@types/base-64": "^1.0.2",
"@types/canvas-size": "^1.2.2",
"@types/concurrently": "^7.0.0",
+ "@types/howler": "^2.2.12",
"@types/jest": "^29.5.2",
"@types/jest-when": "^3.5.2",
"@types/js-yaml": "^4.0.5",
@@ -202,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",
@@ -217,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",
@@ -246,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",
@@ -273,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": {
@@ -613,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",
@@ -3635,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",
@@ -15781,6 +15777,12 @@
"hoist-non-react-statics": "^3.3.0"
}
},
+ "node_modules/@types/howler": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.12.tgz",
+ "integrity": "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g==",
+ "dev": true
+ },
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"dev": true,
@@ -17975,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,
@@ -18363,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",
@@ -22824,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",
@@ -23549,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",
@@ -24073,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",
@@ -25995,7 +25972,8 @@
},
"node_modules/howler": {
"version": "2.2.4",
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
+ "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="
},
"node_modules/hpack.js": {
"version": "2.1.6",
@@ -34093,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",
@@ -34178,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",
@@ -34418,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",
@@ -35419,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",
@@ -35747,16 +35743,6 @@
"react-dom": "^18.0.0"
}
},
- "node_modules/react-native-web-sound": {
- "version": "0.1.3",
- "license": "MIT",
- "dependencies": {
- "howler": "^2.2.1"
- },
- "peerDependencies": {
- "react-native-web": "*"
- }
- },
"node_modules/react-native-web/node_modules/memoize-one": {
"version": "6.0.0",
"license": "MIT"
diff --git a/package.json b/package.json
index 1705abd4cce5..f0425a747967 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.50-1",
+ "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.",
@@ -61,12 +61,13 @@
"e2e-test-runner-build": "node --max-old-space-size=8192 node_modules/.bin/ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/",
"react-compiler-healthcheck": "react-compiler-healthcheck --verbose",
"react-compiler-healthcheck-test": "react-compiler-healthcheck --verbose &> react-compiler-output.txt",
- "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy ",
+ "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy src/libs/SearchParser/baseRules.peggy",
+ "generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy",
"web:prod": "http-server ./dist --cors"
},
"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",
@@ -106,13 +107,14 @@
"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",
"expo-image-manipulator": "12.0.5",
"fast-equals": "^4.0.3",
"focus-trap-react": "^10.2.3",
+ "howler": "^2.2.4",
"htmlparser2": "^7.2.0",
"idb-keyval": "^6.2.1",
"lodash-es": "4.17.21",
@@ -131,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",
@@ -171,7 +174,6 @@
"react-native-view-shot": "3.8.0",
"react-native-vision-camera": "4.0.0-beta.13",
"react-native-web": "^0.19.12",
- "react-native-web-sound": "^0.1.3",
"react-native-webview": "13.8.6",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
@@ -229,6 +231,7 @@
"@types/base-64": "^1.0.2",
"@types/canvas-size": "^1.2.2",
"@types/concurrently": "^7.0.0",
+ "@types/howler": "^2.2.12",
"@types/jest": "^29.5.2",
"@types/jest-when": "^3.5.2",
"@types/js-yaml": "^4.0.5",
@@ -257,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",
@@ -272,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",
@@ -301,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",
@@ -374,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 171dc7ff2c8a..d5680ce34b6f 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -5,18 +5,11 @@ import Config from 'react-native-config';
import * as KeyCommand from 'react-native-key-command';
import type {ValueOf} from 'type-fest';
import type {Video} from './libs/actions/Report';
+import type {MileageRate} from './libs/DistanceRequestUtils';
import BankAccount from './libs/models/BankAccount';
import * as Url from './libs/Url';
import SCREENS from './SCREENS';
import type PlaidBankAccount from './types/onyx/PlaidBankAccount';
-import type {Unit} from './types/onyx/Policy';
-
-type RateAndUnit = {
- unit: Unit;
- rate: number;
- currency: string;
-};
-type CurrencyDefaultMileageRate = Record;
// Creating a default array and object this way because objects ({}) and arrays ([]) are not stable types.
// Freezing the array ensures that it cannot be unintentionally modified.
@@ -304,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',
@@ -478,6 +474,19 @@ const CONST = {
PERSONAL: 'PERSONAL',
},
},
+ NON_USD_BANK_ACCOUNT: {
+ STEP: {
+ COUNTRY: 'CountryStep',
+ BANK_INFO: 'BankInfoStep',
+ BUSINESS_INFO: 'BusinessInfoStep',
+ BENEFICIAL_OWNER_INFO: 'BeneficialOwnerInfoStep',
+ SIGNER_INFO: 'SignerInfoStep',
+ AGREEMENTS: 'AgreementsStep',
+ FINISH: 'FinishStep',
+ },
+ STEP_NAMES: ['1', '2', '3', '4', '5', '6'],
+ STEP_HEADER_HEIGHT: 40,
+ },
INCORPORATION_TYPES: {
LLC: 'LLC',
CORPORATION: 'Corp',
@@ -754,6 +763,11 @@ const CONST = {
DELAYED_SUBMISSION_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports',
// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:',
+ NAVATTIC: {
+ ADMIN_TOUR: 'https://expensify.navattic.com/kh204a7',
+ EMPLOYEE_TOUR: 'https://expensify.navattic.com/35609gb',
+ },
+
OLDDOT_URLS: {
ADMIN_POLICIES_URL: 'admin_policies',
ADMIN_DOMAINS_URL: 'admin_domains',
@@ -825,6 +839,7 @@ const CONST = {
CARD_MISSING_ADDRESS: 'CARDMISSINGADDRESS',
CARD_ISSUED: 'CARDISSUED',
CARD_ISSUED_VIRTUAL: 'CARDISSUEDVIRTUAL',
+ CARD_ASSIGNED: 'CARDASSIGNED',
CHANGE_FIELD: 'CHANGEFIELD', // OldDot Action
CHANGE_POLICY: 'CHANGEPOLICY', // OldDot Action
CHANGE_TYPE: 'CHANGETYPE', // OldDot Action
@@ -1131,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,
},
@@ -1492,14 +1510,18 @@ const CONST = {
EXPORTER: 'exporter',
MARK_CHECKS_TO_BE_PRINTED: 'markChecksToBePrinted',
REIMBURSABLE_ACCOUNT: 'reimbursableAccount',
+ NON_REIMBURSABLE_ACCOUNT: 'nonReimbursableAccount',
REIMBURSABLE: 'reimbursable',
+ NON_REIMBURSABLE: 'nonReimbursable',
+ SHOULD_AUTO_CREATE_VENDOR: 'shouldAutoCreateVendor',
+ NON_REIMBURSABLE_BILL_DEFAULT_VENDOR: 'nonReimbursableBillDefaultVendor',
AUTO_SYNC: 'autoSync',
ENABLE_NEW_CATEGORIES: 'enableNewCategories',
- SHOULD_AUTO_CREATE_VENDOR: 'shouldAutoCreateVendor',
MAPPINGS: {
CLASSES: 'classes',
CUSTOMERS: 'customers',
},
+ IMPORT_ITEMS: 'importItems',
},
QUICKBOOKS_CONFIG: {
@@ -1558,6 +1580,7 @@ const CONST = {
TRACKING_CATEGORY_OPTIONS: {
DEFAULT: 'DEFAULT',
TAG: 'TAG',
+ REPORT_FIELD: 'REPORT_FIELD',
},
},
@@ -1610,11 +1633,16 @@ 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',
JOURNAL_ENTRY: 'JOURNAL_ENTRY',
- NOTHING: 'NOTHING',
},
SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE: {
@@ -1889,7 +1917,7 @@ const CONST = {
QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE: {
CREDIT_CARD: 'CREDIT_CARD_CHARGE',
- JOURNAL_ENTRY: 'JOURNAL_ENTRY',
+ CHECK: 'CHECK',
VENDOR_BILL: 'VENDOR_BILL',
},
@@ -2382,6 +2410,7 @@ const CONST = {
SYNC_STAGE_NAME: {
STARTING_IMPORT_QBO: 'startingImportQBO',
STARTING_IMPORT_XERO: 'startingImportXero',
+ STARTING_IMPORT_QBD: 'startingImportQBD',
QBO_IMPORT_MAIN: 'quickbooksOnlineImportMain',
QBO_IMPORT_CUSTOMERS: 'quickbooksOnlineImportCustomers',
QBO_IMPORT_EMPLOYEES: 'quickbooksOnlineImportEmployees',
@@ -2398,6 +2427,17 @@ const CONST = {
QBO_SYNC_APPLY_CUSTOMERS: 'quickbooksOnlineSyncApplyCustomers',
QBO_SYNC_APPLY_PEOPLE: 'quickbooksOnlineSyncApplyEmployees',
QBO_SYNC_APPLY_CLASSES_LOCATIONS: 'quickbooksOnlineSyncApplyClassesLocations',
+ QBD_IMPORT_TITLE: 'quickbooksDesktopImportTitle',
+ QBD_IMPORT_ACCOUNTS: 'quickbooksDesktopImportAccounts',
+ QBD_IMPORT_APPROVE_CERTIFICATE: 'quickbooksDesktopImportApproveCertificate',
+ QBD_IMPORT_DIMENSIONS: 'quickbooksDesktopImportDimensions',
+ QBD_IMPORT_CLASSES: 'quickbooksDesktopImportClasses',
+ QBD_IMPORT_CUSTOMERS: 'quickbooksDesktopImportCustomers',
+ QBD_IMPORT_VENDORS: 'quickbooksDesktopImportVendors',
+ QBD_IMPORT_EMPLOYEES: 'quickbooksDesktopImportEmployees',
+ QBD_IMPORT_MORE: 'quickbooksDesktopImportMore',
+ QBD_IMPORT_GENERIC: 'quickbooksDesktopImportSavePolicy',
+ QBD_WEB_CONNECTOR_REMINDER: 'quickbooksDesktopWebConnectorReminder',
JOB_DONE: 'jobDone',
XERO_SYNC_STEP: 'xeroSyncStep',
XERO_SYNC_XERO_REIMBURSED_REPORTS: 'xeroSyncXeroReimbursedReports',
@@ -2466,6 +2506,8 @@ const CONST = {
DEFAULT_RATE: 'Default Rate',
RATE_DECIMALS: 3,
FAKE_P2P_ID: '_FAKE_P2P_ID_',
+ MILES_TO_KILOMETERS: 1.609344,
+ KILOMETERS_TO_MILES: 0.621371,
},
TERMS: {
@@ -2509,6 +2551,14 @@ const CONST = {
MASTER_CARD: 'cdf',
VISA: 'vcf',
AMEX: 'gl1025',
+ STRIPE: 'stripe',
+ CITIBANK: 'oauth.citibank.com',
+ CAPITAL_ONE: 'oauth.capitalone.com',
+ BANK_OF_AMERICA: 'oauth.bankofamerica.com',
+ CHASE: 'oauth.chase.com',
+ BREX: 'oauth.brex.com',
+ WELLS_FARGO: 'oauth.wellsfargo.com',
+ AMEX_DIRECT: 'oauth.americanexpressfdx.com',
},
STEP_NAMES: ['1', '2', '3', '4'],
STEP: {
@@ -2562,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',
@@ -2596,6 +2647,15 @@ const CONST = {
WELLS_FARGO: 'Wells Fargo',
OTHER: 'Other',
},
+ BANK_CONNECTIONS: {
+ WELLS_FARGO: 'wellsfargo',
+ BANK_OF_AMERICA: 'bankofamerica',
+ CHASE: 'chase',
+ BREX: 'brex',
+ CAPITAL_ONE: 'capitalone',
+ CITI_BANK: 'citibank',
+ AMEX: 'americanexpressfdx',
+ },
AMEX_CUSTOM_FEED: {
CORPORATE: 'American Express Corporate Cards',
BUSINESS: 'American Express Business Cards',
@@ -2907,6 +2967,7 @@ const CONST = {
SETTINGS: 'settings',
LEAVE_ROOM: 'leaveRoom',
PRIVATE_NOTES: 'privateNotes',
+ DOWNLOAD: 'download',
EXPORT: 'export',
DELETE: 'delete',
MARK_AS_INCOMPLETE: 'markAsIncomplete',
@@ -5525,7 +5586,7 @@ const CONST = {
"rate": 2377,
"unit": "km"
}
- }`) as CurrencyDefaultMileageRate,
+ }`) as Record,
EXIT_SURVEY: {
REASONS: {
@@ -5768,6 +5829,14 @@ const CONST = {
description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}.description` as const,
icon: 'IntacctSquare',
},
+ [this.POLICY.CONNECTIONS.NAME.QBD]: {
+ id: this.POLICY.CONNECTIONS.NAME.QBD,
+ alias: 'qbd',
+ name: this.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.quickbooksDesktop,
+ title: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.QBD}.title` as const,
+ description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.QBD}.description` as const,
+ icon: 'QBDSquare',
+ },
approvals: {
id: 'approvals' as const,
alias: 'approvals' as const,
@@ -5877,6 +5946,21 @@ const CONST = {
// The timeout duration (1 minute) (in milliseconds) before the window reloads due to an error.
ERROR_WINDOW_RELOAD_TIMEOUT: 60000,
+ INDICATOR_STATUS: {
+ HAS_USER_WALLET_ERRORS: 'hasUserWalletErrors',
+ HAS_PAYMENT_METHOD_ERROR: 'hasPaymentMethodError',
+ HAS_POLICY_ERRORS: 'hasPolicyError',
+ HAS_CUSTOM_UNITS_ERROR: 'hasCustomUnitsError',
+ HAS_EMPLOYEE_LIST_ERROR: 'hasEmployeeListError',
+ HAS_SYNC_ERRORS: 'hasSyncError',
+ HAS_SUBSCRIPTION_ERRORS: 'hasSubscriptionError',
+ HAS_REIMBURSEMENT_ACCOUNT_ERRORS: 'hasReimbursementAccountErrors',
+ HAS_LOGIN_LIST_ERROR: 'hasLoginListError',
+ HAS_WALLET_TERMS_ERRORS: 'hasWalletTermsErrors',
+ HAS_LOGIN_LIST_INFO: 'hasLoginListInfo',
+ HAS_SUBSCRIPTION_INFO: 'hasSubscriptionInfo',
+ },
+
DEBUG: {
DETAILS: 'details',
JSON: 'json',
@@ -5904,6 +5988,12 @@ const CONST = {
HAS_CHILD_REPORT_AWAITING_ACTION: 'hasChildReportAwaitingAction',
HAS_MISSING_INVOICE_BANK_ACCOUNT: 'hasMissingInvoiceBankAccount',
},
+
+ RBR_REASONS: {
+ HAS_ERRORS: 'hasErrors',
+ HAS_VIOLATIONS: 'hasViolations',
+ HAS_TRANSACTION_THREAD_VIOLATIONS: 'hasTransactionThreadViolations',
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
@@ -5920,7 +6010,6 @@ export type {
Country,
IOUAction,
IOUType,
- RateAndUnit,
OnboardingPurposeType,
OnboardingCompanySizeType,
IOURequestType,
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 7822ec16b879..e07b03a6d405 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -89,12 +89,12 @@ function Expensify() {
const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
const [userMetadata] = useOnyx(ONYXKEYS.USER_METADATA);
const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false);
- const [isCheckingPublicRoom] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM);
- const [updateAvailable] = useOnyx(ONYXKEYS.UPDATE_AVAILABLE);
- const [updateRequired] = useOnyx(ONYXKEYS.UPDATE_REQUIRED);
+ const [isCheckingPublicRoom] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, {initWithStoredValues: false});
+ const [updateAvailable] = useOnyx(ONYXKEYS.UPDATE_AVAILABLE, {initWithStoredValues: false});
+ const [updateRequired] = useOnyx(ONYXKEYS.UPDATE_REQUIRED, {initWithStoredValues: false});
const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED);
const [screenShareRequest] = useOnyx(ONYXKEYS.SCREEN_SHARE_REQUEST);
- const [focusModeNotification] = useOnyx(ONYXKEYS.FOCUS_MODE_NOTIFICATION);
+ const [focusModeNotification] = useOnyx(ONYXKEYS.FOCUS_MODE_NOTIFICATION, {initWithStoredValues: false});
const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH);
useEffect(() => {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 0b69fe9be80b..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',
@@ -441,6 +445,9 @@ const ONYXKEYS = {
/** Stores recently used currencies */
RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies',
+ /** Company cards custom names */
+ NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES: 'nvp_expensify_ccCustomNames',
+
/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
@@ -849,7 +856,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean;
- [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: string;
+ [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeed;
};
type OnyxValuesMapping = {
@@ -902,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;
@@ -1003,6 +1011,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.LAST_ROUTE]: string;
[ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean;
[ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean;
+ [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 98ea64bc65b4..c346da6cadcb 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -21,6 +21,7 @@ const PUBLIC_SCREENS_ROUTES = {
ROOT: '',
TRANSITION_BETWEEN_APPS: 'transition',
CONNECTION_COMPLETE: 'connection-complete',
+ BANK_CONNECTION_COMPLETE: 'bank-connection-complete',
VALIDATE_LOGIN: 'v/:accountID/:validateCode',
UNLINK_LOGIN: 'u/:accountID/:validateCode',
APPLE_SIGN_IN: 'sign-in-with-apple',
@@ -147,11 +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,
- },
- SETTINGS_DELEGATE_MAGIC_CODE: {
- route: 'settings/security/delegate/:login/role/:role/magic-code',
- getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/magic-code` 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',
@@ -231,7 +231,6 @@ const ROUTES = {
route: 'settings/profile/contact-methods/:contactMethod/details',
getRoute: (contactMethod: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`, backTo),
},
- SETINGS_CONTACT_METHOD_VALIDATE_ACTION: 'settings/profile/contact-methods/validate-action',
SETTINGS_NEW_CONTACT_METHOD: {
route: 'settings/profile/contact-methods/new',
getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods/new', backTo),
@@ -448,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),
@@ -466,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?',
@@ -518,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 = '') =>
@@ -669,6 +735,22 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/date-select',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/date-select` as const,
},
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/export/company-card-expense-account/account-select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/export/company-card-expense-account/account-select` as const,
+ },
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_SELECT: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/export/company-card-expense-account/card-select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/export/company-card-expense-account/card-select` as const,
+ },
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/export/company-card-expense-account/default-vendor-select',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/export/company-card-expense-account/default-vendor-select` as const,
+ },
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/export/company-card-expense-account',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/export/company-card-expense-account` as const,
+ },
WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/advanced',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/advanced` as const,
@@ -733,6 +815,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/import/customers/displayed_as',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/import/customers/displayed_as` as const,
},
+ POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_ITEMS: {
+ route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/import/items',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/import/items` as const,
+ },
WORKSPACE_PROFILE_NAME: {
route: 'settings/workspaces/:policyID/profile/name',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/name` as const,
@@ -942,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',
@@ -1072,7 +1158,7 @@ const ROUTES = {
},
WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: {
route: 'settings/workspaces/:policyID/company-cards/:feed/assign-card',
- getRoute: (policyID: string, feed: string) => `settings/workspaces/${policyID}/company-cards/${feed}/assign-card` as const,
+ getRoute: (policyID: string, feed: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/company-cards/${feed}/assign-card`, backTo),
},
WORKSPACE_COMPANY_CARD_DETAILS: {
route: 'settings/workspaces/:policyID/company-cards/:bank/:cardID',
@@ -1364,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 719c67f0365b..2e44c5ed5695 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -74,7 +74,6 @@ const SCREENS = {
DISPLAY_NAME: 'Settings_Display_Name',
CONTACT_METHODS: 'Settings_ContactMethods',
CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails',
- CONTACT_METHOD_VALIDATE_ACTION: 'Settings_ValidateContactMethodAction',
NEW_CONTACT_METHOD: 'Settings_NewContactMethod',
STATUS_CLEAR_AFTER: 'Settings_Status_Clear_After',
STATUS_CLEAR_AFTER_DATE: 'Settings_Status_Clear_After_Date',
@@ -134,7 +133,6 @@ const SCREENS = {
ADD_DELEGATE: 'Settings_Delegate_Add',
DELEGATE_ROLE: 'Settings_Delegate_Role',
DELEGATE_CONFIRM: 'Settings_Delegate_Confirm',
- DELEGATE_MAGIC_CODE: 'Settings_Delegate_Magic_Code',
UPDATE_DELEGATE_ROLE: 'Settings_Delegate_Update_Role',
UPDATE_DELEGATE_ROLE_MAGIC_CODE: 'Settings_Delegate_Update_Magic_Code',
},
@@ -177,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',
@@ -220,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: {
@@ -246,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',
@@ -258,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',
@@ -316,6 +332,13 @@ 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',
+ QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Non_Reimbursable_Default_Vendor_Select',
QUICKBOOKS_DESKTOP_ADVANCED: 'Policy_Accounting_Quickbooks_Desktop_Advanced',
QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Date_Select',
QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER: 'Workspace_Accounting_Quickbooks_Desktop_Export_Preferred_Exporter',
@@ -332,6 +355,7 @@ const SCREENS = {
QUICKBOOKS_DESKTOP_CLASSES_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Desktop_Import_Classes_Dipslayed_As',
QUICKBOOKS_DESKTOP_CUSTOMERS: 'Policy_Accounting_Quickbooks_Desktop_Import_Customers',
QUICKBOOKS_DESKTOP_CUSTOMERS_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Desktop_Import_Customers_Dipslayed_As',
+ QUICKBOOKS_DESKTOP_ITEMS: 'Policy_Accounting_Quickbooks_Desktop_Import_Items',
XERO_IMPORT: 'Policy_Accounting_Xero_Import',
XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers',
XERO_CHART_OF_ACCOUNTS: 'Policy_Accounting_Xero_Import_Chart_Of_Accounts',
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/AttachmentOfflineIndicator.tsx b/src/components/AttachmentOfflineIndicator.tsx
index d425e6f18e0e..4ff1940ba004 100644
--- a/src/components/AttachmentOfflineIndicator.tsx
+++ b/src/components/AttachmentOfflineIndicator.tsx
@@ -37,7 +37,7 @@ function AttachmentOfflineIndicator({isPreview = false}: AttachmentOfflineIndica
return (
@@ -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/CategoryPicker.tsx b/src/components/CategoryPicker.tsx
index a8117fc1f4a0..33d97c6909f5 100644
--- a/src/components/CategoryPicker.tsx
+++ b/src/components/CategoryPicker.tsx
@@ -44,20 +44,14 @@ function CategoryPicker({selectedCategory, policyID, onSubmit}: CategoryPickerPr
const [sections, headerMessage, shouldShowTextInput] = useMemo(() => {
const categories = policyCategories ?? policyCategoriesDraft ?? {};
const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter?.((p) => !isEmptyObject(p));
- const {categoryOptions} = OptionsListUtils.getFilteredOptions(
- [],
- [],
- [],
- debouncedSearchValue,
+ const {categoryOptions} = OptionsListUtils.getFilteredOptions({
+ searchValue: debouncedSearchValue,
selectedOptions,
- [],
- false,
- false,
- true,
+ includeP2P: false,
+ includeCategories: true,
categories,
- validPolicyRecentlyUsedCategories,
- false,
- );
+ recentlyUsedCategories: validPolicyRecentlyUsedCategories,
+ });
const categoryData = categoryOptions?.at(0)?.data ?? [];
const header = OptionsListUtils.getHeaderMessageForNonUserList(categoryData.length > 0, debouncedSearchValue);
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 bf1315b452c6..6f5a983e4250 100644
--- a/src/components/ConnectToQuickbooksDesktopFlow/index.tsx
+++ b/src/components/ConnectToQuickbooksDesktopFlow/index.tsx
@@ -1,19 +1,17 @@
import {useEffect} from 'react';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyAction from '@userActions/Policy/Policy';
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(() => {
if (isSmallScreenWidth) {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL.getRoute(policyID));
} else {
- // Since QBD doesn't support Taxes, we should disable them from the LHN when connecting to QBD
- PolicyAction.enablePolicyTaxes(policyID, false);
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_SETUP_MODAL.getRoute(policyID));
}
}, [isSmallScreenWidth, policyID]);
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 aa6c3e8dfdfb..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';
@@ -56,7 +57,7 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef emojiPopoverAnchorRef.current ?? emojiPopoverAnchorRef?.current, []);
+ const getEmojiPopoverAnchor = useCallback(() => emojiPopoverAnchorRef.current ?? (emojiPopoverAnchorRef as EmojiPopoverAnchor), []);
/**
* Show the emoji picker menu.
@@ -117,7 +118,7 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef
{title}
{typeof subtitle === 'string' ? {subtitle} : subtitle}
- {!!buttonText && !!buttonAction && (
-
- )}
+
+ {buttons?.map(({buttonText, buttonAction, success}, index) => (
+
+
+
+ ))}
+
diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts
index 16c65781b461..354141ae672c 100644
--- a/src/components/EmptyStateComponent/types.ts
+++ b/src/components/EmptyStateComponent/types.ts
@@ -9,14 +9,14 @@ import type IconAsset from '@src/types/utils/IconAsset';
type ValidSkeletons = typeof SearchRowSkeleton | typeof TableRowSkeleton;
type MediaTypes = ValueOf;
+type Button = {buttonText?: string; buttonAction?: () => void; success?: boolean};
type SharedProps = {
SkeletonComponent: ValidSkeletons;
title: string;
titleStyles?: StyleProp;
subtitle: string | React.ReactNode;
- buttonText?: string;
- buttonAction?: () => void;
+ buttons?: Button[];
containerStyles?: StyleProp;
headerStyles?: StyleProp;
headerMediaType: T;
diff --git a/src/components/FilePicker/index.native.tsx b/src/components/FilePicker/index.native.tsx
index cc9c73d72c56..4b381713e44b 100644
--- a/src/components/FilePicker/index.native.tsx
+++ b/src/components/FilePicker/index.native.tsx
@@ -155,6 +155,7 @@ function FilePicker({children}: FilePickerProps) {
openPicker: ({onPicked, onCanceled: newOnCanceled}) => 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/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx
index 7c2f5579332a..ecf72f89134b 100644
--- a/src/components/FloatingActionButton.tsx
+++ b/src/components/FloatingActionButton.tsx
@@ -5,12 +5,10 @@ import type {GestureResponderEvent, Role, Text, View} from 'react-native';
import {Platform} from 'react-native';
import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Svg, {Path} from 'react-native-svg';
-import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import {PressableWithoutFeedback} from './Pressable';
-import Tooltip from './Tooltip/PopoverAnchorTooltip';
const AnimatedPath = Animated.createAnimatedComponent(Path);
AnimatedPath.displayName = 'AnimatedPath';
@@ -57,7 +55,6 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
const {success, buttonDefaultBG, textLight, textDark} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
- const {translate} = useLocalize();
const fabPressable = useRef(null);
const sharedValue = useSharedValue(isActive ? 1 : 0);
const buttonRef = ref;
@@ -99,34 +96,32 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
};
return (
-
- {
- fabPressable.current = el ?? null;
- if (buttonRef && 'current' in buttonRef) {
- buttonRef.current = el ?? null;
- }
- }}
- style={[styles.h100, styles.bottomTabBarItem]}
- accessibilityLabel={accessibilityLabel}
- onPress={toggleFabAction}
- onLongPress={() => {}}
- role={role}
- shouldUseHapticsOnLongPress={false}
- >
-
-
-
-
-
-
-
+ {
+ fabPressable.current = el ?? null;
+ if (buttonRef && 'current' in buttonRef) {
+ buttonRef.current = el ?? null;
+ }
+ }}
+ style={[styles.h100, styles.bottomTabBarItem]}
+ accessibilityLabel={accessibilityLabel}
+ onPress={toggleFabAction}
+ onLongPress={() => {}}
+ role={role}
+ shouldUseHapticsOnLongPress={false}
+ >
+
+
+
+
+
+
);
}
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)}
+ fallbackIconBackground={theme.highlightBG}
+ fallbackIconColor={theme.border}
/>
);
@@ -102,6 +113,7 @@ function ImageRenderer({tnode}: ImageRendererProps) {
shouldUseHapticsOnLongPress
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
+ disabled={hasLoadFailed}
>
{thumbnailImageComponent}
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 e1843ee506d5..b71f4db246a8 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -191,7 +191,7 @@ function HeaderWithBackButton({
/>
)}
{middleContent}
-
+
{children}
{shouldShowDownloadButton && (
@@ -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/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index cd9c97105ff0..90f0e0d8a151 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -93,6 +93,7 @@ import FlagLevelTwo from '@assets/images/flag_level_02.svg';
import FlagLevelThree from '@assets/images/flag_level_03.svg';
import Folder from '@assets/images/folder.svg';
import Fullscreen from '@assets/images/fullscreen.svg';
+import GalleryNotFound from '@assets/images/gallery-not-found.svg';
import Gallery from '@assets/images/gallery.svg';
import Gear from '@assets/images/gear.svg';
import Globe from '@assets/images/globe.svg';
@@ -404,4 +405,5 @@ export {
Bookmark,
Star,
QBDSquare,
+ GalleryNotFound,
};
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index bae8f6af1ab2..18ae1792686f 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -12,6 +12,7 @@ import WellsFargoCompanyCardDetail from '@assets/images/companyCards/card-wellsf
import OtherCompanyCardDetail from '@assets/images/companyCards/card=-generic.svg';
import CompanyCardsEmptyState from '@assets/images/companyCards/emptystate__card-pos.svg';
import MasterCardCompanyCards from '@assets/images/companyCards/mastercard.svg';
+import PendingBank from '@assets/images/companyCards/pending-bank.svg';
import CompanyCardsPendingState from '@assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg';
import VisaCompanyCards from '@assets/images/companyCards/visa.svg';
import EmptyCardState from '@assets/images/emptystate__expensifycard.svg';
@@ -207,6 +208,7 @@ export {
Approval,
WalletAlt,
Workflows,
+ PendingBank,
ThreeLeggedLaptopWoman,
House,
Alert,
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 (
item.value === colName);
+ const finalIndex = defaultSelectedIndex !== -1 ? defaultSelectedIndex : 0;
useEffect(() => {
if (defaultSelectedIndex === -1) {
@@ -201,7 +202,7 @@ function ImportColumn({column, columnName, columnRoles, columnIndex}: ImportColu
onOptionSelected={(option) => {
setColumnName(columnIndex, option.value);
}}
- defaultSelectedIndex={defaultSelectedIndex}
+ defaultSelectedIndex={finalIndex}
options={options}
/>
diff --git a/src/components/ImportOnyxState/BaseImportOnyxState.tsx b/src/components/ImportOnyxState/BaseImportOnyxState.tsx
index 216a6ddf76e4..c6c80d03b58f 100644
--- a/src/components/ImportOnyxState/BaseImportOnyxState.tsx
+++ b/src/components/ImportOnyxState/BaseImportOnyxState.tsx
@@ -19,6 +19,9 @@ function BaseImportOnyxState({
}) {
const {translate} = useLocalize();
const styles = useThemeStyles();
+
+ // 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 {isSmallScreenWidth} = useResponsiveLayout();
return (
diff --git a/src/components/ImportSpreadsheet.tsx b/src/components/ImportSpreadsheet.tsx
index b68c773bc12d..6865e8ae6c82 100644
--- a/src/components/ImportSpreadsheet.tsx
+++ b/src/components/ImportSpreadsheet.tsx
@@ -42,6 +42,8 @@ function ImportSpreedsheet({backTo, goTo}: ImportSpreedsheetProps) {
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState();
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/Indicator.tsx b/src/components/Indicator.tsx
index 4d352b6a6cde..105399936b43 100644
--- a/src/components/Indicator.tsx
+++ b/src/components/Indicator.tsx
@@ -1,109 +1,17 @@
import React from 'react';
import {StyleSheet, View} from 'react-native';
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
-import {useOnyx, withOnyx} from 'react-native-onyx';
-import useTheme from '@hooks/useTheme';
+import useIndicatorStatus from '@hooks/useIndicatorStatus';
import useThemeStyles from '@hooks/useThemeStyles';
-import {isConnectionInProgress} from '@libs/actions/connections';
-import * as PolicyUtils from '@libs/PolicyUtils';
-import * as SubscriptionUtils from '@libs/SubscriptionUtils';
-import * as UserUtils from '@libs/UserUtils';
-import * as PaymentMethods from '@userActions/PaymentMethods';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {BankAccountList, FundList, LoginList, Policy, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx';
-type CheckingMethod = () => boolean;
-
-type IndicatorOnyxProps = {
- /** All the user's policies (from Onyx via withFullPolicy) */
- policies: OnyxCollection;
-
- /** List of bank accounts */
- bankAccountList: OnyxEntry;
-
- /** List of user cards */
- fundList: OnyxEntry;
-
- /** The user's wallet (coming from Onyx) */
- userWallet: OnyxEntry;
-
- /** Bank account attached to free plan */
- reimbursementAccount: OnyxEntry;
-
- /** Information about the user accepting the terms for payments */
- walletTerms: OnyxEntry;
-
- /** Login list for the user that is signed in */
- loginList: OnyxEntry;
-};
-
-type IndicatorProps = IndicatorOnyxProps;
-
-function Indicator({reimbursementAccount, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) {
- const theme = useTheme();
+function Indicator() {
const styles = useThemeStyles();
- const [allConnectionSyncProgresses] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}`);
-
- // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and
- // those should be cleaned out before doing any error checking
- const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id));
-
- // All of the error & info-checking methods are put into an array. This is so that using _.some() will return
- // early as soon as the first error / info condition is returned. This makes the checks very efficient since
- // we only care if a single error / info condition exists anywhere.
- const errorCheckingMethods: CheckingMethod[] = [
- () => Object.keys(userWallet?.errors ?? {}).length > 0,
- () => PaymentMethods.hasPaymentMethodError(bankAccountList, fundList),
- () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError),
- () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError),
- () => Object.values(cleanPolicies).some(PolicyUtils.hasEmployeeListError),
- () =>
- Object.values(cleanPolicies).some((cleanPolicy) =>
- PolicyUtils.hasSyncError(
- cleanPolicy,
- isConnectionInProgress(allConnectionSyncProgresses?.[`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${cleanPolicy?.id}`], cleanPolicy),
- ),
- ),
- () => SubscriptionUtils.hasSubscriptionRedDotError(),
- () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
- () => !!loginList && UserUtils.hasLoginListError(loginList),
-
- // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
- () => Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID,
- ];
- const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList), () => SubscriptionUtils.hasSubscriptionGreenDotInfo()];
- const shouldShowErrorIndicator = errorCheckingMethods.some((errorCheckingMethod) => errorCheckingMethod());
- const shouldShowInfoIndicator = !shouldShowErrorIndicator && infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod());
+ const {indicatorColor, status} = useIndicatorStatus();
- const indicatorColor = shouldShowErrorIndicator ? theme.danger : theme.success;
const indicatorStyles = [styles.alignItemsCenter, styles.justifyContentCenter, styles.statusIndicator(indicatorColor)];
- return (shouldShowErrorIndicator || shouldShowInfoIndicator) && ;
+ return !!status && ;
}
Indicator.displayName = 'Indicator';
-export default withOnyx({
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- },
- bankAccountList: {
- key: ONYXKEYS.BANK_ACCOUNT_LIST,
- },
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
- fundList: {
- key: ONYXKEYS.FUND_LIST,
- },
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
- walletTerms: {
- key: ONYXKEYS.WALLET_TERMS,
- },
- loginList: {
- key: ONYXKEYS.LOGIN_LIST,
- },
-})(Indicator);
+export default Indicator;
diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx
index fd681546c470..b846449faafd 100644
--- a/src/components/KYCWall/BaseKYCWall.tsx
+++ b/src/components/KYCWall/BaseKYCWall.tsx
@@ -1,8 +1,8 @@
+/* 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';
-import {withOnyx} from 'react-native-onyx';
-import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
import * as BankAccounts from '@libs/actions/BankAccounts';
import getClickedTargetLocation from '@libs/getClickedTargetLocation';
@@ -16,7 +16,6 @@ import * as Wallet from '@userActions/Wallet';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {BankAccountList, FundList, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import viewRef from '@src/types/utils/viewRef';
import type {AnchorPosition, DomRect, KYCWallProps, PaymentMethod} from './types';
@@ -24,25 +23,6 @@ import type {AnchorPosition, DomRect, KYCWallProps, PaymentMethod} from './types
// This sets the Horizontal anchor position offset for POPOVER MENU.
const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20;
-type BaseKYCWallOnyxProps = {
- /** The user's wallet */
- userWallet: OnyxEntry;
-
- /** Information related to the last step of the wallet activation flow */
- walletTerms: OnyxEntry;
-
- /** List of user's cards */
- fundList: OnyxEntry;
-
- /** List of bank accounts */
- bankAccountList: OnyxEntry;
-
- /** The reimbursement account linked to the Workspace */
- reimbursementAccount: OnyxEntry;
-};
-
-type BaseKYCWallProps = KYCWallProps & BaseKYCWallOnyxProps;
-
// This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow
// before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it
// to render the AddPaymentMethodMenu in the correct location.
@@ -53,22 +33,23 @@ function KYCWall({
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
},
- bankAccountList = {},
chatReportID = '',
children,
enablePaymentsRoute,
- fundList,
iouReport,
onSelectPaymentMethod = () => {},
onSuccessfulKYC,
- reimbursementAccount,
shouldIncludeDebitCard = true,
shouldListenForResize = false,
source,
- userWallet,
- walletTerms,
shouldShowPersonalBankAccountOption = false,
-}: BaseKYCWallProps) {
+}: KYCWallProps) {
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
+ const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
+ const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);
+ const [bankAccountList = {}] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+
const anchorRef = useRef(null);
const transferBalanceButtonRef = useRef(null);
@@ -270,21 +251,4 @@ function KYCWall({
KYCWall.displayName = 'BaseKYCWall';
-export default withOnyx({
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
- walletTerms: {
- key: ONYXKEYS.WALLET_TERMS,
- },
- fundList: {
- key: ONYXKEYS.FUND_LIST,
- },
- bankAccountList: {
- key: ONYXKEYS.BANK_ACCOUNT_LIST,
- },
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
-})(KYCWall);
+export default KYCWall;
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..526a090afb73 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -333,6 +333,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;
@@ -432,6 +435,7 @@ function MenuItem(
renderTooltipContent,
shouldShowSelectedItemCheck = false,
onHideTooltip,
+ shouldIconUseAutoWidthStyle = false,
}: MenuItemProps,
ref: PressableRef,
) {
@@ -623,10 +627,22 @@ function MenuItem(
/>
)}
{!icon && shouldPutLeftPaddingWhenNoIcon && (
-
+
)}
{icon && !Array.isArray(icon) && (
-
+
{typeof icon !== 'string' &&
iconType === CONST.ICON_TYPE_ICON &&
(!shouldShowLoadingSpinnerIcon ? (
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index f51fe7e37acd..85a2298f63d6 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -61,6 +61,7 @@ function BaseModal(
const StyleUtils = useStyleUtils();
const {windowWidth, windowHeight} = useWindowDimensions();
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply correct modal width
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const keyboardStateContextValue = useKeyboardState();
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 6fbe8bd33839..680a38843f24 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -7,8 +7,10 @@ import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import {getCurrentUserAccountID} from '@libs/actions/Report';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import Navigation from '@libs/Navigation/Navigation';
+import isReportOpenInRHP from '@libs/Navigation/isReportOpenInRHP';
+import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -60,6 +62,8 @@ type MoneyReportHeaderProps = {
};
function MoneyReportHeader({policy, report: moneyRequestReport, transactionThreadReportID, reportActions, onBackButtonPress}: MoneyReportHeaderProps) {
+ // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID ?? '-1'}`);
const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID ?? '-1'}`);
@@ -90,6 +94,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport);
const isOnHold = TransactionUtils.isOnHold(transaction);
const isDeletedParentAction = !!requestParentReportAction && ReportActionsUtils.isDeletedAction(requestParentReportAction);
+ const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? '');
// Only the requestor can delete the request, admins can only edit it.
const isActionOwner =
@@ -133,9 +138,17 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);
- const shouldShowSubmitButton = !!moneyRequestReport && isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations && !shouldShowBrokenConnectionViolation;
-
+ const currentUserAccountID = getCurrentUserAccountID();
const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+
+ const shouldShowSubmitButton =
+ !!moneyRequestReport &&
+ isDraft &&
+ reimbursableSpend !== 0 &&
+ !hasAllPendingRTERViolations &&
+ !shouldShowBrokenConnectionViolation &&
+ (moneyRequestReport?.ownerAccountID === currentUserAccountID || isAdmin || moneyRequestReport?.managerID === currentUserAccountID);
+
const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(moneyRequestReport);
const shouldShowSettlementButton =
@@ -147,7 +160,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions;
const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
const shouldShowAnyButton =
- shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || shouldShowMarkAsCashButton || shouldShowExportIntegrationButton;
+ isDuplicate ||
+ shouldShowSettlementButton ||
+ shouldShowApproveButton ||
+ shouldShowSubmitButton ||
+ shouldShowNextStep ||
+ shouldShowMarkAsCashButton ||
+ shouldShowExportIntegrationButton;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency);
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy);
@@ -157,6 +176,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
+ const isReportInRHP = isReportOpenInRHP(navigationRef?.getRootState());
+ const shouldDisplaySearchRouter = !isReportInRHP;
+
const confirmPayment = useCallback(
(type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
if (!type || !chatReport) {
@@ -260,6 +282,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
};
const statusBarProps = getStatusBarProps();
+ const shouldAddGapToContents =
+ shouldUseNarrowLayout &&
+ (isDuplicate || shouldShowSettlementButton || !!shouldShowExportIntegrationButton || shouldShowSubmitButton || shouldShowMarkAsCashButton) &&
+ (!!statusBarProps || shouldShowNextStep);
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(
@@ -267,6 +293,11 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
[chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled],
);
+ const shouldDuplicateButtonBeSuccess = useMemo(
+ () => isDuplicate && !shouldShowSettlementButton && !shouldShowExportIntegrationButton && !shouldShowSubmitButton && !shouldShowMarkAsCashButton,
+ [isDuplicate, shouldShowSettlementButton, shouldShowExportIntegrationButton, shouldShowSubmitButton, shouldShowMarkAsCashButton],
+ );
+
useEffect(() => {
if (isLoadingHoldUseExplained) {
return;
@@ -309,11 +340,23 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
report={moneyRequestReport}
policy={policy}
shouldShowBackButton={shouldUseNarrowLayout}
- shouldDisplaySearchRouter
+ shouldDisplaySearchRouter={shouldDisplaySearchRouter}
onBackButtonPress={onBackButtonPress}
// Shows border if no buttons or banners are showing below the header
shouldShowBorderBottom={!isMoreContentShown}
>
+ {isDuplicate && !shouldUseNarrowLayout && (
+
+ {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(transactionThreadReportID ?? '', Navigation.getReportRHPActiveRoute()));
+ }}
+ />
+
+ )}
{shouldShowSettlementButton && !shouldUseNarrowLayout && (
{isMoreContentShown && (
-
- {shouldShowSettlementButton && shouldUseNarrowLayout && (
-
- )}
- {shouldShowExportIntegrationButton && shouldUseNarrowLayout && (
-
- )}
- {shouldShowSubmitButton && shouldUseNarrowLayout && (
- IOU.submitReport(moneyRequestReport)}
- isDisabled={shouldDisableSubmitButton}
- />
- )}
- {shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
-
- )}
+
+
+ {isDuplicate && shouldUseNarrowLayout && (
+ {
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(transactionThreadReportID ?? '', Navigation.getReportRHPActiveRoute()));
+ }}
+ />
+ )}
+ {shouldShowSettlementButton && shouldUseNarrowLayout && (
+
+ )}
+ {shouldShowExportIntegrationButton && shouldUseNarrowLayout && (
+
+ )}
+ {shouldShowSubmitButton && shouldUseNarrowLayout && (
+ IOU.submitReport(moneyRequestReport)}
+ isDisabled={shouldDisableSubmitButton}
+ />
+ )}
+ {shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
+
+ )}
+
{shouldShowNextStep && }
{statusBarProps && (
;
- /** Unit and rate used for if the expense is a distance expense */
- mileageRates: OnyxEntry>;
-
/** Mileage rate default for the policy */
defaultMileageRate: OnyxEntry;
@@ -89,6 +85,9 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
/** IOU amount */
iouAmount: number;
+ /** IOU attendees list */
+ iouAttendees?: Attendee[];
+
/** IOU comment */
iouComment?: string;
@@ -181,7 +180,6 @@ function MoneyRequestConfirmationList({
iouAmount,
policyCategories: policyCategoriesReal,
policyCategoriesDraft,
- mileageRates: mileageRatesReal,
isDistanceRequest = false,
policy: policyReal,
policyDraft,
@@ -199,6 +197,7 @@ function MoneyRequestConfirmationList({
policyID = '',
reportID = '',
receiptPath = '',
+ iouAttendees,
iouComment,
receiptFilename = '',
iouCreated,
@@ -216,10 +215,6 @@ function MoneyRequestConfirmationList({
}: MoneyRequestConfirmationListProps) {
const policy = policyReal ?? policyDraft;
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;
- const [mileageRatesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID || '-1'}`, {
- selector: (selectedPolicy: OnyxEntry) => DistanceRequestUtils.getMileageRates(selectedPolicy),
- });
- const mileageRates = isEmptyObject(mileageRatesReal) ? mileageRatesDraft : mileageRatesReal;
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
@@ -247,15 +242,11 @@ function MoneyRequestConfirmationList({
IOU.setCustomUnitRateID(transactionID, rateID);
}, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, canUseP2PDistanceRequests, transactionID, isDistanceRequest]);
- const policyCurrency = policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD;
-
- const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency) : mileageRates?.[customUnitRateID] ?? defaultMileageRate;
-
- const {unit, rate} = mileageRate ?? {};
-
+ const mileageRate = DistanceRequestUtils.getRate({transaction, policy, policyDraft});
+ const rate = mileageRate.rate;
const prevRate = usePrevious(rate);
-
- const currency = (mileageRate as MileageRate)?.currency ?? policyCurrency;
+ const unit = mileageRate.unit;
+ const currency = mileageRate.currency ?? CONST.CURRENCY.USD;
const prevCurrency = usePrevious(currency);
// A flag for showing the categories field
@@ -301,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('');
@@ -396,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 {
@@ -753,6 +749,7 @@ function MoneyRequestConfirmationList({
}
if (selectedParticipants.length === 0) {
+ setFormError('iou.error.noParticipantSelected');
return;
}
if (!isEditingSplitBill && isMerchantRequired && (isMerchantEmpty || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction)))) {
@@ -839,12 +836,10 @@ function MoneyRequestConfirmationList({
}
const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.PAY;
- const shouldDisableButton = selectedParticipants.length === 0;
const button = shouldShowSettlementButton ? (
confirm(value as PaymentMethodType)}
options={splitOrRequestOptions}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
@@ -893,7 +887,6 @@ function MoneyRequestConfirmationList({
isReadOnly,
isTypeSplit,
iouType,
- selectedParticipants.length,
confirm,
bankAccountRoute,
iouCurrencyCode,
@@ -915,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}
@@ -964,6 +959,7 @@ function MoneyRequestConfirmationList({
shouldSingleExecuteRowSelect
canSelectMultiple={false}
shouldPreventDefaultFocusOnSelectRow
+ shouldShowListEmptyContent={false}
footerContent={footerContent}
listFooterContent={listFooterContent}
containerStyle={[styles.flexBasisAuto]}
@@ -990,10 +986,6 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
selector: DistanceRequestUtils.getDefaultMileageRate,
},
- mileageRates: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- selector: (policy: OnyxEntry) => DistanceRequestUtils.getMileageRates(policy),
- },
policy: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
},
@@ -1029,6 +1021,7 @@ export default withOnyx 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: (
{shouldShowMarkAsCashButton && !shouldUseNarrowLayout && (
diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx
index f098188de270..537919622540 100644
--- a/src/components/OptionListContextProvider.tsx
+++ b/src/components/OptionListContextProvider.tsx
@@ -145,6 +145,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp
}, [loadOptions]);
return (
+ // eslint-disable-next-line react-compiler/react-compiler
({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/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
similarity index 97%
rename from src/components/Pressable/GenericPressable/BaseGenericPressable.tsx
rename to src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
index 5237ff486631..1765560eaae3 100644
--- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx
+++ b/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
@@ -3,6 +3,8 @@ import React, {forwardRef, useCallback, useEffect, useMemo, useState} from 'reac
import type {GestureResponderEvent, View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import {Pressable} from 'react-native';
+import type {PressableRef} from '@components/Pressable/GenericPressable/types';
+import type PressableProps from '@components/Pressable/GenericPressable/types';
import useSingleExecution from '@hooks/useSingleExecution';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -10,8 +12,6 @@ import Accessibility from '@libs/Accessibility';
import HapticFeedback from '@libs/HapticFeedback';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import CONST from '@src/CONST';
-import type {PressableRef} from './types';
-import type PressableProps from './types';
function GenericPressable(
{
@@ -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/Pressable/GenericPressable/index.native.tsx b/src/components/Pressable/GenericPressable/implementation/index.native.tsx
similarity index 78%
rename from src/components/Pressable/GenericPressable/index.native.tsx
rename to src/components/Pressable/GenericPressable/implementation/index.native.tsx
index c17163677cbe..5ce313d21ea6 100644
--- a/src/components/Pressable/GenericPressable/index.native.tsx
+++ b/src/components/Pressable/GenericPressable/implementation/index.native.tsx
@@ -1,7 +1,7 @@
import React, {forwardRef} from 'react';
+import type {PressableRef} from '@components/Pressable/GenericPressable/types';
+import type PressableProps from '@components/Pressable/GenericPressable/types';
import GenericPressable from './BaseGenericPressable';
-import type {PressableRef} from './types';
-import type PressableProps from './types';
function NativeGenericPressable(props: PressableProps, ref: PressableRef) {
return (
diff --git a/src/components/Pressable/GenericPressable/implementation/index.tsx b/src/components/Pressable/GenericPressable/implementation/index.tsx
new file mode 100644
index 000000000000..b52eea83fdcb
--- /dev/null
+++ b/src/components/Pressable/GenericPressable/implementation/index.tsx
@@ -0,0 +1,33 @@
+import React, {forwardRef} from 'react';
+import type {Role} from 'react-native';
+import type {PressableRef} from '@components/Pressable/GenericPressable/types';
+import type PressableProps from '@components/Pressable/GenericPressable/types';
+import GenericPressable from './BaseGenericPressable';
+
+function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: PressableRef) {
+ const accessible = props.accessible ?? props.accessible === undefined ? true : props.accessible;
+
+ return (
+
+ );
+}
+
+WebGenericPressable.displayName = 'WebGenericPressable';
+
+export default forwardRef(WebGenericPressable);
diff --git a/src/components/Pressable/GenericPressable/index.e2e.tsx b/src/components/Pressable/GenericPressable/index.e2e.tsx
new file mode 100644
index 000000000000..5d997977a7e0
--- /dev/null
+++ b/src/components/Pressable/GenericPressable/index.e2e.tsx
@@ -0,0 +1,34 @@
+import React, {forwardRef, useEffect} from 'react';
+import GenericPressable from './implementation';
+import type {PressableRef} from './types';
+import type PressableProps from './types';
+
+const pressableRegistry = new Map();
+
+function getPressableProps(nativeID: string): PressableProps | undefined {
+ return pressableRegistry.get(nativeID);
+}
+
+function E2EGenericPressableWrapper(props: PressableProps, ref: PressableRef) {
+ useEffect(() => {
+ const nativeId = props.nativeID;
+ if (!nativeId) {
+ return;
+ }
+ console.debug(`[E2E] E2EGenericPressableWrapper: Registering pressable with nativeID: ${nativeId}`);
+ pressableRegistry.set(nativeId, props);
+ }, [props]);
+
+ return (
+
+ );
+}
+
+E2EGenericPressableWrapper.displayName = 'E2EGenericPressableWrapper';
+
+export default forwardRef(E2EGenericPressableWrapper);
+export {getPressableProps};
diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx
index 371b4d169714..c3d3a2b2c856 100644
--- a/src/components/Pressable/GenericPressable/index.tsx
+++ b/src/components/Pressable/GenericPressable/index.tsx
@@ -1,33 +1,3 @@
-import React, {forwardRef} from 'react';
-import type {Role} from 'react-native';
-import GenericPressable from './BaseGenericPressable';
-import type {PressableRef} from './types';
-import type PressableProps from './types';
+import GenericPressable from './implementation';
-function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: PressableRef) {
- const accessible = props.accessible ?? props.accessible === undefined ? true : props.accessible;
-
- return (
-
- );
-}
-
-WebGenericPressable.displayName = 'WebGenericPressable';
-
-export default forwardRef(WebGenericPressable);
+export default GenericPressable;
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 2f01bb0f9f46..2f96b9d7dfaf 100644
--- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
+++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
@@ -120,6 +120,7 @@ function ExportWithDropdownMenu({
onOptionSelected={({value}) => savePreferredExportMethod(value)}
options={dropdownOptions}
style={[shouldUseNarrowLayout && styles.flexGrow1]}
+ wrapperStyle={styles.flex1}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
/>
- ${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 13cfb9b30e83..efc7dc0e198e 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -98,9 +98,6 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {
canEvict: false,
});
- const [distanceRates = {}] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
- selector: () => DistanceRequestUtils.getMileageRates(policy, true),
- });
const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${getTransactionID(report, parentReportActions)}`);
const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1'];
@@ -118,6 +115,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const {
created: transactionDate,
amount: transactionAmount,
+ attendees: transactionAttendees,
taxAmount: transactionTaxAmount,
currency: transactionCurrency,
comment: transactionDescription,
@@ -131,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);
@@ -187,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);
@@ -202,14 +202,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
let amountDescription = `${translate('iou.amount')}`;
const hasRoute = TransactionUtils.hasRoute(transactionBackup ?? transaction, isDistanceRequest);
- const rateID = TransactionUtils.getRateID(transaction) ?? '-1';
-
- const currency = transactionCurrency ?? CONST.CURRENCY.USD;
-
- const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : distanceRates[rateID] ?? {};
- const {unit} = mileageRate;
- const rate = transaction?.comment?.customUnit?.defaultP2PRate ?? mileageRate.rate;
-
+ const {unit, rate, currency} = DistanceRequestUtils.getRate({transaction, policy});
const distance = TransactionUtils.getDistanceInMeters(transactionBackup ?? transaction, unit);
const rateToDisplay = DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline);
const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate);
@@ -731,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/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 6cf854a1d906..6bb70a275a30 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -21,6 +21,7 @@ import useNetwork from '@hooks/useNetwork';
import usePolicy from '@hooks/usePolicy';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import {getCurrentUserAccountID} from '@libs/actions/Report';
import ControlSelection from '@libs/ControlSelection';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -173,7 +174,15 @@ function ReportPreview({
formattedMerchant = null;
}
- const shouldShowSubmitButton = isOpenExpenseReport && reimbursableSpend !== 0 && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation;
+ const currentUserAccountID = getCurrentUserAccountID();
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+ const shouldShowSubmitButton =
+ isOpenExpenseReport &&
+ reimbursableSpend !== 0 &&
+ !showRTERViolationMessage &&
+ !shouldShowBrokenConnectionViolation &&
+ (iouReport?.ownerAccountID === currentUserAccountID || isAdmin || iouReport?.managerID === currentUserAccountID);
+
const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport);
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
@@ -345,7 +354,7 @@ function ReportPreview({
const shouldShowSingleRequestMerchantOrDescription =
numberOfRequests === 1 && (!!formattedMerchant || !!formattedDescription) && !(hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend);
const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchantOrDescription || numberOfRequests > 1) && !isDisplayAmountZero(getDisplayAmount());
- const shouldShowScanningSubtitle = numberOfScanningReceipts === 1 && numberOfRequests === 1;
+ const shouldShowScanningSubtitle = (numberOfScanningReceipts === 1 && numberOfRequests === 1) || (numberOfScanningReceipts >= 1 && Number(nonHeldAmount) === 0);
const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && numberOfRequests === 1;
const isPayAtEndExpense = ReportUtils.isPayAtEndExpenseReport(iouReportID, allTransactions);
@@ -406,7 +415,6 @@ function ReportPreview({
*/
const connectedIntegration = PolicyUtils.getConnectedIntegration(policy);
- const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(iouReport);
useEffect(() => {
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/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx
index a76ca767ae2a..e7a60a5dc212 100644
--- a/src/components/Search/SearchFiltersParticipantsSelector.tsx
+++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx
@@ -57,28 +57,13 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
return defaultListOptions;
}
- return OptionsListUtils.getFilteredOptions(
- options.reports,
- options.personalDetails,
- undefined,
- '',
+ return OptionsListUtils.getFilteredOptions({
+ reports: options.reports,
+ personalDetails: options.personalDetails,
selectedOptions,
- CONST.EXPENSIFY_EMAILS,
- false,
- true,
- false,
- {},
- [],
- false,
- {},
- [],
- true,
- false,
- false,
- 0,
- undefined,
- false,
- );
+ excludeLogins: CONST.EXPENSIFY_EMAILS,
+ maxRecentReportsToShow: 0,
+ });
}, [areOptionsInitialized, options.personalDetails, options.reports, selectedOptions]);
const chatOptions = useMemo(() => {
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
index 4c383021645f..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';
@@ -67,7 +67,7 @@ function HeaderWrapper({icon, children, text, value, isCannedQuery, onSubmit, se
/>
)}
{text}} />
- {children}
+ {children}
) : (
@@ -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/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx
index 7ed22ec8162f..76eacd8b991d 100644
--- a/src/components/Search/SearchRouter/SearchButton.tsx
+++ b/src/components/Search/SearchRouter/SearchButton.tsx
@@ -3,6 +3,7 @@ import type {StyleProp, ViewStyle} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import {PressableWithoutFeedback} from '@components/Pressable';
+import Tooltip from '@components/Tooltip';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -23,21 +24,24 @@ function SearchButton({style}: SearchButtonProps) {
const {openSearchRouter} = useSearchRouterContext();
return (
- {
- Timing.start(CONST.TIMING.SEARCH_ROUTER_RENDER);
- Performance.markStart(CONST.TIMING.SEARCH_ROUTER_RENDER);
+
+ {
+ Timing.start(CONST.TIMING.SEARCH_ROUTER_RENDER);
+ Performance.markStart(CONST.TIMING.SEARCH_ROUTER_RENDER);
- openSearchRouter();
- })}
- >
-
-
+ openSearchRouter();
+ })}
+ >
+
+
+
);
}
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index a957806ee4f7..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,21 +117,34 @@ 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(() => {
- const currentSearchOptions = debouncedInputValue === '' ? searchOptions : filteredOptions;
- const reports: OptionData[] = [...currentSearchOptions.recentReports, ...currentSearchOptions.personalDetails];
- if (currentSearchOptions.userToInvite) {
- reports.push(currentSearchOptions.userToInvite);
+ if (debouncedInputValue === '') {
+ return searchOptions.recentReports.slice(0, 10);
+ }
+
+ const reports: OptionData[] = [...filteredOptions.recentReports, ...filteredOptions.personalDetails];
+ if (filteredOptions.userToInvite) {
+ reports.push(filteredOptions.userToInvite);
}
return reports.slice(0, 10);
}, [debouncedInputValue, filteredOptions, searchOptions]);
@@ -113,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);
@@ -144,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();
},
@@ -158,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()}
@@ -174,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/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx
index ef6963152c42..811c34b72a6e 100644
--- a/src/components/Search/SearchRouter/SearchRouterInput.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx
@@ -1,13 +1,14 @@
-import React, {useState} from 'react';
import type {ReactNode, RefObject} from 'react';
-import {View} from 'react-native';
+import React, {useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
+import {View} from 'react-native';
import FormHelpMessage from '@components/FormHelpMessage';
import type {SelectionListHandle} from '@components/SelectionList/types';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
+import shouldDelayFocus from '@libs/shouldDelayFocus';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -93,6 +94,7 @@ function SearchRouterInput({
value={value}
onChangeText={onChangeText}
autoFocus={autoFocus}
+ shouldDelayFocus={shouldDelayFocus}
loadingSpinnerStyle={[styles.mt0, styles.mr2]}
role={CONST.ROLE.PRESENTATION}
placeholder={translate('search.searchPlaceholder')}
@@ -105,7 +107,7 @@ function SearchRouterInput({
onSubmitEditing={onSubmit}
shouldUseDisabledStyles={false}
textInputContainerStyles={[styles.borderNone, styles.pb0]}
- inputStyle={[styles.searchInputStyle, inputWidth, styles.pl3, styles.pr3]}
+ inputStyle={[inputWidth, styles.pl3, styles.pr3]}
onFocus={() => {
setIsFocused(true);
routerListRef?.current?.updateExternalTextInputFocus(true);
diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx
index 9830ea4e9506..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';
@@ -32,7 +32,7 @@ type SearchRouterListProps = {
currentQuery: SearchQueryJSON | undefined;
/** Recent searches */
- recentSearches: ItemWithQuery[] | undefined;
+ recentSearches: Array | undefined;
/** Recent reports */
recentReports: OptionData[];
@@ -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,
@@ -129,13 +133,13 @@ function SearchRouterList(
});
}
- const recentSearchesData = recentSearches?.map(({query}) => {
- const searchQueryJSON = SearchUtils.buildSearchQueryJSON(query);
+ const recentSearchesData = recentSearches?.map(({query, timestamp}) => {
+ 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: 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"
@@ -167,7 +171,7 @@ function SearchRouterList(
if ('reportID' in item && item?.reportID) {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID));
} else if ('login' in item) {
- Report.navigateToAndOpenReport(item?.login ? [item.login] : []);
+ Report.navigateToAndOpenReport(item.login ? [item.login] : [], false);
}
},
[closeAndClearRouter, onSearchSubmit, currentQuery, updateUserSearchQuery],
@@ -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 7e403461dd34..2626b8565e2a 100644
--- a/src/components/Search/SearchRouter/SearchRouterModal.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx
@@ -2,25 +2,31 @@ import React from 'react';
import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal';
import Modal from '@components/Modal';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import * as Browser from '@libs/Browser';
import CONST from '@src/CONST';
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 (
- {isSearchRouterDisplayed && }
+ {isSearchRouterDisplayed && (
+
+
+
+ )}
);
}
diff --git a/src/components/Search/SearchStatusBar.tsx b/src/components/Search/SearchStatusBar.tsx
index afba2acc415c..07b57f8acab8 100644
--- a/src/components/Search/SearchStatusBar.tsx
+++ b/src/components/Search/SearchStatusBar.tsx
@@ -11,7 +11,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
@@ -177,7 +177,7 @@ function SearchStatusBar({queryJSON, onStatusChange}: SearchStatusBarProps) {
{options.map((item, index) => {
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 670cfef54df8..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
}
@@ -439,7 +458,7 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
/>
) : undefined
}
- contentContainerStyle={contentContainerStyle}
+ contentContainerStyle={[contentContainerStyle, styles.pb3]}
scrollEventThrottle={1}
/>
);
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 06bf8eb6434a..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';
@@ -105,6 +105,7 @@ function BaseSelectionList(
shouldIgnoreFocus = false,
scrollEventThrottle,
contentContainerStyle,
+ shouldHighlightSelectedItem = false,
}: BaseSelectionListProps,
ref: ForwardedRef,
) {
@@ -437,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;
@@ -476,6 +477,7 @@ function BaseSelectionList(
setFocusedIndex(normalizedIndex);
}}
shouldSyncFocus={!isTextInputFocusedRef.current}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
wrapperStyle={listItemWrapperStyle}
/>
{item.footerContent && item.footerContent}
@@ -718,7 +720,7 @@ function BaseSelectionList(
)}
{!!headerContent && headerContent}
- {flattenedSections.allOptions.length === 0 ? (
+ {flattenedSections.allOptions.length === 0 && (showLoadingPlaceholder || shouldShowListEmptyContent) ? (
renderListEmptyContent()
) : (
<>
diff --git a/src/components/SelectionList/InviteMemberListItem.tsx b/src/components/SelectionList/InviteMemberListItem.tsx
index 3e0ea0b3ef15..daeb513e3fa9 100644
--- a/src/components/SelectionList/InviteMemberListItem.tsx
+++ b/src/components/SelectionList/InviteMemberListItem.tsx
@@ -36,6 +36,7 @@ function InviteMemberListItem({
rightHandSideComponent,
onFocus,
shouldSyncFocus,
+ shouldHighlightSelectedItem,
}: InviteMemberListItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -58,6 +59,7 @@ function InviteMemberListItem({
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 d90f329dbf4c..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 &
@@ -290,6 +294,9 @@ type ListItemProps = CommonListItemProps & {
/** Whether to show RBR */
shouldDisplayRBR?: boolean;
+
+ /** Whether we highlight all the selected items */
+ shouldHighlightSelectedItem?: boolean;
};
type BaseListItemProps = CommonListItemProps & {
@@ -584,6 +591,9 @@ type BaseSelectionListProps = Partial & {
/** Additional styles to apply to scrollable content */
contentContainerStyle?: StyleProp;
+
+ /** Whether we highlight all the selected items */
+ shouldHighlightSelectedItem?: boolean;
} & TRightHandSideComponent;
type SelectionListHandle = {
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 f859f4a36803..4c3c9adf8ef1 100644
--- a/src/components/SettlementButton/index.tsx
+++ b/src/components/SettlementButton/index.tsx
@@ -59,6 +59,7 @@ function SettlementButton({
onPaymentOptionsShow,
onPaymentOptionsHide,
onlyShowPayElsewhere,
+ wrapperStyle,
}: SettlementButtonProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -190,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);
@@ -250,6 +251,7 @@ function SettlementButton({
savePreferredPaymentMethod(policyID, option.value);
}}
style={style}
+ wrapperStyle={wrapperStyle}
disabledStyle={disabledStyle}
buttonSize={buttonSize}
anchorAlignment={paymentMethodDropdownAnchorAlignment}
diff --git a/src/components/SettlementButton/types.ts b/src/components/SettlementButton/types.ts
index b3ad0c1c9bd0..df8fdedc512e 100644
--- a/src/components/SettlementButton/types.ts
+++ b/src/components/SettlementButton/types.ts
@@ -49,6 +49,9 @@ type SettlementButtonProps = {
/** Additional styles to add to the component */
style?: StyleProp;
+ /** Additional styles to add to the component wrapper */
+ wrapperStyle?: StyleProp;
+
/** Additional styles to add to the component when it's disabled */
disabledStyle?: StyleProp;
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) && (
, routesLength: number, tabIndex: number, active: boolean, affectedTabs: number[]) {
- const activeValue = active ? 1 : 0;
- const inactiveValue = active ? 0 : 1;
-
- if (routesLength > 1) {
- const inputRange = Array.from({length: routesLength}, (v, i) => i);
-
- return position.interpolate({
- inputRange,
- outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)),
- });
- }
- return activeValue;
-}
-
function TabSelector({state, navigation, onTabPress = () => {}, position, onFocusTrapContainerElementChanged}: TabSelectorProps) {
const {translate} = useLocalize();
const theme = useTheme();
@@ -72,21 +58,6 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu
const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: state.routes.length}, (v, i) => i), [state.routes.length]);
const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs);
- const getBackgroundColor = useCallback(
- (routesLength: number, tabIndex: number, affectedTabs: number[]) => {
- if (routesLength > 1) {
- const inputRange = Array.from({length: routesLength}, (v, i) => i);
-
- return position.interpolate({
- inputRange,
- outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)),
- }) as unknown as Animated.AnimatedInterpolation;
- }
- return theme.border;
- },
- [theme, position],
- );
-
useEffect(() => {
// It is required to wait transition end to reset affectedAnimatedTabs because tabs style is still animating during transition.
setTimeout(() => {
@@ -98,10 +69,10 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu
{state.routes.map((route, index) => {
- const activeOpacity = getOpacity(position, state.routes.length, index, true, affectedAnimatedTabs);
- const inactiveOpacity = getOpacity(position, state.routes.length, index, false, affectedAnimatedTabs);
- const backgroundColor = getBackgroundColor(state.routes.length, index, affectedAnimatedTabs);
const isActive = index === state.index;
+ const activeOpacity = getOpacity({routesLength: state.routes.length, tabIndex: index, active: true, affectedTabs: affectedAnimatedTabs, position, isActive});
+ const inactiveOpacity = getOpacity({routesLength: state.routes.length, tabIndex: index, active: false, affectedTabs: affectedAnimatedTabs, position, isActive});
+ const backgroundColor = getBackgroundColor({routesLength: state.routes.length, tabIndex: index, affectedTabs: affectedAnimatedTabs, theme, position, isActive});
const {icon, title} = getIconAndTitle(route.name, translate);
const onPress = () => {
diff --git a/src/components/TabSelector/getBackground/index.native.ts b/src/components/TabSelector/getBackground/index.native.ts
new file mode 100644
index 000000000000..09a9b3f347e6
--- /dev/null
+++ b/src/components/TabSelector/getBackground/index.native.ts
@@ -0,0 +1,17 @@
+import type {Animated} from 'react-native';
+import type GetBackgroudColor from './types';
+
+const getBackgroundColor: GetBackgroudColor = ({routesLength, tabIndex, affectedTabs, theme, position}) => {
+ if (routesLength > 1) {
+ const inputRange = Array.from({length: routesLength}, (v, i) => i);
+ return position?.interpolate({
+ inputRange,
+ outputRange: inputRange.map((i) => {
+ return affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG;
+ }),
+ }) as unknown as Animated.AnimatedInterpolation;
+ }
+ return theme.border;
+};
+
+export default getBackgroundColor;
diff --git a/src/components/TabSelector/getBackground/index.ts b/src/components/TabSelector/getBackground/index.ts
new file mode 100644
index 000000000000..2eb60a5115a1
--- /dev/null
+++ b/src/components/TabSelector/getBackground/index.ts
@@ -0,0 +1,9 @@
+import type GetBackgroudColor from './types';
+
+const getBackgroundColor: GetBackgroudColor = ({routesLength, tabIndex, affectedTabs, theme, isActive}) => {
+ if (routesLength > 1) {
+ return affectedTabs.includes(tabIndex) && isActive ? theme.border : theme.appBG;
+ }
+ return theme.border;
+};
+export default getBackgroundColor;
diff --git a/src/components/TabSelector/getBackground/types.ts b/src/components/TabSelector/getBackground/types.ts
new file mode 100644
index 000000000000..f66ee37e9b73
--- /dev/null
+++ b/src/components/TabSelector/getBackground/types.ts
@@ -0,0 +1,46 @@
+import type {Animated} from 'react-native';
+import type {ThemeColors} from '@styles/theme/types';
+
+/**
+ * Configuration for the getBackgroundColor function.
+ */
+type GetBackgroudColorConfig = {
+ /**
+ * The number of routes.
+ */
+ routesLength: number;
+
+ /**
+ * The index of the current tab.
+ */
+ tabIndex: number;
+
+ /**
+ * The indices of the affected tabs.
+ */
+ affectedTabs: number[];
+
+ /**
+ * The theme colors.
+ */
+ theme: ThemeColors;
+
+ /**
+ * The animated position interpolation.
+ */
+ position: Animated.AnimatedInterpolation;
+
+ /**
+ * Whether the tab is active.
+ */
+ isActive: boolean;
+};
+
+/**
+ * Function to get the background color.
+ * @param args - The configuration for the background color.
+ * @returns The interpolated background color or a string.
+ */
+type GetBackgroudColor = (args: GetBackgroudColorConfig) => Animated.AnimatedInterpolation | string;
+
+export default GetBackgroudColor;
diff --git a/src/components/TabSelector/getOpacity/index.native.ts b/src/components/TabSelector/getOpacity/index.native.ts
new file mode 100644
index 000000000000..0da5455214c9
--- /dev/null
+++ b/src/components/TabSelector/getOpacity/index.native.ts
@@ -0,0 +1,18 @@
+import type GetOpacity from './types';
+
+const getOpacity: GetOpacity = ({routesLength, tabIndex, active, affectedTabs, position, isActive}) => {
+ const activeValue = active ? 1 : 0;
+ const inactiveValue = active ? 0 : 1;
+
+ if (routesLength > 1) {
+ const inputRange = Array.from({length: routesLength}, (v, i) => i);
+
+ return position?.interpolate({
+ inputRange,
+ outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex && isActive ? activeValue : inactiveValue)),
+ });
+ }
+ return activeValue;
+};
+
+export default getOpacity;
diff --git a/src/components/TabSelector/getOpacity/index.ts b/src/components/TabSelector/getOpacity/index.ts
new file mode 100644
index 000000000000..d9f3a2eb6167
--- /dev/null
+++ b/src/components/TabSelector/getOpacity/index.ts
@@ -0,0 +1,13 @@
+import type GetOpacity from './types';
+
+const getOpacity: GetOpacity = ({routesLength, tabIndex, active, affectedTabs, isActive}) => {
+ const activeValue = active ? 1 : 0;
+ const inactiveValue = active ? 0 : 1;
+
+ if (routesLength > 1) {
+ return affectedTabs.includes(tabIndex) && isActive ? activeValue : inactiveValue;
+ }
+ return activeValue;
+};
+
+export default getOpacity;
diff --git a/src/components/TabSelector/getOpacity/types.ts b/src/components/TabSelector/getOpacity/types.ts
new file mode 100644
index 000000000000..46e4568b2783
--- /dev/null
+++ b/src/components/TabSelector/getOpacity/types.ts
@@ -0,0 +1,45 @@
+import type {Animated} from 'react-native';
+
+/**
+ * Configuration for the getOpacity function.
+ */
+type GetOpacityConfig = {
+ /**
+ * The number of routes in the tab bar.
+ */
+ routesLength: number;
+
+ /**
+ * The index of the tab.
+ */
+ tabIndex: number;
+
+ /**
+ * Whether we are calculating the opacity for the active tab.
+ */
+ active: boolean;
+
+ /**
+ * The indexes of the tabs that are affected by the animation.
+ */
+ affectedTabs: number[];
+
+ /**
+ * Scene's position, value which we would like to interpolate.
+ */
+ position: Animated.AnimatedInterpolation;
+
+ /**
+ * Whether the tab is active.
+ */
+ isActive: boolean;
+};
+
+/**
+ * Function to get the opacity.
+ * @param args - The configuration for the opacity.
+ * @returns The interpolated opacity or a fixed value (1 or 0).
+ */
+type GetOpacity = (args: GetOpacityConfig) => 1 | 0 | Animated.AnimatedInterpolation;
+
+export default GetOpacity;
diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx
index 1d72285be9a0..9d3a70d4d50c 100644
--- a/src/components/TagPicker/index.tsx
+++ b/src/components/TagPicker/index.tsx
@@ -1,6 +1,5 @@
import React, {useMemo, useState} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
@@ -10,7 +9,7 @@ import * as PolicyUtils from '@libs/PolicyUtils';
import type * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags} from '@src/types/onyx';
+import type {PolicyTag, PolicyTags} from '@src/types/onyx';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
type SelectedTagOption = {
@@ -21,15 +20,7 @@ type SelectedTagOption = {
pendingAction?: PendingAction;
};
-type TagPickerOnyxProps = {
- /** Collection of tag list on a policy */
- policyTags: OnyxEntry;
-
- /** List of recently used tags */
- policyRecentlyUsedTags: OnyxEntry;
-};
-
-type TagPickerProps = TagPickerOnyxProps & {
+type TagPickerProps = {
/** The policyID we are getting tags for */
// It's used in withOnyx HOC.
// eslint-disable-next-line react/no-unused-prop-types
@@ -51,7 +42,9 @@ type TagPickerProps = TagPickerOnyxProps & {
tagListIndex: number;
};
-function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, onSubmit}: TagPickerProps) {
+function TagPicker({selectedTag, tagListName, policyID, tagListIndex, shouldShowDisabledAndSelectedOption = false, onSubmit}: TagPickerProps) {
+ const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`);
+ const [policyRecentlyUsedTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`);
const styles = useThemeStyles();
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');
@@ -87,7 +80,16 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe
}, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]);
const sections = useMemo(
- () => OptionsListUtils.getFilteredOptions([], [], [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions,
+ () =>
+ OptionsListUtils.getFilteredOptions({
+ searchValue,
+ selectedOptions,
+ includeP2P: false,
+ includeTags: true,
+ tags: enabledTags,
+ recentlyUsedTags: policyRecentlyUsedTagsList,
+ canInviteUser: false,
+ }).tagOptions,
[searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList],
);
@@ -113,13 +115,6 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe
TagPicker.displayName = 'TagPicker';
-export default withOnyx({
- policyTags: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
- },
- policyRecentlyUsedTags: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`,
- },
-})(TagPicker);
+export default TagPicker;
export type {SelectedTagOption};
diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx
index edf9ac029ac0..6dc6201da782 100644
--- a/src/components/TextInput/TextInputLabel/index.tsx
+++ b/src/components/TextInput/TextInputLabel/index.tsx
@@ -21,6 +21,7 @@ function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}:
return (
void;
+
+ /** Callback fired when the image has been measured */
+ onMeasure?: () => void;
};
type UpdateImageSizeParams = {
@@ -75,6 +85,9 @@ function ThumbnailImage({
fallbackIconColor,
fallbackIconBackground,
objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL,
+ isDeleted,
+ onLoadFailure,
+ onMeasure,
}: ThumbnailImageProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -133,12 +146,19 @@ function ThumbnailImage({
return (
+ {isDeleted && }
setFailedToLoad(true)}
+ onMeasure={(args) => {
+ updateImageSize(args);
+ onMeasure?.();
+ }}
+ onLoadFailure={() => {
+ setFailedToLoad(true);
+ onLoadFailure?.();
+ }}
isAuthTokenRequired={isAuthTokenRequired}
objectPosition={objectPosition}
/>
diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
index f586c20cba49..f6611d2ca452 100644
--- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
+++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
@@ -50,6 +50,7 @@ function BaseGenericTooltip({
const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo(
() =>
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 f71b957387a8..02121ce26906 100644
--- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
+++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -62,6 +62,8 @@ type ValidateCodeFormProps = {
/** Function to clear error of the form */
clearError: () => void;
+
+ sendValidateCode: () => void;
};
function BaseValidateCodeForm({
@@ -73,6 +75,7 @@ function BaseValidateCodeForm({
validateError,
handleSubmitForm,
clearError,
+ sendValidateCode,
buttonStyles,
}: ValidateCodeFormProps) {
const {translate} = useLocalize();
@@ -125,10 +128,6 @@ function BaseValidateCodeForm({
}, []),
);
- useEffect(() => {
- clearError();
- }, [clearError]);
-
useEffect(() => {
if (!hasMagicCodeBeenSent) {
return;
@@ -140,7 +139,7 @@ function BaseValidateCodeForm({
* Request a validate code / magic code be sent to verify this contact method
*/
const resendValidateCode = () => {
- User.requestValidateCodeAction();
+ sendValidateCode();
inputValidateCodeRef.current?.clear();
};
@@ -189,7 +188,7 @@ function BaseValidateCodeForm({
errorText={formError?.validateCode ? translate(formError?.validateCode) : ErrorUtils.getLatestErrorMessage(account ?? {})}
hasError={!isEmptyObject(validateError)}
onFulfill={validateAndSubmitForm}
- autoFocus={false}
+ autoFocus
/>
diff --git a/src/components/ValidateCodeActionModal/index.tsx b/src/components/ValidateCodeActionModal/index.tsx
index 63528f1d5cf9..ff84198002d8 100644
--- a/src/components/ValidateCodeActionModal/index.tsx
+++ b/src/components/ValidateCodeActionModal/index.tsx
@@ -6,14 +6,26 @@ import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as User from '@libs/actions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ValidateCodeActionModalProps} from './type';
import ValidateCodeForm from './ValidateCodeForm';
import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm';
-function ValidateCodeActionModal({isVisible, title, description, onClose, validatePendingAction, validateError, handleSubmitForm, clearError}: ValidateCodeActionModalProps) {
+function ValidateCodeActionModal({
+ isVisible,
+ title,
+ description,
+ onClose,
+ onModalHide,
+ validatePendingAction,
+ validateError,
+ handleSubmitForm,
+ clearError,
+ footer,
+ sendValidateCode,
+ hasMagicCodeBeenSent,
+}: ValidateCodeActionModalProps) {
const themeStyles = useThemeStyles();
const firstRenderRef = useRef(true);
const validateCodeFormRef = useRef(null);
@@ -30,15 +42,16 @@ function ValidateCodeActionModal({isVisible, title, description, onClose, valida
return;
}
firstRenderRef.current = false;
- User.requestValidateCodeAction();
- }, [isVisible]);
+
+ sendValidateCode();
+ }, [isVisible, sendValidateCode]);
return (
+ {footer?.()}
);
diff --git a/src/components/ValidateCodeActionModal/type.ts b/src/components/ValidateCodeActionModal/type.ts
index 3cbfe62513d1..5556287b370e 100644
--- a/src/components/ValidateCodeActionModal/type.ts
+++ b/src/components/ValidateCodeActionModal/type.ts
@@ -1,3 +1,4 @@
+import type React from 'react';
import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
type ValidateCodeActionModalProps = {
@@ -13,6 +14,9 @@ type ValidateCodeActionModalProps = {
/** Function to call when the user closes the modal */
onClose: () => void;
+ /** Function to be called when the modal is closed */
+ onModalHide?: () => void;
+
/** The pending action for submitting form */
validatePendingAction?: PendingAction | null;
@@ -24,6 +28,15 @@ type ValidateCodeActionModalProps = {
/** Function to clear error of the form */
clearError: () => void;
+
+ /** A component to be rendered inside the modal */
+ footer?: () => React.JSX.Element;
+
+ /** Function is called when validate code modal is mounted and on magic code resend */
+ sendValidateCode: () => void;
+
+ /** If the magic code has been resent previously */
+ hasMagicCodeBeenSent?: boolean;
};
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx
index 84eb988d0758..012537b75108 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.tsx
+++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx
@@ -136,6 +136,8 @@ function BaseVideoPlayer({
debouncedHideControl();
}, [isPlaying, debouncedHideControl, controlStatusState, isPopoverVisible, canUseTouchScreen]);
+ const stopWheelPropagation = useCallback((ev: WheelEvent) => ev.stopPropagation(), []);
+
const toggleControl = useCallback(() => {
if (controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) {
hideControl();
@@ -233,7 +235,18 @@ function BaseVideoPlayer({
(event: VideoFullscreenUpdateEvent) => {
onFullscreenUpdate?.(event);
+ if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_PRESENT) {
+ // When the video is in fullscreen, we don't want the scroll to be captured by the InvertedFlatList of report screen.
+ // This will also allow the user to scroll the video playback speed.
+ if (videoPlayerElementParentRef.current && 'addEventListener' in videoPlayerElementParentRef.current) {
+ videoPlayerElementParentRef.current.addEventListener('wheel', stopWheelPropagation);
+ }
+ }
+
if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
+ if (videoPlayerElementParentRef.current && 'removeEventListener' in videoPlayerElementParentRef.current) {
+ videoPlayerElementParentRef.current.removeEventListener('wheel', stopWheelPropagation);
+ }
isFullScreenRef.current = false;
// Sync volume updates in full screen mode after leaving it
@@ -254,7 +267,7 @@ function BaseVideoPlayer({
}
}
},
- [isFullScreenRef, onFullscreenUpdate, pauseVideo, playVideo, videoResumeTryNumberRef, updateVolume, currentVideoPlayerRef],
+ [isFullScreenRef, onFullscreenUpdate, pauseVideo, playVideo, videoResumeTryNumberRef, updateVolume, currentVideoPlayerRef, stopWheelPropagation],
);
const bindFunctions = useCallback(() => {
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/useIndicatorStatus.ts b/src/hooks/useIndicatorStatus.ts
new file mode 100644
index 000000000000..b026bc52fd7b
--- /dev/null
+++ b/src/hooks/useIndicatorStatus.ts
@@ -0,0 +1,79 @@
+import {useMemo} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import {isConnectionInProgress} from '@libs/actions/connections';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as SubscriptionUtils from '@libs/SubscriptionUtils';
+import * as UserUtils from '@libs/UserUtils';
+import * as PaymentMethods from '@userActions/PaymentMethods';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useTheme from './useTheme';
+
+type IndicatorStatus = ValueOf;
+
+type IndicatorStatusResult = {
+ indicatorColor: string;
+ status: ValueOf | undefined;
+ policyIDWithErrors: string | undefined;
+};
+
+function useIndicatorStatus(): IndicatorStatusResult {
+ const theme = useTheme();
+ const [allConnectionSyncProgresses] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS);
+ const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
+ const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
+ const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
+
+ // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and
+ // those should be cleaned out before doing any error checking
+ const cleanPolicies = useMemo(() => Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)), [policies]);
+
+ const policyErrors = {
+ [CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS]: Object.values(cleanPolicies).find(PolicyUtils.hasPolicyError),
+ [CONST.INDICATOR_STATUS.HAS_CUSTOM_UNITS_ERROR]: Object.values(cleanPolicies).find(PolicyUtils.hasCustomUnitsError),
+ [CONST.INDICATOR_STATUS.HAS_EMPLOYEE_LIST_ERROR]: Object.values(cleanPolicies).find(PolicyUtils.hasEmployeeListError),
+ [CONST.INDICATOR_STATUS.HAS_SYNC_ERRORS]: Object.values(cleanPolicies).find((cleanPolicy) =>
+ PolicyUtils.hasSyncError(
+ cleanPolicy,
+ isConnectionInProgress(allConnectionSyncProgresses?.[`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${cleanPolicy?.id}`], cleanPolicy),
+ ),
+ ),
+ };
+
+ // All of the error & info-checking methods are put into an array. This is so that using _.some() will return
+ // early as soon as the first error / info condition is returned. This makes the checks very efficient since
+ // we only care if a single error / info condition exists anywhere.
+ const errorChecking: Partial> = {
+ [CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS]: Object.keys(userWallet?.errors ?? {}).length > 0,
+ [CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: PaymentMethods.hasPaymentMethodError(bankAccountList, fundList),
+ ...(Object.fromEntries(Object.entries(policyErrors).map(([error, policy]) => [error, !!policy])) as Record),
+ [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: SubscriptionUtils.hasSubscriptionRedDotError(),
+ [CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS]: Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
+ [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR]: !!loginList && UserUtils.hasLoginListError(loginList),
+ // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
+ [CONST.INDICATOR_STATUS.HAS_WALLET_TERMS_ERRORS]: Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID,
+ };
+
+ const infoChecking: Partial> = {
+ [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_INFO]: !!loginList && UserUtils.hasLoginListInfo(loginList),
+ [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO]: SubscriptionUtils.hasSubscriptionGreenDotInfo(),
+ };
+
+ const [error] = Object.entries(errorChecking).find(([, value]) => value) ?? [];
+ const [info] = Object.entries(infoChecking).find(([, value]) => value) ?? [];
+
+ const status = (error ?? info) as IndicatorStatus | undefined;
+ const policyIDWithErrors = Object.values(policyErrors).find(Boolean)?.id;
+ const indicatorColor = error ? theme.danger : theme.success;
+
+ return {indicatorColor, status, policyIDWithErrors};
+}
+
+export default useIndicatorStatus;
+
+export type {IndicatorStatus};
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 ffb210960286..4f26c8e45169 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -39,6 +39,7 @@ import type {
ChangeTypeParams,
CharacterLengthLimitParams,
CharacterLimitParams,
+ CompanyCardBankName,
CompanyCardFeedNameParams,
ConfirmThatParams,
ConnectionNameParams,
@@ -258,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',
@@ -956,13 +958,14 @@ const translations = {
invalidSplit: 'The sum of splits must equal the total amount.',
invalidSplitParticipants: 'Please enter an amount greater than zero for at least two participants.',
invalidSplitYourself: 'Please enter a non-zero amount for your split.',
+ noParticipantSelected: 'Please select a participant.',
other: 'Unexpected error. Please try again later.',
genericCreateFailureMessage: 'Unexpected error submitting this expense. Please try again later.',
genericCreateInvoiceFailureMessage: 'Unexpected error sending this invoice. Please try again later.',
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.',
@@ -973,6 +976,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',
@@ -1028,6 +1032,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)',
},
@@ -2159,6 +2164,7 @@ const translations = {
companyAddress: 'Company address',
listOfRestrictedBusinesses: 'list of restricted businesses',
confirmCompanyIsNot: 'I confirm that this company is not on the',
+ businessInfoTitle: 'Business info',
},
beneficialOwnerInfoStep: {
doYouOwn25percent: 'Do you own 25% or more of',
@@ -2237,6 +2243,21 @@ const translations = {
enable2FAText: 'We take your security seriously. Please set up 2FA now to add an extra layer of protection to your account.',
secureYourAccount: 'Secure your account',
},
+ countryStep: {
+ confirmBusinessBank: 'Confirm business bank account currency and country',
+ confirmCurrency: 'Confirm currency and country',
+ },
+ signerInfoStep: {
+ signerInfo: 'Signer info',
+ },
+ agreementsStep: {
+ agreements: 'Agreements',
+ pleaseConfirm: 'Please confirm the agreements below',
+ accept: 'Accept and add bank account',
+ },
+ finishStep: {
+ connect: 'Connect bank account',
+ },
reimbursementAccountLoadingAnimation: {
oneMoment: 'One moment',
explanationLine: "We’re taking a look at your information. You'll be able to continue with next steps shortly.",
@@ -2343,7 +2364,7 @@ const translations = {
topLevel: 'Top level',
appliedOnExport: 'Not imported into Expensify, applied on export',
shareNote: {
- header: 'Easily share your workspace with other members.',
+ header: 'Share your workspace with other members',
content: {
firstPart:
'Share this QR code or copy the link below to make it easy for members to request access to your workspace. All requests to join the workspace will show up in the',
@@ -2411,18 +2432,17 @@ 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',
[CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY]: 'Journal entry',
[CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.CHECK]: 'Check',
+ [`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CHECK}Description`]:
+ "We'll create an itemized check for each Expensify report and send it from the bank account below.",
[`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}Description`]:
"We'll automatically match the merchant name on the credit card transaction to any corresponding vendors in QuickBooks. If no vendors exist, we'll create a 'Credit Card Misc.' vendor for association.",
[`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Description`]:
@@ -2430,6 +2450,7 @@ const translations = {
[`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}AccountDescription`]: 'Choose where to export credit card transactions.',
[`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}AccountDescription`]: 'Choose a vendor to apply to all credit card transactions.',
+ [`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.CHECK}AccountDescription`]: 'Choose where to send checks from.',
[`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Error`]:
'Vendor bills are unavailable when locations are enabled. Please choose a different export option.',
@@ -2453,6 +2474,8 @@ const translations = {
classes: 'Classes',
items: 'Items',
customers: 'Customers/projects',
+ exportCompanyCardsDescription: 'Set how company card purchases export to QuickBooks Desktop.',
+ defaultVendorDescription: 'Set a default vendor that will apply to all credit card transactions upon export.',
accountsDescription: 'Your QuickBooks Desktop chart of accounts will import into Expensify as categories.',
accountsSwitchTitle: 'Choose to import new accounts as enabled or disabled categories.',
accountsSwitchDescription: 'Enabled categories will be available for members to select when creating their expenses.',
@@ -2463,9 +2486,9 @@ const translations = {
advancedConfig: {
autoSyncDescription: 'Expensify will automatically sync with QuickBooks Desktop every day.',
createEntities: 'Auto-create entities',
- createEntitiesDescription:
- "Expensify will automatically create vendors in QuickBooks Desktop if they don't exist already, and auto-create customers when exporting invoices.",
+ createEntitiesDescription: "Expensify will automatically create vendors in QuickBooks Desktop if they don't exist already.",
},
+ itemsDescription: 'Choose how to handle QuickBooks Desktop items in Expensify.',
},
qbo: {
importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.',
@@ -2479,10 +2502,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',
@@ -2532,7 +2553,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',
@@ -2587,8 +2607,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',
@@ -3047,9 +3068,10 @@ const translations = {
addNewCard: {
other: 'Other',
cardProviders: {
- amex: 'American Express Corporate Cards',
- mastercard: 'Mastercard Commercial Cards',
- visa: 'Visa Commercial Cards',
+ gl1025: 'American Express Corporate Cards',
+ cdf: 'Mastercard Commercial Cards',
+ vcf: 'Visa Commercial Cards',
+ stripe: 'Stripe Cards',
},
yourCardProvider: `Who's your card provider?`,
whoIsYourBankAccount: 'Who’s your bank?',
@@ -3063,25 +3085,25 @@ const translations = {
enableFeed: {
title: ({provider}: GoBackMessageParams) => `Enable your ${provider} feed`,
heading: 'We have a direct integration with your card issuer and can import your transaction data into Expensify quickly and accurately.\n\nTo get started, simply:',
- visa: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructionson how to set up your Visa Commercial Cards.\n\n2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them toenable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`,
- amex: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) to find out if American Express can enable a custom feed for your program.\n\n2. Once the feed is enabled, Amex will send you a production letter.\n\n3. *Once you have the feed information, continue to the next screen.*`,
- mastercard: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructions on how to set up your Mastercard Commercial Cards.\n\n 2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them to enable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`,
+ vcf: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructionson how to set up your Visa Commercial Cards.\n\n2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them toenable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`,
+ gl1025: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) to find out if American Express can enable a custom feed for your program.\n\n2. Once the feed is enabled, Amex will send you a production letter.\n\n3. *Once you have the feed information, continue to the next screen.*`,
+ cdf: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructions on how to set up your Mastercard Commercial Cards.\n\n 2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them to enable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`,
stripe: `1. Visit Stripe’s Dashboard, and go to [Settings](${CONST.COMPANY_CARDS_STRIPE_HELP}).\n\n2. Under Product Integrations, click Enable next to Expensify.\n\n3. Once the feed is enabled, click Submit below and we’ll work on adding it.`,
},
whatBankIssuesCard: 'What bank issues these cards?',
enterNameOfBank: 'Enter name of bank',
feedDetails: {
- visa: {
+ vcf: {
title: 'What are the Visa feed details?',
processorLabel: 'Processor ID',
bankLabel: 'Financial institution (bank) ID',
companyLabel: 'Company ID',
},
- amex: {
+ gl1025: {
title: `What's the Amex delivery file name?`,
fileNameLabel: 'Delivery file name',
},
- mastercard: {
+ cdf: {
title: `What's the Mastercard distribution ID?`,
distributionLabel: 'Distribution ID',
},
@@ -3117,7 +3139,8 @@ const translations = {
brokenConnectionErrorFirstPart: `Card feed connection is broken. Please `,
brokenConnectionErrorLink: 'log into your bank ',
brokenConnectionErrorSecondPart: 'so we can establish the connection again.',
- assignedYouCard: ({assigner}: AssignedYouCardParams) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`,
+ assignedYouCard: ({link}: AssignedYouCardParams) => `assigned you a ${link}! Imported transactions will appear in this chat.`,
+ companyCard: 'company card',
chooseCardFeed: 'Choose card feed',
},
expensifyCard: {
@@ -3307,6 +3330,9 @@ const translations = {
emptyAddedFeedDescription: 'Get started by assigning your first card to a member.',
pendingFeedTitle: `We're reviewing your request...`,
pendingFeedDescription: `We're currently reviewing your feed details. Once that's done we'll reach out to you via`,
+ pendingBankTitle: 'Check your browser window',
+ pendingBankDescription: ({bankName}: CompanyCardBankName) => `Please connect to ${bankName} via your browser window that just opened. If one didn’t open, `,
+ pendingBankLink: 'please click here.',
giveItNameInstruction: 'Give the card a name that sets it apart from the others.',
updating: 'Updating...',
noAccountsFound: 'No accounts found',
@@ -3582,6 +3608,7 @@ const translations = {
},
errorODIntegration: "There's an error with a connection that's been set up in Expensify Classic. ",
goToODToFix: 'Go to Expensify Classic to fix this issue.',
+ goToODToSettings: 'Go to Expensify Classic to manage your settings.',
setup: 'Connect',
lastSync: ({relativeDate}: LastSyncAccountingParams) => `Last synced ${relativeDate}`,
import: 'Import',
@@ -3590,6 +3617,7 @@ const translations = {
other: 'Other integrations',
syncNow: 'Sync now',
disconnect: 'Disconnect',
+ reinstall: 'Reinstall connector',
disconnectTitle: ({connectionName}: OptionalParam = {}) => {
const integrationName =
connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'integration';
@@ -3638,14 +3666,18 @@ const translations = {
syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
case 'quickbooksOnlineImportCustomers':
+ case 'quickbooksDesktopImportCustomers':
return 'Importing customers';
case 'quickbooksOnlineImportEmployees':
case 'netSuiteSyncImportEmployees':
case 'intacctImportEmployees':
+ case 'quickbooksDesktopImportEmployees':
return 'Importing employees';
case 'quickbooksOnlineImportAccounts':
+ case 'quickbooksDesktopImportAccounts':
return 'Importing accounts';
case 'quickbooksOnlineImportClasses':
+ case 'quickbooksDesktopImportClasses':
return 'Importing classes';
case 'quickbooksOnlineImportLocations':
return 'Importing locations';
@@ -3664,6 +3696,19 @@ const translations = {
return 'Importing Xero data';
case 'startingImportQBO':
return 'Importing QuickBooks Online data';
+ case 'startingImportQBD':
+ case 'quickbooksDesktopImportMore':
+ return 'Importing QuickBooks Desktop data';
+ case 'quickbooksDesktopImportTitle':
+ return 'Importing title';
+ case 'quickbooksDesktopImportApproveCertificate':
+ return 'Importing approve ceritificate';
+ case 'quickbooksDesktopImportDimensions':
+ return 'Importing dimensions';
+ case 'quickbooksDesktopImportSavePolicy':
+ return 'Importing save policy';
+ case 'quickbooksDesktopWebConnectorReminder':
+ return 'Still syncing data with QuickBooks... Please make sure the Web Connector is running';
case 'quickbooksOnlineSyncTitle':
return 'Syncing QuickBooks Online data';
case 'quickbooksOnlineSyncLoadData':
@@ -3737,6 +3782,7 @@ const translations = {
case 'netSuiteSyncImportSubsidiaries':
return 'Importing subsidiaries';
case 'netSuiteSyncImportVendors':
+ case 'quickbooksDesktopImportVendors':
return 'Importing vendors';
case 'intacctCheckConnection':
return 'Checking Sage Intacct connection';
@@ -3780,14 +3826,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',
@@ -3803,8 +3841,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',
@@ -3963,6 +4001,11 @@ const translations = {
description: `Enjoy automated syncing and reduce manual entries with the Expensify + Sage Intacct integration. Gain in-depth, real-time financial insights with user-defined dimensions, as well as expense coding by department, class, location, customer, and project (job).`,
onlyAvailableOnPlan: 'Our Sage Intacct integration is only available on the Control plan, starting at ',
},
+ [CONST.POLICY.CONNECTIONS.NAME.QBD]: {
+ title: 'QuickBooks Desktop',
+ description: `Enjoy automated syncing and reduce manual entries with the Expensify + QuickBooks Desktop integration. Gain ultimate efficiency with a realtime, two-way connection and expense coding by class, item, customer, and project.`,
+ onlyAvailableOnPlan: 'Our QuickBooks Desktop integration is only available on the Control plan, starting at ',
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: {
title: 'Advanced Approvals',
description: `If you want to add more layers of approval to the mix – or just make sure the largest expenses get another set of eyes – we’ve got you covered. Advanced approvals help you put the right checks in place at every level so you keep your team’s spend under control.`,
@@ -4005,7 +4048,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',
@@ -4251,7 +4294,11 @@ 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",
+ subtitle: 'Use the green button below to create an expense or take a tour of Expensify to learn more.',
},
emptyTripResults: {
title: 'No trips to display',
@@ -5094,6 +5141,30 @@ const translations = {
hasChildReportAwaitingAction: 'Has child report awaiting action',
hasMissingInvoiceBankAccount: 'Has missing invoice bank account',
},
+ reasonRBR: {
+ hasErrors: 'Has errors in report or report actions data',
+ hasViolations: 'Has violations',
+ hasTransactionThreadViolations: 'Has transaction thread violations',
+ },
+ indicatorStatus: {
+ theresAReportAwaitingAction: "There's a report awaiting action",
+ theresAReportWithErrors: "There's a report with errors",
+ theresAWorkspaceWithCustomUnitsErrors: "There's a workspace with custom units errors",
+ theresAProblemWithAWorkspaceMember: "There's a problem with a workspace member",
+ theresAProblemWithAContactMethod: "There's a problem with a contact method",
+ aContactMethodRequiresVerification: 'A contact method requires verification',
+ theresAProblemWithAPaymentMethod: "There's a problem with a payment method",
+ theresAProblemWithAWorkspace: "There's a problem with a workspace",
+ theresAProblemWithYourReimbursementAccount: "There's a problem with your reimbursement account",
+ theresABillingProblemWithYourSubscription: "There's a billing problem with your subscription",
+ yourSubscriptionHasBeenSuccessfullyRenewed: 'Your subscription has been successfully renewed',
+ theresWasAProblemDuringAWorkspaceConnectionSync: 'There was a problem during a workspace connection sync',
+ theresAProblemWithYourWallet: "There's a problem with your wallet",
+ theresAProblemWithYourWalletTerms: "There's a problem with your wallet terms",
+ },
+ },
+ emptySearchView: {
+ takeATour: 'Take a tour',
},
};
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 095182b7d14d..c145de8d1d08 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -37,6 +37,7 @@ import type {
ChangeTypeParams,
CharacterLengthLimitParams,
CharacterLimitParams,
+ CompanyCardBankName,
CompanyCardFeedNameParams,
ConfirmThatParams,
ConnectionNameParams,
@@ -242,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',
@@ -930,10 +932,12 @@ const translations = {
noReimbursableExpenses: 'El importe de este informe no es válido',
pendingConversionMessage: 'El total se actualizará cuando estés online',
changedTheExpense: 'cambió el gasto',
- setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`,
+ setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) =>
+ `${valueName === 'comerciante' || valueName === 'importe' || valueName === 'gasto' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`,
setTheDistanceMerchant: ({translatedChangedField, newMerchant, newAmountToDisplay}: SetTheDistanceMerchantParams) =>
`estableció la ${translatedChangedField} a ${newMerchant}, lo que estableció el importe a ${newAmountToDisplay}`,
- removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`,
+ removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) =>
+ `${valueName === 'comerciante' || valueName === 'importe' || valueName === 'gasto' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`,
updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) =>
`${valueName === 'comerciante' || valueName === 'importe' || valueName === 'gasto' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`,
updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) =>
@@ -951,13 +955,14 @@ const translations = {
invalidSplit: 'La suma de las partes debe ser igual al importe total.',
invalidSplitParticipants: 'Introduce un importe superior a cero para al menos dos participantes.',
invalidSplitYourself: 'Por favor, introduce una cantidad diferente de cero para tu parte.',
+ noParticipantSelected: 'Por favor, selecciona un participante.',
other: 'Error inesperado. Por favor, inténtalo más tarde.',
genericHoldExpenseFailureMessage: 'Error inesperado al bloquear el gasto. Por favor, inténtalo de nuevo más tarde.',
genericUnholdExpenseFailureMessage: 'Error inesperado al desbloquear el gasto. Por favor, inténtalo de nuevo más tarde.',
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.',
@@ -968,6 +973,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',
@@ -1023,6 +1029,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)',
},
@@ -1544,7 +1551,6 @@ const translations = {
'Has introducido incorrectamente los 4 últimos dígitos de tu tarjeta Expensify demasiadas veces. Si estás seguro de que los números son correctos, ponte en contacto con Conserjería para solucionarlo. De lo contrario, inténtalo de nuevo más tarde.',
},
},
- // TODO: add translation
getPhysicalCard: {
header: 'Obtener tarjeta física',
nameMessage: 'Introduce tu nombre y apellido como aparecerá en tu tarjeta.',
@@ -2181,6 +2187,7 @@ const translations = {
companyAddress: 'Dirección de la empresa',
listOfRestrictedBusinesses: 'lista de negocios restringidos',
confirmCompanyIsNot: 'Confirmo que esta empresa no está en la',
+ businessInfoTitle: 'Información del negocio',
},
beneficialOwnerInfoStep: {
doYouOwn25percent: '¿Posees el 25% o más de',
@@ -2259,6 +2266,21 @@ const translations = {
enable2FAText: 'Tu seguridad es importante para nosotros. Por favor, configura ahora la autenticación de dos factores para añadir una capa adicional de protección a tu cuenta.',
secureYourAccount: 'Asegura tu cuenta',
},
+ countryStep: {
+ confirmBusinessBank: 'Confirmar moneda y país de la cuenta bancaria comercial',
+ confirmCurrency: 'Confirmar moneda y país',
+ },
+ signerInfoStep: {
+ signerInfo: 'Información del firmante',
+ },
+ agreementsStep: {
+ agreements: 'Acuerdos',
+ pleaseConfirm: 'Por favor confirme los acuerdos a continuación',
+ accept: 'Aceptar y añadir cuenta bancaria',
+ },
+ finishStep: {
+ connect: 'Conectar cuenta bancaria',
+ },
reimbursementAccountLoadingAnimation: {
oneMoment: 'Un momento',
explanationLine: 'Estamos verificando tu información y podrás continuar con los siguientes pasos en unos momentos.',
@@ -2364,7 +2386,7 @@ const translations = {
reportLevel: 'Nivel de informe',
appliedOnExport: 'No se importa en Expensify, se aplica en la exportación',
shareNote: {
- header: 'Comparte fácilmente tu espacio de trabajo con otros miembros.',
+ header: 'Comparte tu espacio de trabajo con otros miembros',
content: {
firstPart:
'Comparte este código QR o copia el enlace de abajo para facilitar que los miembros soliciten acceso a tu espacio de trabajo. Todas las solicitudes para unirse al espacio de trabajo aparecerán en la sala',
@@ -2433,18 +2455,17 @@ 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',
[CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY]: 'Asiento contable',
[CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.CHECK]: 'Cheque',
+ [`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CHECK}Description`]:
+ 'Crearemos un cheque desglosado para cada informe de Expensify y lo enviaremos desde la cuenta bancaria a continuación.',
[`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}Description`]:
"Automáticamente relacionaremos el nombre del comerciante de la transacción con tarjeta de crédito con cualquier proveedor correspondiente en QuickBooks. Si no existen proveedores, crearemos un proveedor asociado 'Credit Card Misc.'.",
[`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Description`]:
@@ -2453,6 +2474,7 @@ const translations = {
[`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}AccountDescription`]: 'Elige dónde exportar las transacciones con tarjeta de crédito.',
[`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}AccountDescription`]:
'Selecciona el proveedor que se aplicará a todas las transacciones con tarjeta de crédito.',
+ [`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.CHECK}AccountDescription`]: 'Elige desde dónde enviar los cheques.',
[`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Error`]:
'Las facturas de proveedores no están disponibles cuando las ubicaciones están habilitadas. Por favor, selecciona otra opción de exportación.',
@@ -2477,6 +2499,8 @@ const translations = {
classes: 'Clases',
items: 'Artículos',
customers: 'Clientes/proyectos',
+ exportCompanyCardsDescription: 'Establece cómo se exportan las compras con tarjeta de empresa a QuickBooks Desktop.',
+ defaultVendorDescription: 'Establece un proveedor predeterminado que se aplicará a todas las transacciones con tarjeta de crédito al momento de exportarlas.',
accountsDescription: 'Tu plan de cuentas de QuickBooks Desktop se importará a Expensify como categorías.',
accountsSwitchTitle: 'Elige importar cuentas nuevas como categorías activadas o desactivadas.',
accountsSwitchDescription: 'Las categorías activas estarán disponibles para ser escogidas cuando se crea un gasto.',
@@ -2487,8 +2511,9 @@ const translations = {
advancedConfig: {
autoSyncDescription: 'Expensify se sincronizará automáticamente con QuickBooks Desktop todos los días.',
createEntities: 'Crear entidades automáticamente',
- createEntitiesDescription: 'Expensify creará automáticamente proveedores en QuickBooks Desktop si aún no existen, y creará automáticamente clientes al exportar facturas.',
+ createEntitiesDescription: 'Expensify creará automáticamente proveedores en QuickBooks Desktop si aún no existen.',
},
+ itemsDescription: 'Elige cómo gestionar los elementos de QuickBooks Desktop en Expensify.',
},
qbo: {
importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.',
@@ -2501,10 +2526,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',
@@ -2553,10 +2578,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.',
@@ -2617,8 +2638,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',
@@ -3084,9 +3106,10 @@ const translations = {
addNewCard: {
other: 'Otros',
cardProviders: {
- amex: 'Tarjetas de empresa American Express',
- mastercard: 'Tarjetas comerciales Mastercard',
- visa: 'Tarjetas comerciales Visa',
+ gl1025: 'Tarjetas de empresa American Express',
+ cdf: 'Tarjetas comerciales Mastercard',
+ vcf: 'Tarjetas comerciales Visa',
+ stripe: 'Tarjetas comerciales Stripe',
},
yourCardProvider: `¿Quién es su proveedor de tarjetas?`,
whoIsYourBankAccount: '¿Cuál es tu banco?',
@@ -3101,25 +3124,25 @@ const translations = {
title: ({provider}: GoBackMessageParams) => `Habilita tu feed ${provider}`,
heading:
'Tenemos una integración directa con el emisor de su tarjeta y podemos importar los datos de sus transacciones a Expensify de forma rápida y precisa.\n\nPara empezar, simplemente:',
- visa: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Visa.\n\n2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para comprobar que admiten un feed personalizado para su programa, y pídales que lo activen.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`,
- amex: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para saber si American Express puede habilitar un feed personalizado para su programa.\n\n2. Una vez activada la alimentación, Amex le enviará una carta de producción.\n\n3. *Una vez que tenga la información de alimentación, continúe con la siguiente pantalla.*`,
- mastercard: `1. Visite [este artículo de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Mastercard.\n\n 2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para verificar que admiten un feed personalizado para su programa, y pídales que lo habiliten.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`,
+ vcf: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Visa.\n\n2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para comprobar que admiten un feed personalizado para su programa, y pídales que lo activen.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`,
+ gl1025: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para saber si American Express puede habilitar un feed personalizado para su programa.\n\n2. Una vez activada la alimentación, Amex le enviará una carta de producción.\n\n3. *Una vez que tenga la información de alimentación, continúe con la siguiente pantalla.*`,
+ cdf: `1. Visite [este artículo de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Mastercard.\n\n 2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para verificar que admiten un feed personalizado para su programa, y pídales que lo habiliten.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`,
stripe: `1. Visita el Panel de Stripe y ve a [Configuraciones](${CONST.COMPANY_CARDS_STRIPE_HELP}).\n\n2. En Integraciones de Productos, haz clic en Habilitar junto a Expensify.\n\n3. Una vez que la fuente esté habilitada, haz clic en Enviar abajo y comenzaremos a añadirla.`,
},
whatBankIssuesCard: '¿Qué banco emite estas tarjetas?',
enterNameOfBank: 'Introduzca el nombre del banco',
feedDetails: {
- visa: {
+ vcf: {
title: '¿Cuáles son los datos de alimentación de Visa?',
processorLabel: 'ID del procesador',
bankLabel: 'Identificación de la institución financiera (banco)',
companyLabel: 'Empresa ID',
},
- amex: {
+ gl1025: {
title: `¿Cuál es el nombre del archivo de entrega de Amex?`,
fileNameLabel: 'Nombre del archivo de entrega',
},
- mastercard: {
+ cdf: {
title: `¿Cuál es el identificador de distribución de Mastercard?`,
distributionLabel: 'ID de distribución',
},
@@ -3155,7 +3178,8 @@ const translations = {
brokenConnectionErrorFirstPart: `La conexión de la fuente de tarjetas está rota. Por favor, `,
brokenConnectionErrorLink: 'inicia sesión en tu banco ',
brokenConnectionErrorSecondPart: 'para que podamos restablecer la conexión.',
- assignedYouCard: ({assigner}: AssignedYouCardParams) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`,
+ assignedYouCard: ({link}: AssignedYouCardParams) => `te ha asignado una ${link}! Las transacciones importadas aparecerán en este chat.`,
+ companyCard: 'tarjeta de empresa',
chooseCardFeed: 'Elige feed de tarjetas',
},
expensifyCard: {
@@ -3349,6 +3373,9 @@ const translations = {
emptyAddedFeedDescription: 'Comienza asignando tu primera tarjeta a un miembro.',
pendingFeedTitle: `Estamos revisando tu solicitud...`,
pendingFeedDescription: `Actualmente estamos revisando los detalles de tu feed. Una vez hecho esto, nos pondremos en contacto contigo a través de`,
+ pendingBankTitle: 'Comprueba la ventana de tu navegador',
+ pendingBankDescription: ({bankName}: CompanyCardBankName) => `Conéctese a ${bankName} a través de la ventana del navegador que acaba de abrir. Si no se abrió, `,
+ pendingBankLink: 'por favor haga clic aquí.',
giveItNameInstruction: 'Nombra la tarjeta para distingirla de las demás.',
updating: 'Actualizando...',
noAccountsFound: 'No se han encontrado cuentas',
@@ -3588,6 +3615,7 @@ const translations = {
},
errorODIntegration: 'Hay un error con una conexión que se ha configurado en Expensify Classic. ',
goToODToFix: 'Ve a Expensify Classic para solucionar este problema.',
+ goToODToSettings: 'Ve a Expensify Classic para gestionar tus configuraciones.',
setup: 'Configurar',
lastSync: ({relativeDate}: LastSyncAccountingParams) => `Recién sincronizado ${relativeDate}`,
import: 'Importar',
@@ -3596,6 +3624,7 @@ const translations = {
other: 'Otras integraciones',
syncNow: 'Sincronizar ahora',
disconnect: 'Desconectar',
+ reinstall: 'Reinstalar el conector',
disconnectTitle: ({connectionName}: OptionalParam = {}) => {
const integrationName =
connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'integración';
@@ -3643,14 +3672,18 @@ const translations = {
syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
case 'quickbooksOnlineImportCustomers':
+ case 'quickbooksDesktopImportCustomers':
return 'Importando clientes';
case 'quickbooksOnlineImportEmployees':
case 'netSuiteSyncImportEmployees':
case 'intacctImportEmployees':
+ case 'quickbooksDesktopImportEmployees':
return 'Importando empleados';
case 'quickbooksOnlineImportAccounts':
+ case 'quickbooksDesktopImportAccounts':
return 'Importando cuentas';
case 'quickbooksOnlineImportClasses':
+ case 'quickbooksDesktopImportClasses':
return 'Importando clases';
case 'quickbooksOnlineImportLocations':
return 'Importando localidades';
@@ -3669,6 +3702,19 @@ const translations = {
return 'Importando datos desde Xero';
case 'startingImportQBO':
return 'Importando datos desde QuickBooks Online';
+ case 'startingImportQBD':
+ case 'quickbooksDesktopImportMore':
+ return 'Importando datos desde QuickBooks Desktop';
+ case 'quickbooksDesktopImportTitle':
+ return 'Importando título';
+ case 'quickbooksDesktopImportApproveCertificate':
+ return 'Importando certificado de aprobación';
+ case 'quickbooksDesktopImportDimensions':
+ return 'Importando dimensiones';
+ case 'quickbooksDesktopImportSavePolicy':
+ return 'Importando política de guardado';
+ case 'quickbooksDesktopWebConnectorReminder':
+ return 'Aún sincronizando datos con QuickBooks... Por favor, asegúrate de que el Conector Web esté en funcionamiento';
case 'quickbooksOnlineSyncTitle':
return 'Sincronizando datos desde QuickBooks Online';
case 'quickbooksOnlineSyncLoadData':
@@ -3736,6 +3782,7 @@ const translations = {
case 'netSuiteSyncImportSubsidiaries':
return 'Importando subsidiarias';
case 'netSuiteSyncImportVendors':
+ case 'quickbooksDesktopImportVendors':
return 'Importando proveedores';
case 'netSuiteSyncExpensifyReimbursedReports':
return 'Marcando facturas y recibos de NetSuite como pagados';
@@ -3823,14 +3870,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',
@@ -3846,8 +3885,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',
@@ -4008,6 +4047,11 @@ const translations = {
description: `Disfruta de una sincronización automatizada y reduce las entradas manuales con la integración Expensify + Sage Intacct. Obtén información financiera en profundidad y en tiempo real con dimensiones definidas por el usuario, así como codificación de gastos por departamento, clase, ubicación, cliente y proyecto (trabajo).`,
onlyAvailableOnPlan: 'Nuestra integración Sage Intacct sólo está disponible en el plan Control, a partir de ',
},
+ [CONST.POLICY.CONNECTIONS.NAME.QBD]: {
+ title: 'QuickBooks Desktop',
+ description: `Disfruta de la sincronización automática y reduce las entradas manuales con la integración de Expensify + QuickBooks Desktop. Obtén la máxima eficiencia con una conexión bidireccional en tiempo real y la codificación de gastos por clase, artículo, cliente y proyecto.`,
+ onlyAvailableOnPlan: 'Nuestra integración con QuickBooks Desktop solo está disponible en el plan Control, que comienza en ',
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: {
title: 'Aprobaciones anticipadas',
description: `Si quieres añadir más niveles de aprobación, o simplemente asegurarte de que los gastos más importantes reciben otro vistazo, no hay problema. Las aprobaciones avanzadas ayudan a realizar las comprobaciones adecuadas a cada nivel para mantener los gastos de tu equipo bajo control.`,
@@ -4050,7 +4094,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.',
@@ -4297,7 +4341,11 @@ 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',
+ subtitle: 'Usa el botón verde de abajo para crear un gasto o haz un tour por Expensify para aprender más.',
},
emptyTripResults: {
title: 'No tienes viajes',
@@ -5608,6 +5656,30 @@ const translations = {
hasChildReportAwaitingAction: 'Informe secundario pendiente de acción',
hasMissingInvoiceBankAccount: 'Falta la cuenta bancaria de la factura',
},
+ reasonRBR: {
+ hasErrors: 'Tiene errores en los datos o las acciones del informe',
+ hasViolations: 'Tiene violaciones',
+ hasTransactionThreadViolations: 'Tiene violaciones de hilo de transacciones',
+ },
+ indicatorStatus: {
+ theresAReportAwaitingAction: 'Hay un informe pendiente de acción',
+ theresAReportWithErrors: 'Hay un informe con errores',
+ theresAWorkspaceWithCustomUnitsErrors: 'Hay un espacio de trabajo con errores en las unidades personalizadas',
+ theresAProblemWithAWorkspaceMember: 'Hay un problema con un miembro del espacio de trabajo',
+ theresAProblemWithAContactMethod: 'Hay un problema con un método de contacto',
+ aContactMethodRequiresVerification: 'Un método de contacto requiere verificación',
+ theresAProblemWithAPaymentMethod: 'Hay un problema con un método de pago',
+ theresAProblemWithAWorkspace: 'Hay un problema con un espacio de trabajo',
+ theresAProblemWithYourReimbursementAccount: 'Hay un problema con tu cuenta de reembolso',
+ theresABillingProblemWithYourSubscription: 'Hay un problema de facturación con tu suscripción',
+ yourSubscriptionHasBeenSuccessfullyRenewed: 'Tu suscripción se ha renovado con éxito',
+ theresWasAProblemDuringAWorkspaceConnectionSync: 'Hubo un problema durante la sincronización de la conexión del espacio de trabajo',
+ theresAProblemWithYourWallet: 'Hay un problema con tu billetera',
+ theresAProblemWithYourWalletTerms: 'Hay un problema con los términos de tu billetera',
+ },
+ },
+ emptySearchView: {
+ takeATour: 'Haz un tour',
},
};
diff --git a/src/languages/params.ts b/src/languages/params.ts
index 12b119777016..9341b914d1d0 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -477,7 +477,7 @@ type SpreadCategoriesParams = {
};
type AssignedYouCardParams = {
- assigner: string;
+ link: string;
};
type FeatureNameParams = {
@@ -538,6 +538,10 @@ type ImportedTypesParams = {
importedTypes: string[];
};
+type CompanyCardBankName = {
+ bankName: string;
+};
+
export type {
AuthenticationErrorParams,
ImportMembersSuccessfullDescriptionParams,
@@ -729,6 +733,7 @@ export type {
DateParams,
FiltersAmountBetweenParams,
StatementPageTitleParams,
+ CompanyCardBankName,
DisconnectPromptParams,
DisconnectTitleParams,
CharacterLengthLimitParams,
diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index 0d1bab053182..ad0650374011 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -208,23 +208,32 @@ function paginate;
-function paginate>(
+function paginate>(
type: TRequestType,
command: TCommand,
apiCommandParameters: ApiRequestCommandParameters[TCommand],
onyxData: OnyxData,
config: PaginationConfig,
): void;
+function paginate>(
+ type: TRequestType,
+ command: TCommand,
+ apiCommandParameters: ApiRequestCommandParameters[TCommand],
+ onyxData: OnyxData,
+ config: PaginationConfig,
+ conflictResolver?: RequestConflictResolver,
+): void;
function paginate>(
type: TRequestType,
command: TCommand,
apiCommandParameters: ApiRequestCommandParameters[TCommand],
onyxData: OnyxData,
config: PaginationConfig,
+ conflictResolver: RequestConflictResolver = {},
): Promise | void {
Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters});
const request: PaginatedRequest = {
- ...prepareRequest(command, type, apiCommandParameters, onyxData),
+ ...prepareRequest(command, type, apiCommandParameters, onyxData, conflictResolver),
...config,
...{
isPaginated: true,
diff --git a/src/libs/API/parameters/AssignCompanyCardParams.ts b/src/libs/API/parameters/AssignCompanyCardParams.ts
new file mode 100644
index 000000000000..c4dcd7c628a0
--- /dev/null
+++ b/src/libs/API/parameters/AssignCompanyCardParams.ts
@@ -0,0 +1,10 @@
+type AssignCompanyCardParams = {
+ policyID: string;
+ bankName: string;
+ encryptedCardNumber: string;
+ email: string;
+ startDate: string;
+ reportActionID: string;
+};
+
+export default AssignCompanyCardParams;
diff --git a/src/libs/API/parameters/ExportReportCSVParams.ts b/src/libs/API/parameters/ExportReportCSVParams.ts
new file mode 100644
index 000000000000..ad66b0d8bae2
--- /dev/null
+++ b/src/libs/API/parameters/ExportReportCSVParams.ts
@@ -0,0 +1,6 @@
+type ExportReportCSVParams = {
+ transactionIDList: string[];
+ reportID: string;
+};
+
+export default ExportReportCSVParams;
diff --git a/src/libs/API/parameters/OpenPolicyCompanyCardsFeedParams.ts b/src/libs/API/parameters/OpenPolicyCompanyCardsFeedParams.ts
new file mode 100644
index 000000000000..e3a8653886b9
--- /dev/null
+++ b/src/libs/API/parameters/OpenPolicyCompanyCardsFeedParams.ts
@@ -0,0 +1,6 @@
+type OpenPolicyCompanyCardsFeedParams = {
+ policyID: string;
+ feed: string;
+};
+
+export default OpenPolicyCompanyCardsFeedParams;
diff --git a/src/libs/API/parameters/RequestFeedSetupParams.ts b/src/libs/API/parameters/RequestFeedSetupParams.ts
new file mode 100644
index 000000000000..98e22c611efd
--- /dev/null
+++ b/src/libs/API/parameters/RequestFeedSetupParams.ts
@@ -0,0 +1,8 @@
+type RequestFeedSetupParams = {
+ authToken: string;
+ policyID: string;
+ feedDetails: string;
+ feedType: string;
+};
+
+export default RequestFeedSetupParams;
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/SyncPolicyToQuickbooksDesktopParams.ts b/src/libs/API/parameters/SyncPolicyToQuickbooksDesktopParams.ts
new file mode 100644
index 000000000000..db6d9cd43437
--- /dev/null
+++ b/src/libs/API/parameters/SyncPolicyToQuickbooksDesktopParams.ts
@@ -0,0 +1,7 @@
+type SyncPolicyToQuickbooksDesktopParams = {
+ policyID: string;
+ idempotencyKey: string;
+ forceDataRefresh?: boolean;
+};
+
+export default SyncPolicyToQuickbooksDesktopParams;
diff --git a/src/libs/API/parameters/UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams.ts b/src/libs/API/parameters/UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams.ts
new file mode 100644
index 000000000000..6d43fe03670c
--- /dev/null
+++ b/src/libs/API/parameters/UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams.ts
@@ -0,0 +1,11 @@
+import type {QBDNonReimbursableExportAccountType} from '@src/types/onyx/Policy';
+
+type UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams = {
+ policyID: string;
+ nonReimbursableExpensesExportDestination: QBDNonReimbursableExportAccountType;
+ nonReimbursableExpensesAccount: string;
+ nonReimbursableBillDefaultVendor: string;
+ idempotencyKey: string;
+};
+
+export default UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index ddf10a138725..9f07049736ed 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -19,6 +19,7 @@ export type {default as OpenPolicyInitialPageParams} from './OpenPolicyInitialPa
export type {default as SyncPolicyToQuickbooksOnlineParams} from './SyncPolicyToQuickbooksOnlineParams';
export type {default as SyncPolicyToXeroParams} from './SyncPolicyToXeroParams';
export type {default as SyncPolicyToNetSuiteParams} from './SyncPolicyToNetSuiteParams';
+export type {default as SyncPolicyToQuickbooksDesktopParams} from './SyncPolicyToQuickbooksDesktopParams';
export type {default as DeleteContactMethodParams} from './DeleteContactMethodParams';
export type {default as DeletePaymentBankAccountParams} from './DeletePaymentBankAccountParams';
export type {default as DeletePaymentCardParams} from './DeletePaymentCardParams';
@@ -273,6 +274,7 @@ export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIn
export type {default as UpdateNetSuiteCustomersJobsParams} from './UpdateNetSuiteCustomersJobsParams';
export type {default as CopyExistingPolicyConnectionParams} from './CopyExistingPolicyConnectionParams';
export type {default as ExportSearchItemsToCSVParams} from './ExportSearchItemsToCSVParams';
+export type {default as ExportReportCSVParams} from './ExportReportCSVParams';
export type {default as UpdateExpensifyCardLimitParams} from './UpdateExpensifyCardLimitParams';
export type {CreateWorkspaceApprovalParams, UpdateWorkspaceApprovalParams, RemoveWorkspaceApprovalParams} from './WorkspaceApprovalParams';
export type {default as StartIssueNewCardFlowParams} from './StartIssueNewCardFlowParams';
@@ -327,11 +329,15 @@ export type {default as UpdateCardSettlementAccountParams} from './UpdateCardSet
export type {default as SetCompanyCardFeedName} from './SetCompanyCardFeedName';
export type {default as DeleteCompanyCardFeed} from './DeleteCompanyCardFeed';
export type {default as SetCompanyCardTransactionLiability} from './SetCompanyCardTransactionLiability';
+export type {default as OpenPolicyCompanyCardsFeedParams} from './OpenPolicyCompanyCardsFeedParams';
+export type {default as AssignCompanyCardParams} from './AssignCompanyCardParams';
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 SetMissingPersonalDetailsAndShipExpensifyCardParams} from './SetMissingPersonalDetailsAndShipExpensifyCardParams';
+export type {default as SetPersonalDetailsAndShipExpensifyCardsParams} from './SetPersonalDetailsAndShipExpensifyCardsParams';
+export type {default as RequestFeedSetupParams} from './RequestFeedSetupParams';
export type {default as SetInvoicingTransferBankAccountParams} from './SetInvoicingTransferBankAccountParams';
export type {default as ConnectPolicyToQuickBooksDesktopParams} from './ConnectPolicyToQuickBooksDesktopParams';
export type {default as UpdateQuickbooksDesktopExpensesExportDestinationTypeParams} from './UpdateQuickbooksDesktopExpensesExportDestinationTypeParams';
+export type {default as UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams} from './UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 14387b0e170c..063be53a2eda 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -149,6 +149,7 @@ const WRITE_COMMANDS = {
EXPORT_CATEGORIES_CSV: 'ExportCategoriesCSV',
EXPORT_MEMBERS_CSV: 'ExportMembersCSV',
EXPORT_TAGS_CSV: 'ExportTagsCSV',
+ EXPORT_REPORT_TO_CSV: 'ExportReportToCSV',
RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory',
CREATE_POLICY_TAG: 'CreatePolicyTag',
RENAME_POLICY_TAG: 'RenamePolicyTag',
@@ -174,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',
@@ -259,7 +261,10 @@ const WRITE_COMMANDS = {
UPDATE_QUICKBOOKS_ONLINE_EXPORT: 'UpdateQuickbooksOnlineExport',
UPDATE_QUICKBOOKS_DESKTOP_EXPORT_DATE: 'UpdateQuickbooksDesktopExportDate',
UPDATE_MANY_POLICY_CONNECTION_CONFIGS: 'UpdateManyPolicyConnectionConfigurations',
+ UPDATE_QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'UpdateQuickbooksDesktopNonReimbursableExpensesExportDestination',
+ UPDATE_QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPENSES_ACCOUNT: 'UpdateQuickbooksDesktopNonReimbursableExpensesAccount',
UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR: 'UpdateQuickbooksDesktopAutoCreateVendor',
+ UPDATE_QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_BILL_DEFAULT_VENDOR: 'UpdateQuickbooksDesktopNonReimbursableBillDefaultVendor',
UPDATE_QUICKBOOKS_DESKTOP_AUTO_SYNC: 'UpdateQuickbooksDesktopAutoSync',
UPDATE_QUICKBOOKS_DESKTOP_EXPORT: 'UpdateQuickbooksDesktopExport',
UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_ACCOUNT: 'UpdateQuickbooksDesktopReimbursableExpensesAccount',
@@ -268,6 +273,7 @@ const WRITE_COMMANDS = {
UPDATE_QUICKBOOKS_DESKTOP_ENABLE_NEW_CATEGORIES: 'UpdateQuickbooksDesktopEnableNewCategories',
UPDATE_QUICKBOOKS_DESKTOP_SYNC_CLASSES: 'UpdateQuickbooksDesktopSyncClasses',
UPDATE_QUICKBOOKS_DESKTOP_SYNC_CUSTOMERS: 'UpdateQuickbooksDesktopSyncCustomers',
+ UPDATE_QUICKBOOKS_DESKTOP_SYNC_ITEMS: 'UpdateQuickbooksDesktopSyncItems',
REMOVE_POLICY_CONNECTION: 'RemovePolicyConnection',
SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled',
DELETE_POLICY_TAXES: 'DeletePolicyTaxes',
@@ -413,14 +419,16 @@ const WRITE_COMMANDS = {
UPDATE_XERO_SYNC_INVOICE_COLLECTIONS_ACCOUNT_ID: 'UpdateXeroSyncInvoiceCollectionsAccountID',
UPDATE_XERO_SYNC_SYNC_REIMBURSED_REPORTS: 'UpdateXeroSyncSyncReimbursedReports',
UPDATE_XERO_SYNC_REIMBURSEMENT_ACCOUNT_ID: 'UpdateXeroSyncReimbursementAccountID',
+ REQUEST_FEED_SETUP: 'RequestFeedSetup',
SET_COMPANY_CARD_FEED_NAME: 'SetFeedName',
DELETE_COMPANY_CARD_FEED: 'RemoveFeed',
SET_COMPANY_CARD_TRANSACTION_LIABILITY: 'SetFeedTransactionLiability',
+ ASSIGN_COMPANY_CARD: 'AssignCard',
UNASSIGN_COMPANY_CARD: 'UnassignCard',
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;
@@ -480,8 +488,10 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_STATUS]: Parameters.UpdateStatusParams;
[WRITE_COMMANDS.CLEAR_STATUS]: null;
[WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET]: Parameters.UpdatePersonalDetailsForWalletParams;
+ [WRITE_COMMANDS.REQUEST_FEED_SETUP]: Parameters.RequestFeedSetupParams;
[WRITE_COMMANDS.SET_COMPANY_CARD_FEED_NAME]: Parameters.SetCompanyCardFeedName;
[WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED]: Parameters.DeleteCompanyCardFeed;
+ [WRITE_COMMANDS.ASSIGN_COMPANY_CARD]: Parameters.AssignCompanyCardParams;
[WRITE_COMMANDS.UNASSIGN_COMPANY_CARD]: Parameters.UnassignCompanyCard;
[WRITE_COMMANDS.UPDATE_COMPANY_CARD]: Parameters.UpdateCompanyCard;
[WRITE_COMMANDS.UPDATE_COMPANY_CARD_NAME]: Parameters.UpdateCompanyCardNameParams;
@@ -583,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;
@@ -702,12 +713,16 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_EXPORT_DATE]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_MARK_CHECKS_TO_BE_PRINTED]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_CREATE_VENDOR]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
+ [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPENSES_ACCOUNT]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
+ [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams;
+ [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_BILL_DEFAULT_VENDOR]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_AUTO_SYNC]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_ACCOUNT]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateQuickbooksDesktopExpensesExportDestinationTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_ENABLE_NEW_CATEGORIES]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_SYNC_CLASSES]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_SYNC_CUSTOMERS]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
+ [WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_SYNC_ITEMS]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_DESKTOP_EXPORT]: Parameters.UpdateQuickbooksDesktopGenericTypeParams;
[WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams;
[WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS]: Parameters.UpdateManyPolicyConnectionConfigurationsParams;
@@ -820,6 +835,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_TAX_CONFIGURATION]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_SAGE_INTACCT_USER_DIMENSION]: Parameters.UpdateSageIntacctGenericTypeParams<'dimensions', string>;
[WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV]: Parameters.ExportSearchItemsToCSVParams;
+ [WRITE_COMMANDS.EXPORT_REPORT_TO_CSV]: Parameters.ExportReportCSVParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_APPROVAL]: Parameters.CreateWorkspaceApprovalParams;
[WRITE_COMMANDS.UPDATE_WORKSPACE_APPROVAL]: Parameters.UpdateWorkspaceApprovalParams;
[WRITE_COMMANDS.REMOVE_WORKSPACE_APPROVAL]: Parameters.RemoveWorkspaceApprovalParams;
@@ -834,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;
@@ -863,6 +879,7 @@ const READ_COMMANDS = {
SYNC_POLICY_TO_XERO: 'SyncPolicyToXero',
SYNC_POLICY_TO_NETSUITE: 'SyncPolicyToNetSuite',
SYNC_POLICY_TO_SAGE_INTACCT: 'SyncPolicyToSageIntacct',
+ SYNC_POLICY_TO_QUICKBOOKS_DESKTOP: 'SyncPolicyToQuickbooksDesktop',
OPEN_REIMBURSEMENT_ACCOUNT_PAGE: 'OpenReimbursementAccountPage',
OPEN_WORKSPACE_VIEW: 'OpenWorkspaceView',
GET_MAPBOX_ACCESS_TOKEN: 'GetMapboxAccessToken',
@@ -896,6 +913,7 @@ const READ_COMMANDS = {
OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage',
OPEN_POLICY_REPORT_FIELDS_PAGE: 'OpenPolicyReportFieldsPage',
OPEN_POLICY_EXPENSIFY_CARDS_PAGE: 'OpenPolicyExpensifyCardsPage',
+ OPEN_POLICY_COMPANY_CARDS_FEED: 'OpenPolicyCompanyCardsFeed',
OPEN_POLICY_COMPANY_CARDS_PAGE: 'OpenPolicyCompanyCardsPage',
OPEN_POLICY_EDIT_CARD_LIMIT_TYPE_PAGE: 'OpenPolicyEditCardLimitTypePage',
OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage',
@@ -921,6 +939,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.SYNC_POLICY_TO_XERO]: Parameters.SyncPolicyToXeroParams;
[READ_COMMANDS.SYNC_POLICY_TO_NETSUITE]: Parameters.SyncPolicyToNetSuiteParams;
[READ_COMMANDS.SYNC_POLICY_TO_SAGE_INTACCT]: Parameters.SyncPolicyToNetSuiteParams;
+ [READ_COMMANDS.SYNC_POLICY_TO_QUICKBOOKS_DESKTOP]: Parameters.SyncPolicyToQuickbooksDesktopParams;
[READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: Parameters.OpenReimbursementAccountPageParams;
[READ_COMMANDS.OPEN_WORKSPACE_VIEW]: Parameters.OpenWorkspaceViewParams;
[READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: null;
@@ -961,6 +980,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams;
[READ_COMMANDS.OPEN_POLICY_EXPENSIFY_CARDS_PAGE]: Parameters.OpenPolicyExpensifyCardsPageParams;
[READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_PAGE]: Parameters.OpenPolicyExpensifyCardsPageParams;
+ [READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED]: Parameters.OpenPolicyCompanyCardsFeedParams;
[READ_COMMANDS.OPEN_POLICY_EDIT_CARD_LIMIT_TYPE_PAGE]: Parameters.OpenPolicyEditCardLimitTypePageParams;
[READ_COMMANDS.OPEN_POLICY_PROFILE_PAGE]: Parameters.OpenPolicyProfilePageParams;
[READ_COMMANDS.OPEN_POLICY_INITIAL_PAGE]: Parameters.OpenPolicyInitialPageParams;
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index 0ee037c3c354..9fda616557a8 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -1,14 +1,14 @@
import groupBy from 'lodash/groupBy';
import Onyx from 'react-native-onyx';
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
+import ExpensifyCardImage from '@assets/images/expensify-card.svg';
import * as Illustrations from '@src/components/Icon/Illustrations';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {OnyxValues} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {BankAccountList, Card, CardList, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx';
-import type Policy from '@src/types/onyx/Policy';
+import type {BankAccountList, Card, CardFeeds, CardList, CompanyCardFeed, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
import localeCompare from './LocaleCompare';
@@ -172,7 +172,8 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry, personalDetails: OnyxEntry): Card[] {
- return Object.values(cardsList ?? {}).sort((cardA: Card, cardB: Card) => {
+ const {cardList, ...cards} = cardsList ?? {};
+ return Object.values(cards).sort((cardA: Card, cardB: Card) => {
const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {};
const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {};
@@ -183,48 +184,64 @@ function sortCardsByCardholderName(cardsList: OnyxEntry, per
});
}
-function getCardFeedIcon(cardFeed: string): IconAsset {
- if (cardFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD)) {
- return Illustrations.MasterCardCompanyCards;
- }
-
- if (cardFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.VISA)) {
- return Illustrations.VisaCompanyCards;
+function getCompanyCardNumber(cardList: Record, lastFourPAN?: string): string {
+ if (!lastFourPAN) {
+ return '';
}
- return Illustrations.AmexCompanyCards;
+ return Object.keys(cardList).find((card) => card.endsWith(lastFourPAN)) ?? '';
}
-function getCardDetailsImage(cardFeed: string): IconAsset {
- if (cardFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD)) {
- return Illustrations.MasterCardCompanyCardDetail;
+function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD.BANK): IconAsset {
+ const feedIcons = {
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: Illustrations.VisaCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX]: Illustrations.AmexCardCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: Illustrations.MasterCardCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX_DIRECT]: Illustrations.AmexCardCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.BANK_OF_AMERICA]: Illustrations.BankOfAmericaCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]: Illustrations.CapitalOneCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]: Illustrations.ChaseCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CITIBANK]: Illustrations.CitibankCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.WELLS_FARGO]: Illustrations.WellsFargoCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.BREX]: Illustrations.BrexCompanyCardDetail,
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE]: Illustrations.StripeCompanyCardDetail,
+ [CONST.EXPENSIFY_CARD.BANK]: ExpensifyCardImage,
+ };
+
+ if (cardFeed.startsWith(CONST.EXPENSIFY_CARD.BANK)) {
+ return ExpensifyCardImage;
+ }
+
+ if (feedIcons[cardFeed]) {
+ return feedIcons[cardFeed];
}
- if (cardFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.VISA)) {
- return Illustrations.VisaCompanyCardDetail;
+ // In existing OldDot setups other variations of feeds could exist, ex: vcf2, vcf3, cdfbmo
+ const feedKey = (Object.keys(feedIcons) as CompanyCardFeed[]).find((feed) => cardFeed.startsWith(feed));
+
+ if (feedKey) {
+ return feedIcons[feedKey];
}
- return Illustrations.AmexCardCompanyCardDetail;
+ return Illustrations.AmexCompanyCards;
}
-function getMemberCards(policy: OnyxEntry, allCardsList: OnyxCollection, accountID?: number) {
- const workspaceId = policy?.workspaceAccountID ? policy.workspaceAccountID.toString() : '';
- const cards: WorkspaceCardsList = {};
- const mockedCardsList = allCardsList ?? {};
- Object.keys(mockedCardsList)
- .filter((key) => key !== `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceId}_${CONST.EXPENSIFY_CARD.BANK}` && key.includes(workspaceId))
- .forEach((key) => {
- const feedCards = mockedCardsList?.[key];
- if (feedCards && Object.keys(feedCards).length > 0) {
- Object.keys(feedCards).forEach((feedCardKey) => {
- if (feedCards?.[feedCardKey].accountID !== accountID) {
- return;
- }
- cards[feedCardKey] = feedCards[feedCardKey];
- });
- }
- });
- return cards;
+function getCardFeedName(feedType: CompanyCardFeed): string {
+ const feedNamesMapping = {
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: 'Visa',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: 'Mastercard',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX]: 'American Express',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE]: 'Stripe',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX_DIRECT]: 'American Express',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.BANK_OF_AMERICA]: 'Bank of America',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]: 'Capital One',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]: 'Chase',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CITIBANK]: 'Citibank',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.WELLS_FARGO]: 'Wells Fargo',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.BREX]: 'Brex',
+ };
+
+ return feedNamesMapping[feedType];
}
const getBankCardDetailsImage = (bank: ValueOf): IconAsset => {
@@ -275,6 +292,11 @@ const getCorrectStepForSelectedBank = (selectedBank: ValueOf, cardFeeds: OnyxEntry): CompanyCardFeed {
+ const defaultFeed = Object.keys(cardFeeds?.settings?.companyCards ?? {}).at(0) as CompanyCardFeed;
+ return lastSelectedFeed ?? defaultFeed;
+}
+
export {
isExpensifyCard,
isCorporateCard,
@@ -290,9 +312,10 @@ export {
getTranslationKeyForLimitType,
getEligibleBankAccountsForCard,
sortCardsByCardholderName,
+ getCompanyCardNumber,
getCardFeedIcon,
- getCardDetailsImage,
- getMemberCards,
+ getCardFeedName,
getBankCardDetailsImage,
+ getSelectedFeed,
getCorrectStepForSelectedBank,
};
diff --git a/src/libs/ConnectionUtils.ts b/src/libs/ConnectionUtils.ts
index b3a5e38ffb8a..9708ef3451c7 100644
--- a/src/libs/ConnectionUtils.ts
+++ b/src/libs/ConnectionUtils.ts
@@ -1,5 +1,5 @@
import CONST from '@src/CONST';
-import type {QBONonReimbursableExportAccountType} from '@src/types/onyx/Policy';
+import type {QBDNonReimbursableExportAccountType, QBONonReimbursableExportAccountType} from '@src/types/onyx/Policy';
import {translateLocal} from './Localize';
function getQBONonReimbursableExportAccountType(exportDestination: QBONonReimbursableExportAccountType | undefined): string {
@@ -15,5 +15,17 @@ function getQBONonReimbursableExportAccountType(exportDestination: QBONonReimbur
}
}
-// eslint-disable-next-line import/prefer-default-export
-export {getQBONonReimbursableExportAccountType};
+function getQBDNonReimbursableExportAccountType(exportDestination: QBDNonReimbursableExportAccountType | undefined): string {
+ switch (exportDestination) {
+ case CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CHECK:
+ return translateLocal('workspace.qbd.bankAccount');
+ case CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD:
+ return translateLocal('workspace.qbd.creditCardAccount');
+ case CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL:
+ return translateLocal('workspace.qbd.accountsPayable');
+ default:
+ return translateLocal('workspace.qbd.account');
+ }
+}
+
+export {getQBONonReimbursableExportAccountType, getQBDNonReimbursableExportAccountType};
diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts
index e7ad63467781..d63758761c3c 100644
--- a/src/libs/DebugUtils.ts
+++ b/src/libs/DebugUtils.ts
@@ -7,6 +7,7 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Beta, Policy, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx';
import * as ReportUtils from './ReportUtils';
+import SidebarUtils from './SidebarUtils';
class NumberError extends SyntaxError {
constructor() {
@@ -645,13 +646,22 @@ function getReasonAndReportActionForGBRInLHNRow(report: OnyxEntry): GBRR
return null;
}
+type RBRReasonAndReportAction = {
+ reason: TranslationPaths;
+ reportAction: OnyxEntry;
+};
+
/**
* Gets the report action that is causing the RBR to show up in LHN
*/
-function getRBRReportAction(report: OnyxEntry, reportActions: OnyxEntry): OnyxEntry {
- const {reportAction} = ReportUtils.getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions);
+function getReasonAndReportActionForRBRInLHNRow(report: Report, reportActions: OnyxEntry, hasViolations: boolean): RBRReasonAndReportAction | null {
+ const {reason, reportAction} = SidebarUtils.getReasonAndReportActionThatHasRedBrickRoad(report, reportActions, hasViolations, transactionViolations) ?? {};
- return reportAction;
+ if (reason) {
+ return {reason: `debug.reasonRBR.${reason}`, reportAction};
+ }
+
+ return null;
}
const DebugUtils = {
@@ -673,7 +683,7 @@ const DebugUtils = {
validateReportActionJSON,
getReasonForShowingRowInLHN,
getReasonAndReportActionForGBRInLHNRow,
- getRBRReportAction,
+ getReasonAndReportActionForRBRInLHNRow,
REPORT_ACTION_REQUIRED_PROPERTIES,
REPORT_REQUIRED_PROPERTIES,
};
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index 3efd711d4258..286f952b3484 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -1,10 +1,9 @@
import type {OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
-import type {RateAndUnit} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {LastSelectedDistanceRates, OnyxInputOrEntry} from '@src/types/onyx';
+import type {LastSelectedDistanceRates, OnyxInputOrEntry, Transaction} from '@src/types/onyx';
import type {Unit} from '@src/types/onyx/Policy';
import type Policy from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -12,6 +11,7 @@ import * as CurrencyUtils from './CurrencyUtils';
import * as PolicyUtils from './PolicyUtils';
import * as ReportConnection from './ReportConnection';
import * as ReportUtils from './ReportUtils';
+import * as TransactionUtils from './TransactionUtils';
type MileageRate = {
customUnitRateID?: string;
@@ -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;
}
@@ -219,17 +219,30 @@ function getDistanceMerchant(
return `${distanceInUnits} @ ${ratePerUnit}`;
}
+function ensureRateDefined(rate: number | undefined): asserts rate is number {
+ if (rate !== undefined) {
+ return;
+ }
+ throw new Error('All default P2P rates should have a rate defined');
+}
+
/**
* Retrieves the rate and unit for a P2P distance expense for a given currency.
*
* @param currency
- * @returns The rate and unit in RateAndUnit object.
+ * @returns The rate and unit in MileageRate object.
*/
-function getRateForP2P(currency: string): RateAndUnit {
+function getRateForP2P(currency: string, transaction: OnyxEntry): MileageRate {
const currencyWithExistingRate = CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE[currency] ? currency : CONST.CURRENCY.USD;
+ const mileageRate = CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE[currencyWithExistingRate];
+ ensureRateDefined(mileageRate.rate);
+
+ // Ensure the rate is updated when the currency changes, otherwise use the stored rate
+ const rate = TransactionUtils.getCurrency(transaction) === currency ? transaction?.comment?.customUnit?.defaultP2PRate ?? mileageRate.rate : mileageRate.rate;
return {
- ...CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE[currencyWithExistingRate],
+ ...mileageRate,
currency: currencyWithExistingRate,
+ rate,
};
}
@@ -289,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;
}
@@ -301,6 +314,52 @@ function getTaxableAmount(policy: OnyxEntry, customUnitRateID: string, d
return amount * taxClaimablePercentage;
}
+function getDistanceUnit(transaction: OnyxEntry, mileageRate: OnyxEntry): Unit {
+ return transaction?.comment?.customUnit?.distanceUnit ?? mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES;
+}
+
+/**
+ * Get the selected rate for a transaction, from the policy or P2P default rate.
+ * Use the distanceUnit stored on the transaction by default to prevent policy changes modifying existing transactions. Otherwise, get the unit from the rate.
+ */
+function getRate({
+ transaction,
+ policy,
+ policyDraft,
+ useTransactionDistanceUnit = true,
+}: {
+ transaction: OnyxEntry;
+ policy: OnyxEntry;
+ policyDraft?: OnyxEntry;
+ useTransactionDistanceUnit?: boolean;
+}): MileageRate {
+ let mileageRates = getMileageRates(policy, true, transaction?.comment?.customUnit?.customUnitRateID);
+ if (isEmptyObject(mileageRates) && policyDraft) {
+ mileageRates = getMileageRates(policyDraft, true, transaction?.comment?.customUnit?.customUnitRateID);
+ }
+ const policyCurrency = policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD;
+ const defaultMileageRate = getDefaultMileageRate(policy);
+ const customUnitRateID = TransactionUtils.getRateID(transaction) ?? '';
+ const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? getRateForP2P(policyCurrency, transaction) : mileageRates?.[customUnitRateID] ?? defaultMileageRate;
+ const unit = getDistanceUnit(useTransactionDistanceUnit ? transaction : undefined, mileageRate);
+ return {
+ ...mileageRate,
+ unit,
+ currency: mileageRate?.currency ?? policyCurrency,
+ };
+}
+
+/**
+ * Get the updated distance unit from the selected rate instead of the distanceUnit stored on the transaction.
+ * Useful for updating the transaction distance unit when the distance or rate changes.
+ *
+ * For example, if an expense is '10 mi @ $1.00 / mi' and the rate is updated to '$1.00 / km',
+ * then the updated distance unit should be 'km' from the updated rate, not 'mi' from the currently stored transaction distance unit.
+ */
+function getUpdatedDistanceUnit({transaction, policy, policyDraft}: {transaction: OnyxEntry; policy: OnyxEntry; policyDraft?: OnyxEntry}) {
+ return getRate({transaction, policy, policyDraft, useTransactionDistanceUnit: false}).unit;
+}
+
export default {
getDefaultMileageRate,
getDistanceMerchant,
@@ -312,6 +371,9 @@ export default {
getCustomUnitRateID,
convertToDistanceInMeters,
getTaxableAmount,
+ getDistanceUnit,
+ getUpdatedDistanceUnit,
+ getRate,
};
export type {MileageRate};
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 840af5acc2c9..de9464c9c286 100644
--- a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts
+++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts
@@ -1,15 +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(() =>
@@ -31,11 +36,34 @@ const test = () => {
Performance.subscribeToMeasurements((entry) => {
console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`);
+ if (entry.name === CONST.TIMING.SIDEBAR_LOADED) {
+ const props = E2EGenericPressableWrapper.getPressableProps('searchButton');
+ if (!props) {
+ console.debug('[E2E] Search button not found, failing test!');
+ E2EClient.submitTestResults({
+ branch: Config.E2E_BRANCH,
+ error: 'Search button not found',
+ name: `${name} Open Search Router TTI`,
+ }).then(() => E2EClient.submitTestDone());
+ return;
+ }
+ if (!props.onPress) {
+ console.debug('[E2E] Search button found but onPress prop was not present, failing test!');
+ E2EClient.submitTestResults({
+ branch: Config.E2E_BRANCH,
+ error: 'Search button found but onPress prop was not present',
+ name: `${name} Open Search Router TTI`,
+ }).then(() => E2EClient.submitTestDone());
+ return;
+ }
+ // Open the search router
+ props.onPress();
+ }
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',
})
@@ -51,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/index.native.ts b/src/libs/Firebase/index.native.ts
index 0af52eefb58c..39a23440c77d 100644
--- a/src/libs/Firebase/index.native.ts
+++ b/src/libs/Firebase/index.native.ts
@@ -2,10 +2,8 @@
import crashlytics from '@react-native-firebase/crashlytics';
import perf from '@react-native-firebase/perf';
import * as Environment from '@libs/Environment/Environment';
-import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
-import * as SessionUtils from '@libs/SessionUtils';
-import type {FirebaseAttributes, Log, StartTrace, StopTrace, TraceMap} from './types';
+import type {Log, StartTrace, StopTrace, TraceMap} from './types';
+import utils from './utils';
const traceMap: TraceMap = {};
@@ -19,7 +17,7 @@ const startTrace: StartTrace = (customEventName) => {
return;
}
- const attributes = getAttributes();
+ const attributes = utils.getAttributes();
perf()
.startTrace(customEventName)
@@ -59,20 +57,6 @@ const log: Log = (action: string) => {
crashlytics().log(action);
};
-function getAttributes(): FirebaseAttributes {
- const session = SessionUtils.getSession();
-
- const accountId = session?.accountID?.toString() ?? 'N/A';
- const reportsLength = ReportConnection.getAllReportsLength().toString();
- const personalDetailsLength = PersonalDetailsUtils.getPersonalDetailsLength().toString();
-
- return {
- accountId,
- reportsLength,
- personalDetailsLength,
- };
-}
-
export default {
startTrace,
stopTrace,
diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts
index e63ea24b4a09..2d42154d3c26 100644
--- a/src/libs/Firebase/index.web.ts
+++ b/src/libs/Firebase/index.web.ts
@@ -1,10 +1,8 @@
import {trace} from '@firebase/performance';
import * as Environment from '@libs/Environment/Environment';
-import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
-import * as SessionUtils from '@libs/SessionUtils';
import {firebasePerfWeb} from './firebaseWebConfig';
-import type {FirebaseAttributes, Log, StartTrace, StopTrace, TraceMap} from './types';
+import type {Log, StartTrace, StopTrace, TraceMap} from './types';
+import utils from './utils';
const traceMap: TraceMap = {};
@@ -21,7 +19,7 @@ const startTrace: StartTrace = (customEventName) => {
const perfTrace = trace(firebasePerfWeb, customEventName);
- const attributes = getAttributes();
+ const attributes = utils.getAttributes();
Object.entries(attributes).forEach(([name, value]) => {
perfTrace.putAttribute(name, value);
@@ -55,20 +53,6 @@ const log: Log = () => {
// crashlytics is not supported on WEB
};
-function getAttributes(): FirebaseAttributes {
- const session = SessionUtils.getSession();
-
- const accountId = session?.accountID?.toString() ?? 'N/A';
- const reportsLength = ReportConnection.getAllReportsLength().toString();
- const personalDetailsLength = PersonalDetailsUtils.getPersonalDetailsLength().toString();
-
- return {
- accountId,
- reportsLength,
- personalDetailsLength,
- };
-}
-
export default {
startTrace,
stopTrace,
diff --git a/src/libs/Firebase/types.ts b/src/libs/Firebase/types.ts
index bb212596b535..4c970375c226 100644
--- a/src/libs/Firebase/types.ts
+++ b/src/libs/Firebase/types.ts
@@ -13,6 +13,12 @@ type FirebaseAttributes = {
accountId: string;
personalDetailsLength: string;
reportsLength: string;
+ reportActionsLength: string;
+ transactionViolationsLength: string;
+ policiesLength: string;
+ transactionsLength: string;
+ policyType: string;
+ policyRole: string;
};
export type {StartTrace, StopTrace, TraceMap, Log, FirebaseAttributes};
diff --git a/src/libs/Firebase/utils.ts b/src/libs/Firebase/utils.ts
new file mode 100644
index 000000000000..4f7718e691ed
--- /dev/null
+++ b/src/libs/Firebase/utils.ts
@@ -0,0 +1,36 @@
+import {getAllTransactions, getAllTransactionViolationsLength} from '@libs/actions/Transaction';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import {getActivePolicy, getAllPoliciesLength} from '@libs/PolicyUtils';
+import {getReportActionsLength} from '@libs/ReportActionsUtils';
+import * as ReportConnection from '@libs/ReportConnection';
+import * as SessionUtils from '@libs/SessionUtils';
+import type {FirebaseAttributes} from './types';
+
+function getAttributes(): FirebaseAttributes {
+ const session = SessionUtils.getSession();
+
+ const accountId = session?.accountID?.toString() ?? 'N/A';
+ const reportsLength = ReportConnection.getAllReportsLength().toString();
+ const reportActionsLength = getReportActionsLength().toString();
+ const personalDetailsLength = PersonalDetailsUtils.getPersonalDetailsLength().toString();
+ const transactionViolationsLength = getAllTransactionViolationsLength().toString();
+ const policiesLength = getAllPoliciesLength().toString();
+ const transactionsLength = getAllTransactions().toString();
+ const policy = getActivePolicy();
+
+ return {
+ accountId,
+ reportsLength,
+ reportActionsLength,
+ personalDetailsLength,
+ transactionViolationsLength,
+ policiesLength,
+ transactionsLength,
+ policyType: policy?.type ?? 'N/A',
+ policyRole: policy?.role ?? 'N/A',
+ };
+}
+
+export default {
+ getAttributes,
+};
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index 73152ed196a9..e0fd37db5b3b 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -1,12 +1,22 @@
+import Onyx from 'react-native-onyx';
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';
import Navigation from './Navigation/Navigation';
import * as TransactionUtils from './TransactionUtils';
+let lastLocationPermissionPrompt: string;
+Onyx.connect({
+ key: ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT,
+ callback: (val) => (lastLocationPermissionPrompt = val ?? ''),
+});
+
function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: IOUType, transactionID: string, reportID: string, iouAction?: IOUAction): void {
if (iouAction === CONST.IOU.ACTION.CATEGORIZE || iouAction === CONST.IOU.ACTION.SUBMIT || iouAction === CONST.IOU.ACTION.SHARE) {
Navigation.goBack();
@@ -153,6 +163,32 @@ 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 ||
+ (DateUtils.isValidDateString(lastLocationPermissionPrompt ?? '') &&
+ DateUtils.getDifferenceInDaysFromNow(new Date(lastLocationPermissionPrompt ?? '')) > CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS)
+ );
+}
+
export {
calculateAmount,
insertTagIntoTransactionTagsString,
@@ -162,4 +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 fabf7fb78591..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({
@@ -204,7 +222,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default,
- [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION]: () => require('../../../../pages/settings/Profile/Contacts/ValidateContactActionPage').default,
[SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: () => require('../../../../pages/settings/Profile/Contacts/NewContactMethodPage').default,
[SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: () => require('../../../../pages/settings/Preferences/PriorityModePage').default,
[SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default,
@@ -312,6 +329,14 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER]: () =>
require('../../../../pages/workspace/accounting/qbo/export/QuickbooksPreferredExporterConfigurationPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT]: () =>
+ require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT]: () =>
+ require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectCardPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT]: () =>
+ require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT]: () =>
+ require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT]: () =>
require('../../../../pages/workspace/accounting/qbd/export/QuickbooksDesktopExportDateSelectPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER]: () =>
@@ -340,6 +365,7 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_CUSTOMERS_DISPLAYED_AS]: () =>
require('../../../../pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersDisplayedAsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ITEMS]: () => require('../../../../pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage').default,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../../pages/ReimbursementAccount/ReimbursementAccountPage').default,
[SCREENS.GET_ASSISTANCE]: () => require('../../../../pages/GetAssistancePage').default,
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default,
@@ -355,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,
@@ -523,7 +555,6 @@ const SettingsModalStackNavigator = createModalStackNavigator
require('../../../../pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage').default,
[SCREENS.SETTINGS.DELEGATE.DELEGATE_CONFIRM]: () => require('../../../../pages/settings/Security/AddDelegate/ConfirmDelegatePage').default,
- [SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE]: () => require('../../../../pages/settings/Security/AddDelegate/DelegateMagicCodePage').default,
[SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE_MAGIC_CODE]: () =>
require('../../../../pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodePage').default,
[SCREENS.WORKSPACE.RULES_CUSTOM_NAME]: () => require('../../../../pages/workspace/rules/RulesCustomNamePage').default,
@@ -655,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}
/>
+
(getChatTabBrickRoad(activeWorkspaceID));
useEffect(() => {
setChatTabBrickRoad(getChatTabBrickRoad(activeWorkspaceID));
- }, [activeWorkspaceID, transactionViolations]);
+ }, [activeWorkspaceID, transactionViolations, reports, reportActions]);
const navigateToChats = useCallback(() => {
if (selectedTab === SCREENS.HOME) {
@@ -101,7 +104,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}));
@@ -109,8 +112,15 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
}, [activeWorkspaceID, selectedTab]);
return (
-
-
+ <>
+ {user?.isDebugModeEnabled && (
+
+ )}
+
-
-
-
-
-
-
+
+
+
+
-
+ >
);
}
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx
new file mode 100644
index 000000000000..3e5803b797dc
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx
@@ -0,0 +1,172 @@
+import React, {useCallback, useMemo} from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import Button from '@components/Button';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import Text from '@components/Text';
+import type {IndicatorStatus} from '@hooks/useIndicatorStatus';
+import useIndicatorStatus from '@hooks/useIndicatorStatus';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
+import {getChatTabBrickRoadReport} from '@libs/WorkspacesSettingsUtils';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
+import type {ReimbursementAccount} from '@src/types/onyx';
+
+type DebugTabViewProps = {
+ selectedTab?: string;
+ chatTabBrickRoad: BrickRoad;
+ activeWorkspaceID?: string;
+};
+
+function getSettingsMessage(status: IndicatorStatus | undefined): TranslationPaths | undefined {
+ switch (status) {
+ case CONST.INDICATOR_STATUS.HAS_CUSTOM_UNITS_ERROR:
+ return 'debug.indicatorStatus.theresAWorkspaceWithCustomUnitsErrors';
+ case CONST.INDICATOR_STATUS.HAS_EMPLOYEE_LIST_ERROR:
+ return 'debug.indicatorStatus.theresAProblemWithAWorkspaceMember';
+ case CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR:
+ return 'debug.indicatorStatus.theresAProblemWithAContactMethod';
+ case CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_INFO:
+ return 'debug.indicatorStatus.aContactMethodRequiresVerification';
+ case CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR:
+ return 'debug.indicatorStatus.theresAProblemWithAPaymentMethod';
+ case CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS:
+ return 'debug.indicatorStatus.theresAProblemWithAWorkspace';
+ case CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS:
+ return 'debug.indicatorStatus.theresAProblemWithYourReimbursementAccount';
+ case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS:
+ return 'debug.indicatorStatus.theresABillingProblemWithYourSubscription';
+ case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO:
+ return 'debug.indicatorStatus.yourSubscriptionHasBeenSuccessfullyRenewed';
+ case CONST.INDICATOR_STATUS.HAS_SYNC_ERRORS:
+ return 'debug.indicatorStatus.theresWasAProblemDuringAWorkspaceConnectionSync';
+ case CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS:
+ return 'debug.indicatorStatus.theresAProblemWithYourWallet';
+ case CONST.INDICATOR_STATUS.HAS_WALLET_TERMS_ERRORS:
+ return 'debug.indicatorStatus.theresAProblemWithYourWalletTerms';
+ default:
+ return undefined;
+ }
+}
+
+function getSettingsRoute(status: IndicatorStatus | undefined, reimbursementAccount: OnyxEntry, policyIDWithErrors = ''): Route | undefined {
+ switch (status) {
+ case CONST.INDICATOR_STATUS.HAS_CUSTOM_UNITS_ERROR:
+ return ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyIDWithErrors);
+ case CONST.INDICATOR_STATUS.HAS_EMPLOYEE_LIST_ERROR:
+ return ROUTES.WORKSPACE_MEMBERS.getRoute(policyIDWithErrors);
+ case CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR:
+ return ROUTES.SETTINGS_CONTACT_METHODS.route;
+ case CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_INFO:
+ return ROUTES.SETTINGS_CONTACT_METHODS.route;
+ case CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR:
+ return ROUTES.SETTINGS_WALLET;
+ case CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS:
+ return ROUTES.WORKSPACE_INITIAL.getRoute(policyIDWithErrors);
+ case CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS:
+ return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(reimbursementAccount?.achData?.currentStep, reimbursementAccount?.achData?.policyID);
+ case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS:
+ return ROUTES.SETTINGS_SUBSCRIPTION;
+ case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO:
+ return ROUTES.SETTINGS_SUBSCRIPTION;
+ case CONST.INDICATOR_STATUS.HAS_SYNC_ERRORS:
+ return ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyIDWithErrors);
+ case CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS:
+ return ROUTES.SETTINGS_WALLET;
+ case CONST.INDICATOR_STATUS.HAS_WALLET_TERMS_ERRORS:
+ return ROUTES.SETTINGS_WALLET;
+ default:
+ return undefined;
+ }
+}
+
+function DebugTabView({selectedTab = '', chatTabBrickRoad, activeWorkspaceID}: DebugTabViewProps) {
+ const StyleUtils = useStyleUtils();
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const {status, indicatorColor, policyIDWithErrors} = useIndicatorStatus();
+
+ const message = useMemo((): TranslationPaths | undefined => {
+ if (selectedTab === SCREENS.HOME) {
+ if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) {
+ return 'debug.indicatorStatus.theresAReportAwaitingAction';
+ }
+ if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) {
+ return 'debug.indicatorStatus.theresAReportWithErrors';
+ }
+ }
+ if (selectedTab === SCREENS.SETTINGS.ROOT) {
+ return getSettingsMessage(status);
+ }
+ }, [selectedTab, chatTabBrickRoad, status]);
+
+ const indicator = useMemo(() => {
+ if (selectedTab === SCREENS.HOME) {
+ if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) {
+ return theme.success;
+ }
+ if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) {
+ return theme.danger;
+ }
+ }
+ if (selectedTab === SCREENS.SETTINGS.ROOT) {
+ if (status) {
+ return indicatorColor;
+ }
+ }
+ }, [selectedTab, chatTabBrickRoad, theme.success, theme.danger, status, indicatorColor]);
+
+ const navigateTo = useCallback(() => {
+ if (selectedTab === SCREENS.HOME && !!chatTabBrickRoad) {
+ const report = getChatTabBrickRoadReport(activeWorkspaceID);
+
+ if (report) {
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID));
+ }
+ }
+ if (selectedTab === SCREENS.SETTINGS.ROOT) {
+ const route = getSettingsRoute(status, reimbursementAccount, policyIDWithErrors);
+
+ if (route) {
+ Navigation.navigate(route);
+ }
+ }
+ }, [selectedTab, chatTabBrickRoad, activeWorkspaceID, status, reimbursementAccount, policyIDWithErrors]);
+
+ if (!([SCREENS.HOME, SCREENS.SETTINGS.ROOT] as string[]).includes(selectedTab) || !indicator) {
+ return null;
+ }
+
+ return (
+
+
+
+ {message && {translate(message)} }
+
+
+
+ );
+}
+
+DebugTabView.displayName = 'DebugTabView';
+
+export default DebugTabView;
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
index 8967486165f8..eba7a7448ad0 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/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/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/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
index cec9e86c5be4..574f4d26a01c 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -6,7 +6,6 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> =
SCREENS.SETTINGS.PROFILE.DISPLAY_NAME,
SCREENS.SETTINGS.PROFILE.CONTACT_METHODS,
SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS,
- SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION,
SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD,
SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER,
SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE,
@@ -46,7 +45,6 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> =
SCREENS.SETTINGS.DELEGATE.DELEGATE_ROLE,
SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE,
SCREENS.SETTINGS.DELEGATE.DELEGATE_CONFIRM,
- SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE,
SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE_MAGIC_CODE,
],
[SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS],
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 fb13ecdf8459..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,13 @@ 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,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_DATE_SELECT,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_EXPORT_PREFERRED_EXPORTER,
@@ -61,6 +68,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_CLASSES_DISPLAYED_AS,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_CUSTOMERS,
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_CUSTOMERS_DISPLAYED_AS,
+ SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ITEMS,
SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT,
SCREENS.WORKSPACE.ACCOUNTING.XERO_CHART_OF_ACCOUNTS,
SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 65103945746a..6f551d7cc41c 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -256,9 +256,6 @@ const config: LinkingOptions['config'] = {
[SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: {
path: ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.route,
},
- [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION]: {
- path: ROUTES.SETINGS_CONTACT_METHOD_VALIDATE_ACTION,
- },
[SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: {
path: ROUTES.SETTINGS_NEW_CONTACT_METHOD.route,
exact: true,
@@ -309,12 +306,6 @@ const config: LinkingOptions['config'] = {
login: (login: string) => decodeURIComponent(login),
},
},
- [SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE]: {
- path: ROUTES.SETTINGS_DELEGATE_MAGIC_CODE.route,
- parse: {
- login: (login: string) => decodeURIComponent(login),
- },
- },
[SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE_MAGIC_CODE]: {
path: ROUTES.SETTINGS_UPDATE_DELEGATE_ROLE_MAGIC_CODE.route,
parse: {
@@ -392,6 +383,27 @@ 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,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT]: {
+ path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT]: {
+ path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_SELECT.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT]: {
+ path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT.route,
+ },
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: {
path: ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.route,
},
@@ -422,6 +434,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_CLASSES_DISPLAYED_AS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CLASSES_DISPLAYED_AS.route},
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_CUSTOMERS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CUSTOMERS.route},
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_CUSTOMERS_DISPLAYED_AS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CUSTOMERS_DISPLAYED_AS.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ITEMS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_ITEMS.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_CHART_OF_ACCOUNTS]: {path: ROUTES.POLICY_ACCOUNTING_XERO_CHART_OF_ACCOUNTS.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.route},
@@ -969,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]: {
@@ -1087,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,
@@ -1108,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/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index a9ce45214e5f..fce13143f3fe 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -379,7 +379,7 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldR
const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>;
if (shouldReplacePathInNestedState) {
- replacePathInNestedState(state, path);
+ replacePathInNestedState(state, normalizedPath);
}
if (state === undefined) {
throw new Error('Unable to parse path');
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;
@@ -210,10 +211,12 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE]: {
policyID: string;
categoryName: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.CATEGORY_GL_CODE]: {
policyID: string;
categoryName: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.CATEGORY_DEFAULT_TAX_RATE]: {
policyID: string;
@@ -251,12 +254,15 @@ type SettingsNavigatorParamList = {
};
[SCREENS.WORKSPACE.CATEGORIES_IMPORT]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.CATEGORIES_IMPORTED]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAG_CREATE]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS]: {
policyID: string;
@@ -276,40 +282,49 @@ type SettingsNavigatorParamList = {
};
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAGS_IMPORT]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAGS_IMPORTED]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAG_SETTINGS]: {
policyID: string;
orderWeight: number;
tagName: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAG_LIST_VIEW]: {
policyID: string;
orderWeight: number;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAGS_EDIT]: {
policyID: string;
orderWeight: number;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAG_EDIT]: {
policyID: string;
orderWeight: number;
tagName: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAG_APPROVER]: {
policyID: string;
orderWeight: number;
tagName: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAG_GL_CODE]: {
policyID: string;
orderWeight: number;
tagName: string;
+ backTo?: Routes;
};
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined;
[SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: {
@@ -436,6 +451,18 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ADVANCED]: {
policyID: string;
};
@@ -481,6 +508,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_CUSTOMERS_DISPLAYED_AS]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ITEMS]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: {
policyID: string;
};
@@ -736,10 +766,7 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.DELEGATE.DELEGATE_CONFIRM]: {
login: string;
role: string;
- };
- [SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE]: {
- login: string;
- role: string;
+ showValidateActionModal?: string;
};
[SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: {
/** cardID of selected card */
@@ -817,6 +844,11 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: {
+ policyID: string;
+ feed: CompanyCardFeed;
+ backTo?: Routes;
+ };
[SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME]: {
policyID: string;
};
@@ -1130,6 +1162,13 @@ type MoneyRequestNavigatorParamList = {
backTo?: Routes;
currency?: string;
};
+ [SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: {
+ action: IOUAction;
+ iouType: Exclude;
+ transactionID: string;
+ reportID: string;
+ backTo: Routes;
+ };
};
type NewTaskNavigatorParamList = {
@@ -1301,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;
@@ -1351,10 +1391,6 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.COMPANY_CARDS_ADD_NEW]: {
policyID: string;
};
- [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: {
- policyID: string;
- feed: string;
- };
[SCREENS.WORKSPACE.WORKFLOWS]: {
policyID: string;
};
@@ -1396,6 +1432,7 @@ type FullScreenNavigatorParamList = {
};
[SCREENS.WORKSPACE.TAGS]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.TAXES]: {
policyID: string;
@@ -1418,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/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 142a299f3d74..f414d2328ef6 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -35,7 +35,7 @@ import type {
Transaction,
TransactionViolation,
} from '@src/types/onyx';
-import type {Participant} from '@src/types/onyx/IOU';
+import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -178,6 +178,8 @@ type GetOptionsConfig = {
includeInvoiceRooms?: boolean;
includeDomainEmail?: boolean;
action?: IOUAction;
+ shouldAcceptName?: boolean;
+ recentAttendees?: Attendee[];
shouldBoldTitleByDefault?: boolean;
};
@@ -188,6 +190,7 @@ type GetUserToInviteConfig = {
selectedOptions?: Array>;
reportActions?: ReportActions;
showChatPreviewLine?: boolean;
+ shouldAcceptName?: boolean;
};
type MemberForList = {
@@ -219,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;
@@ -416,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),
@@ -1624,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({
@@ -1633,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);
@@ -1642,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;
}
@@ -1659,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
@@ -1722,6 +1730,7 @@ function getOptions(
includeInvoiceRooms = false,
includeDomainEmail = false,
action,
+ recentAttendees,
shouldBoldTitleByDefault = true,
}: GetOptionsConfig,
): Options {
@@ -1897,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;
@@ -1977,12 +1986,10 @@ function getOptions(
}
}
}
-
- // Add this login to the exclude list so it won't appear when we process the personal details
- if (reportOption.login) {
- optionsToExclude.push({login: reportOption.login});
- }
}
+ } 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}];
@@ -2114,34 +2121,75 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: OnyxEn
/**
* Build the options for the New Group view
*/
-function getFilteredOptions(
- reports: Array> = [],
- personalDetails: Array> = [],
- betas: OnyxEntry = [],
- searchValue = '',
- selectedOptions: Array> = [],
- excludeLogins: string[] = [],
- includeOwnedWorkspaceChats = false,
- includeP2P = true,
- includeCategories = false,
- categories: PolicyCategories = {},
- recentlyUsedCategories: string[] = [],
- includeTags = false,
- tags: PolicyTags | Array = {},
- recentlyUsedTags: string[] = [],
- canInviteUser = true,
- includeSelectedOptions = false,
- includeTaxRates = false,
- maxRecentReportsToShow: number = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
- taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault,
- includeSelfDM = false,
- includePolicyReportFieldOptions = false,
- policyReportFieldOptions: string[] = [],
- recentlyUsedPolicyReportFieldOptions: string[] = [],
- includeInvoiceRooms = false,
- action: IOUAction | undefined = undefined,
- sortByReportTypeInSearch = false,
-) {
+type FilteredOptionsParams = {
+ reports?: Array>;
+ personalDetails?: Array>;
+ betas?: OnyxEntry;
+ searchValue?: string;
+ selectedOptions?: Array>;
+ excludeLogins?: string[];
+ includeOwnedWorkspaceChats?: boolean;
+ includeP2P?: boolean;
+ includeCategories?: boolean;
+ categories?: PolicyCategories;
+ recentlyUsedCategories?: string[];
+ includeTags?: boolean;
+ tags?: PolicyTags | Array;
+ recentlyUsedTags?: string[];
+ canInviteUser?: boolean;
+ includeSelectedOptions?: boolean;
+ includeTaxRates?: boolean;
+ taxRates?: TaxRatesWithDefault;
+ maxRecentReportsToShow?: number;
+ includeSelfDM?: boolean;
+ includePolicyReportFieldOptions?: boolean;
+ policyReportFieldOptions?: string[];
+ recentlyUsedPolicyReportFieldOptions?: string[];
+ includeInvoiceRooms?: boolean;
+ action?: IOUAction;
+ sortByReportTypeInSearch?: boolean;
+};
+
+// It is not recommended to pass a search value to getFilteredOptions when passing reports and personalDetails.
+// If a search value is passed, the search value should be passed to filterOptions.
+// When it is necessary to pass a search value when passing reports and personalDetails, follow these steps:
+// 1. Use getFilteredOptions with reports and personalDetails only, without the search value.
+// 2. Pass the returned options from getFilteredOptions to filterOptions along with the search value.
+// The above constraints are enforced with TypeScript.
+
+type FilteredOptionsParamsWithDefaultSearchValue = Omit & {searchValue?: ''};
+
+type FilteredOptionsParamsWithoutOptions = Omit & {reports?: []; personalDetails?: []};
+
+function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue | FilteredOptionsParamsWithoutOptions) {
+ const {
+ reports = [],
+ personalDetails = [],
+ betas = [],
+ searchValue = '',
+ selectedOptions = [],
+ excludeLogins = [],
+ includeOwnedWorkspaceChats = false,
+ includeP2P = true,
+ includeCategories = false,
+ categories = {},
+ recentlyUsedCategories = [],
+ includeTags = false,
+ tags = {},
+ recentlyUsedTags = [],
+ canInviteUser = true,
+ includeSelectedOptions = false,
+ includeTaxRates = false,
+ maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
+ taxRates = {} as TaxRatesWithDefault,
+ includeSelfDM = false,
+ includePolicyReportFieldOptions = false,
+ policyReportFieldOptions = [],
+ recentlyUsedPolicyReportFieldOptions = [],
+ includeInvoiceRooms = false,
+ action,
+ sortByReportTypeInSearch = false,
+ } = params;
return getOptions(
{reports, personalDetails},
{
@@ -2174,6 +2222,52 @@ function getFilteredOptions(
);
}
+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,
+ },
+ );
+}
+
/**
* Build the options for the Share Destination for a Task
*/
@@ -2383,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
*/
@@ -2396,8 +2515,19 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
preferPolicyExpenseChat = false,
preferRecentExpenseReports = false,
} = config ?? {};
+ // Remove the personal details for the DMs that are already in the recent reports so that we don't show duplicates
+ function filteredPersonalDetailsOfRecentReports(recentReports: ReportUtils.OptionData[], personalDetails: ReportUtils.OptionData[]) {
+ const excludedLogins = new Set(recentReports.map((report) => report.login));
+ return personalDetails.filter((personalDetail) => !excludedLogins.has(personalDetail.login));
+ }
if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) {
- return {...options, recentReports: options.recentReports.slice(0, maxRecentReportsToShow)};
+ const recentReports = options.recentReports.slice(0, maxRecentReportsToShow);
+ const personalDetails = filteredPersonalDetailsOfRecentReports(recentReports, options.personalDetails);
+ return {
+ ...options,
+ recentReports,
+ personalDetails,
+ };
}
const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue)));
@@ -2439,7 +2569,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
const currentUserOptionSearchText = items.currentUserOption ? uniqFast(getCurrentUserSearchTerms(items.currentUserOption)).join(' ') : '';
const currentUserOption = isSearchStringMatch(term, currentUserOptionSearchText) ? items.currentUserOption : null;
-
return {
recentReports: recentReports ?? [],
personalDetails: personalDetails ?? [],
@@ -2454,28 +2583,21 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
let {recentReports, personalDetails} = matchResults;
if (sortByReportTypeInSearch) {
+ personalDetails = filteredPersonalDetailsOfRecentReports(recentReports, personalDetails);
recentReports = recentReports.concat(personalDetails);
personalDetails = [];
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);
}
+ const filteredPersonalDetails = filteredPersonalDetailsOfRecentReports(recentReports, personalDetails);
return {
- personalDetails,
+ personalDetails: filteredPersonalDetails,
recentReports: orderOptions(recentReports, searchValue, {preferChatroomsOverThreads, preferPolicyExpenseChat, preferRecentExpenseReports}),
userToInvite,
currentUserOption: matchResults.currentUserOption,
@@ -2502,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 {
@@ -2536,6 +2659,7 @@ export {
formatMemberForList,
formatSectionsFromSearchTerm,
getShareLogOptions,
+ orderOptions,
filterOptions,
createOptionList,
createOptionFromReport,
@@ -2548,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