diff --git a/.eslintrc.js b/.eslintrc.js
index d9e25cc596f7..761a62b8314b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -108,7 +108,7 @@ module.exports = {
'plugin:you-dont-need-lodash-underscore/all',
'plugin:prettier/recommended',
],
- plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler', 'lodash'],
+ 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: {
@@ -177,6 +177,7 @@ module.exports = {
// ESLint core rules
'es/no-nullish-coalescing-operators': 'off',
'es/no-optional-chaining': 'off',
+ 'deprecation/deprecation': 'off',
// Import specific rules
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml
index f0ca77bdbf00..40dfc05e5448 100644
--- a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml
+++ b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml
@@ -14,7 +14,6 @@ inputs:
GITHUB_TOKEN:
description: "Github token for authentication"
required: true
- default: "${{ github.token }}"
ANDROID:
description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')"
required: true
@@ -27,6 +26,12 @@ inputs:
WEB:
description: "Web job result ('success', 'failure', 'cancelled', or 'skipped')"
required: true
+ DATE:
+ description: "The date of deployment"
+ required: false
+ NOTE:
+ description: "Additional note from the deployer"
+ required: false
runs:
using: "node20"
main: "./index.js"
diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js
index 9f97e4a72d20..b38b04141395 100644
--- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js
+++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js
@@ -12713,9 +12713,15 @@ async function run() {
const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', { required: true }));
const iOSResult = getDeployTableMessage(core.getInput('IOS', { required: true }));
const webResult = getDeployTableMessage(core.getInput('WEB', { required: true }));
+ const date = core.getInput('DATE');
+ const note = core.getInput('NOTE');
function getDeployMessage(deployer, deployVerb, prTitle) {
let message = `🚀 [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`;
- message += ` by https://github.com/${deployer} in version: ${version} 🚀`;
+ message += ` by https://github.com/${deployer} in version: ${version} `;
+ if (date) {
+ message += `on ${date}`;
+ }
+ message += `🚀`;
message += `\n\nplatform | result\n---|---\n🤖 android 🤖|${androidResult}\n🖥 desktop 🖥|${desktopResult}`;
message += `\n🍎 iOS 🍎|${iOSResult}\n🕸 web 🕸|${webResult}`;
if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle ?? '')) {
@@ -12723,6 +12729,9 @@ async function run() {
message +=
'\n\n@Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.';
}
+ if (note) {
+ message += `\n\n_Note:_ ${note}`;
+ }
return message;
}
if (isProd) {
diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts
index 71a5c7d5c6ee..e6424c89833a 100644
--- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts
+++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts
@@ -55,9 +55,16 @@ async function run() {
const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true}) as PlatformResult);
const webResult = getDeployTableMessage(core.getInput('WEB', {required: true}) as PlatformResult);
+ const date = core.getInput('DATE');
+ const note = core.getInput('NOTE');
+
function getDeployMessage(deployer: string, deployVerb: string, prTitle?: string): string {
let message = `🚀 [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`;
- message += ` by https://github.com/${deployer} in version: ${version} 🚀`;
+ message += ` by https://github.com/${deployer} in version: ${version} `;
+ if (date) {
+ message += `on ${date}`;
+ }
+ message += `🚀`;
message += `\n\nplatform | result\n---|---\n🤖 android 🤖|${androidResult}\n🖥 desktop 🖥|${desktopResult}`;
message += `\n🍎 iOS 🍎|${iOSResult}\n🕸 web 🕸|${webResult}`;
@@ -67,6 +74,10 @@ async function run() {
'\n\n@Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.';
}
+ if (note) {
+ message += `\n\n_Note:_ ${note}`;
+ }
+
return message;
}
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 2ab19d13183a..53afe03720f7 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -649,34 +649,14 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- postGithubComment:
- name: Post a GitHub comments on all deployed PRs when platforms are done building and deploying
- runs-on: ubuntu-latest
+ postGithubComments:
+ uses: ./.github/workflows/postDeployComments.yml
if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }}
needs: [prep, android, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease]
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node
- uses: ./.github/actions/composite/setupNode
-
- - name: Get Release Pull Request List
- id: getReleasePRList
- uses: ./.github/actions/javascript/getDeployPullRequestList
- with:
- TAG: ${{ needs.prep.outputs.APP_VERSION }}
- GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
- IS_PRODUCTION_DEPLOY: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
-
- - name: Comment on issues
- uses: ./.github/actions/javascript/markPullRequestsAsDeployed
- with:
- PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }}
- IS_PRODUCTION_DEPLOY: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- DEPLOY_VERSION: ${{ needs.prep.outputs.APP_VERSION }}
- GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
- ANDROID: ${{ needs.android.result }}
- DESKTOP: ${{ needs.desktop.result }}
- IOS: ${{ needs.iOS.result }}
- WEB: ${{ needs.web.result }}
+ with:
+ version: ${{ needs.prep.outputs.APP_VERSION }}
+ env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }}
+ android: ${{ needs.android.result }}
+ ios: ${{ needs.iOS.result }}
+ web: ${{ needs.web.result }}
+ desktop: ${{ needs.desktop.result }}
diff --git a/.github/workflows/postDeployComments.yml b/.github/workflows/postDeployComments.yml
new file mode 100644
index 000000000000..3893d3cf3f7c
--- /dev/null
+++ b/.github/workflows/postDeployComments.yml
@@ -0,0 +1,118 @@
+name: Post Deploy Comments
+
+on:
+ workflow_call:
+ inputs:
+ version:
+ description: The version that was deployed
+ required: true
+ type: string
+ env:
+ description: The environment that was deployed (staging or prod)
+ required: true
+ type: string
+ android:
+ description: Android deploy status
+ required: true
+ type: string
+ ios:
+ description: iOS deploy status
+ required: true
+ type: string
+ web:
+ description: Web deploy status
+ required: true
+ type: string
+ desktop:
+ description: Desktop deploy status
+ required: true
+ type: string
+ workflow_dispatch:
+ inputs:
+ version:
+ description: The version that was deployed
+ required: true
+ type: string
+ env:
+ description: The environment that was deployed (staging or prod)
+ required: true
+ type: choice
+ options:
+ - staging
+ - production
+ android:
+ description: Android deploy status
+ required: true
+ type: choice
+ options:
+ - success
+ - failure
+ - cancelled
+ - skipped
+ ios:
+ description: iOS deploy status
+ required: true
+ type: choice
+ options:
+ - success
+ - failure
+ - cancelled
+ - skipped
+ web:
+ description: Web deploy status
+ required: true
+ type: choice
+ options:
+ - success
+ - failure
+ - cancelled
+ - skipped
+ desktop:
+ description: Desktop deploy status
+ required: true
+ type: choice
+ options:
+ - success
+ - failure
+ - cancelled
+ - skipped
+ date:
+ description: The date when this deploy occurred
+ required: false
+ type: string
+ note:
+ description: Any additional note you want to include with the deploy comment
+ required: false
+ type: string
+
+jobs:
+ postDeployComments:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: ./.github/actions/composite/setupNode
+
+ - name: Get pull request list
+ id: getPullRequestList
+ uses: ./.github/actions/javascript/getDeployPullRequestList
+ with:
+ TAG: ${{ inputs.version }}
+ GITHUB_TOKEN: ${{ github.token }}
+ IS_PRODUCTION_DEPLOY: ${{ inputs.env == 'production' }}
+
+ - name: Comment on issues
+ uses: ./.github/actions/javascript/markPullRequestsAsDeployed
+ with:
+ PR_LIST: ${{ steps.getPullRequestList.outputs.PR_LIST }}
+ IS_PRODUCTION_DEPLOY: ${{ inputs.env == 'production' }}
+ DEPLOY_VERSION: ${{ inputs.version }}
+ GITHUB_TOKEN: ${{ github.token }}
+ ANDROID: ${{ inputs.android }}
+ DESKTOP: ${{ inputs.desktop }}
+ IOS: ${{ inputs.ios }}
+ WEB: ${{ inputs.web }}
+ DATE: ${{ inputs.date }}
+ NOTE: ${{ inputs.note }}
diff --git a/README.md b/README.md
index c8faff111bae..4a691045e7c2 100644
--- a/README.md
+++ b/README.md
@@ -619,7 +619,30 @@ Some pointers:
key to the translation file and use the arrow function version, like so:
`nameOfTheKey: ({amount, dateTime}) => "User has sent " + amount + " to you on " + dateTime,`.
This is because the order of the phrases might vary from one language to another.
-
+- When working with translations that involve plural forms, it's important to handle different cases correctly.
+
+ For example:
+ - zero: Used when there are no items **(optional)**.
+ - one: Used when there's exactly one item.
+ - two: Used when there's two items. **(optional)**
+ - few: Used for a small number of items **(optional)**.
+ - many: Used for larger quantities **(optional)**.
+ - other: A catch-all case for other counts or variations.
+
+ Here’s an example of how to implement plural translations:
+
+ messages: () => ({
+ zero: 'No messages',
+ one: 'One message',
+ two: 'Two messages',
+ few: (count) => `${count} messages`,
+ many: (count) => `You have ${count} messages`,
+ other: (count) => `You have ${count} unread messages`,
+ })
+
+ In your code, you can use the translation like this:
+
+ `translate('common.messages', {count: 1});`
----
# Deploying
diff --git a/__mocks__/react-native-haptic-feedback.ts b/__mocks__/react-native-haptic-feedback.ts
new file mode 100644
index 000000000000..6d20b410d835
--- /dev/null
+++ b/__mocks__/react-native-haptic-feedback.ts
@@ -0,0 +1,5 @@
+import type HapticFeedback from 'react-native-haptic-feedback';
+
+const RNHapticFeedback: typeof HapticFeedback = {trigger: jest.fn()};
+
+export default RNHapticFeedback;
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 491c810cb350..9153966e1d5d 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 1009003902
- versionName "9.0.39-2"
+ versionCode 1009004004
+ versionName "9.0.40-4"
// 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/build.gradle b/android/build.gradle
index 85e547439cc1..fd3f9997612e 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -29,7 +29,7 @@ buildscript {
classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1")
classpath("com.google.firebase:perf-plugin:1.4.1")
// Fullstory integration
- classpath ("com.fullstory:gradle-plugin-local:1.49.0")
+ classpath ("com.fullstory:gradle-plugin-local:1.52.0")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/android/gradle.properties b/android/gradle.properties
index 87333d20f743..46cd98554d29 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -55,3 +55,5 @@ MYAPP_UPLOAD_KEY_ALIAS=ReactNativeChat-Key-Alias
disableFrameProcessors=true
android.nonTransitiveRClass=false
+
+org.gradle.parallel=true
diff --git a/assets/images/table.svg b/assets/images/table.svg
index a9cfe68f339e..36d4ced774f1 100644
--- a/assets/images/table.svg
+++ b/assets/images/table.svg
@@ -1,3 +1,3 @@
-
+
diff --git a/babel.config.js b/babel.config.js
index 3721edaa7afb..663eb29d5d2f 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -21,6 +21,7 @@ const defaultPlugins = [
'@babel/transform-runtime',
'@babel/plugin-proposal-class-properties',
+ ['@babel/plugin-transform-object-rest-spread', {useBuiltIns: true, loose: true}],
// We use `@babel/plugin-transform-class-properties` for transforming ReactNative libraries and do not use it for our own
// source code transformation as we do not use class property assignment.
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md
index b0c767fce277..37d8d8bbe42b 100644
--- a/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md
@@ -90,6 +90,10 @@ Have the employee double-check that their [default workspace](https://help.expen
- **Authorized User**: The person who will process global reimbursements. The Authorized User should be the same person who manages the bank account connection in Expensify.
- **User**: You can leave this section blank because the “User” is Expensify.
+**Does Global Reimbursement support Sepa in the EU?**
+
+Global Reimbursement uses Sepa B2B to facilitate payments from EU-based accounts. Sepa Core is not supported.
+
{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md
index 1fb1b09328b9..bda84eb0a49f 100644
--- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md
+++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md
@@ -26,9 +26,7 @@ To connect QuickBooks Desktop to Expensify, you must log into QuickBooks Desktop
7. Download the Web Connector and go through the guided installation process.
8. Open the Web Connector.
-9. Click on **Add an Application**.
-
- ![The Web Connnector Pop-up where you will need to click on Add an Application](https://help.expensify.com/assets/images/QBO_desktop_03.png){:width="100%"}
+9. Download the config file when prompted during the setup process, then open it using your File Explorer. This will automatically load the application into the QuickBooks Web Connector.
{% include info.html %}
For this step, it is key to ensure that the correct company file is open in QuickBooks Desktop and that it is the only one open.
diff --git a/docs/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account.md
index b8e66c937a0a..2d33552f3e3a 100644
--- a/docs/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account.md
+++ b/docs/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account.md
@@ -8,7 +8,10 @@ Connecting a personal bank account to Expensify allows you to get reimbursed for
1. Click your profile image or icon in the bottom left menu.
2. Click **Wallet**.
3. Click **Add Bank Account**.
-4. Click **Continue** (this will open a new window and redirect you to Plaid).
+
+ ![Wallet Settings, showing where to connect a bank account](https://help.expensify.com/assets/images/addbankaccount_01.png){:width="100%"}
+
+5. Click **Continue** (this will open a new window and redirect you to Plaid).
{% include info.html %}
Plaid is an encrypted third-party financial data platform that Expensify uses to securely verify your banking information.
@@ -19,4 +22,6 @@ Plaid is an encrypted third-party financial data platform that Expensify uses to
7. Choose which account you want to connect to Expensify.
8. Click **Save & continue**.
+ ![Wallet Settings, showing bank account connected](https://help.expensify.com/assets/images/addbankaccount_03.png){:width="100%"}
+
Once connected, all payments and reimbursements will be deposited directly into that bank account.
diff --git a/docs/articles/new-expensify/workspaces/Add-approvals.md b/docs/articles/new-expensify/workspaces/Add-approvals.md
new file mode 100644
index 000000000000..5d8c1f733287
--- /dev/null
+++ b/docs/articles/new-expensify/workspaces/Add-approvals.md
@@ -0,0 +1,73 @@
+---
+title: Add approvals
+description: Add approvals to your workspace to require additional approval before authorizing payments.
+---
+
+
+# Add approvals
+
+Each Expensify workspace can be configured to require additional approvals before payments are authorized. Once approvals are enabled on a workspace, admins will be able to set a default approval workflow to apply to all members of the workspace, as well as set custom approval workflows for specific members.
+
+When workspace members submit expenses, the expenses will require approval from each approver in their approval workflow before payment is authorized.
+
+## Add approvals on a workspace
+
+**To enable Add approvals on a workspace you are an admin on:**
+
+1. Click your profile image or icon in the bottom left menu
+2. Click **Workspaces** in the left menu
+3. Select the workspace where you want to add approvals
+4. Click **Workflows** in the left menu
+5. Click the toggle next to **Add approvals**
+
+Toggling on **Add approvals** will reveal an option to set a default approval workflow.
+
+## Configure approval workflows
+
+**To configure the default approval workflow for the workspace:**
+
+1. Click your profile image or icon in the bottom left menu
+2. Click **Workspaces** in the left menu
+3. Select the workspace where you want to set the approval workflow
+4. Click **Workflows** in the left menu
+5. Under **Expenses from Everyone**, click on **First approver**
+6. Select the workspace member who should be the first approver in the approval workflow
+7. Under **Additional approver**, continue selecting workspace members until all the desired approvers are listed
+8. Click **Save**
+
+Note: When Add approvals is enabled, the workspace must have a default approval workflow.
+
+**To set an approval workflow that applies only to specific workspace members:**
+
+1. Click your profile image or icon in the bottom left menu
+2. Click **Workspaces** in the left menu
+3. Select the workspace where you want to add approvals
+4. Click **Workflows** in the left menu
+5. Under **Add approvals**, click on **Add approval workflow**
+6. Choose the workspace member whose expenses should go through the custom approval workfow
+7. Click **Next**
+8. Choose the workspace member who should be the first approver on submitted expenses in the approval workflow
+9. Click **Next**
+10. Click **Additional approver** to continue selecting workspace members until all the desired approvers are listed
+11. Click **Add workflow** to save it
+
+## Edit or delete approval workflows
+
+**To edit an approval workflow:**
+
+1. On the **Workflows** page, click the approval workflow that should be edited
+2. Click on the Approver field for the approval level where the edit should be made
+3. Choose the workspace member who should be set as the approver for that level, or deselect them to remove the approval level from the workflow
+4. Click **Save**
+
+**To delete an approval workflow:**
+
+1. On the **Workflows** page, click the approval workflow that shoudld be deleted
+2. Click **Delete**
+3. In the window that appears,click **Delete** again
+
+# FAQ
+
+## Can an employee have more than one approval workflow?
+No, each employee can have only one approval workflow
+
diff --git a/docs/articles/new-expensify/workspaces/Set-distance-rates.md b/docs/articles/new-expensify/workspaces/Set-distance-rates.md
new file mode 100644
index 000000000000..c434f34d2cef
--- /dev/null
+++ b/docs/articles/new-expensify/workspaces/Set-distance-rates.md
@@ -0,0 +1,50 @@
+---
+title: Set Distance Rates
+description: Set distance rates on your Expensify workspace
+---
+
+
+# Set Distance eates
+
+Each Expensify workspace can be configured with one or more distance rates. Once distance rates are enabled on your workspace, employees will be able to choose between the available rates to create distance expenses.
+
+## Enable distance rates on a workspace
+
+**To enable distance rates on a workspace you are an admin on:**
+
+1. Click your profile image or icon in the bottom left menu
+2. Click **Workspaces** in the left menu
+3. Select the workspace where you want to enable distance rates
+4. Click **More features** in the left menu
+5. Click the toggle next to **Distance rates**
+
+After toggling on distance rates, you will see a new **Distance rates** option in the left menu.
+
+## Add, delete, or edit distance rates
+
+**To add a distance rate:**
+
+1. Click your profile image or icon in the bottom left menu
+2. Click **Workspaces** in the left menu
+3. Select the workspace where you want to add distance rates
+4. Click **Distance rates** in the left menu
+5. Click **Add rate** in the top right
+6. Enter a value, then click **Save**
+
+**To enable, disable, edit or delete a single distance rate:**
+
+1. Click the distance rate on the **Distance rates** settings page
+2. To enable or disable the distance rate, click the toggle next to **Enable rate**, then click **Save**
+3. To edit the rate amount, click on the amount field, enter the new value, then click **Save**
+4. To permanently delete the distance rate, click **Delete**
+
+Note: When Distance rates is enabled, the workspace must have at least one enabled distance rate.
+
+**To enable, disable, edit or delete distance rates in bulk:**
+
+1. On the **Distance rates** settings page, click the checkboxes next to the distance rates that should me modified
+2. Click “x selected” at the top right
+3. To enable or disable all the selected distance rates, click **Enable rates** or **Disable rates**
+4. To permanently delete the distance rates, click **Delete rates**
+
+Note: When Distance rates are enabled, the workspace must have at least one enabled distance rate.
diff --git a/docs/assets/images/invoices_01.png b/docs/assets/images/invoices_01.png
new file mode 100644
index 000000000000..fc6d5587bb03
Binary files /dev/null and b/docs/assets/images/invoices_01.png differ
diff --git a/docs/assets/images/invoices_02.png b/docs/assets/images/invoices_02.png
new file mode 100644
index 000000000000..29038987c18a
Binary files /dev/null and b/docs/assets/images/invoices_02.png differ
diff --git a/docs/assets/images/invoices_03.png b/docs/assets/images/invoices_03.png
new file mode 100644
index 000000000000..fd78aa731784
Binary files /dev/null and b/docs/assets/images/invoices_03.png differ
diff --git a/docs/assets/images/invoices_04.png b/docs/assets/images/invoices_04.png
new file mode 100644
index 000000000000..d2e301a9d1a5
Binary files /dev/null and b/docs/assets/images/invoices_04.png differ
diff --git a/docs/assets/images/invoices_05.png b/docs/assets/images/invoices_05.png
new file mode 100644
index 000000000000..8eae5efaa9df
Binary files /dev/null and b/docs/assets/images/invoices_05.png differ
diff --git a/docs/assets/images/invoices_06.png b/docs/assets/images/invoices_06.png
new file mode 100644
index 000000000000..2858227891eb
Binary files /dev/null and b/docs/assets/images/invoices_06.png differ
diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg
index 78fb5d53d9e9..d2f181f6b7f4 100644
Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ
diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg
index ac2c76a118d5..c0afa40ecb29 100644
Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ
diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj
index 768062717d4b..1a29a275b956 100644
--- a/ios/NewExpensify.xcodeproj/project.pbxproj
+++ b/ios/NewExpensify.xcodeproj/project.pbxproj
@@ -43,7 +43,7 @@
D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; };
DD79042B2792E76D004484B4 /* RCTBootSplash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.mm */; };
DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; };
- E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
+ E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */ = {isa = PBXBuildFile; };
E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; };
ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; };
F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; };
@@ -131,6 +131,7 @@
7F3784A52C7512CF00063508 /* NewExpensifyReleaseDevelopment.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NewExpensifyReleaseDevelopment.entitlements; path = NewExpensify/NewExpensifyReleaseDevelopment.entitlements; sourceTree = "
"; };
7F3784A62C7512D900063508 /* NewExpensifyReleaseAdHoc.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NewExpensifyReleaseAdHoc.entitlements; path = NewExpensify/NewExpensifyReleaseAdHoc.entitlements; sourceTree = ""; };
7F3784A72C75131000063508 /* NewExpensifyReleaseProduction.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NewExpensifyReleaseProduction.entitlements; path = NewExpensify/NewExpensifyReleaseProduction.entitlements; sourceTree = ""; };
+ 7F9C91352CA5EC4900FC4DC1 /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = ""; };
7F9DD8D92B2A445B005E3AFA /* ExpError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpError.swift; sourceTree = ""; };
7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
@@ -175,8 +176,8 @@
buildActionMask = 2147483647;
files = (
383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */,
- E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */,
- E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */,
+ E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */,
+ E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */,
8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -266,6 +267,7 @@
7FD73C9C2B23CE9500420AF3 /* NotificationServiceExtension */ = {
isa = PBXGroup;
children = (
+ 7F9C91352CA5EC4900FC4DC1 /* NotificationServiceExtension.entitlements */,
7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */,
7FD73C9F2B23CE9500420AF3 /* Info.plist */,
7F9DD8D92B2A445B005E3AFA /* ExpError.swift */,
@@ -1183,6 +1185,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
@@ -1347,6 +1350,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
@@ -1433,6 +1437,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
@@ -1518,8 +1523,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
@@ -1560,7 +1566,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat.NotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
- "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) Development: Notification Service";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) AppStore: Notification Service";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
@@ -1604,6 +1610,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
@@ -1683,6 +1690,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
@@ -1761,6 +1769,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 7bed47c032af..af696a13c998 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.0.39
+ 9.0.40
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.39.2
+ 9.0.40.4
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 6e01c0d2f912..0795209286ed 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.39
+ 9.0.40
CFBundleSignature
????
CFBundleVersion
- 9.0.39.2
+ 9.0.40.4
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 74158e9d574a..a545bd82c164 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.0.39
+ 9.0.40
CFBundleVersion
- 9.0.39.2
+ 9.0.40.4
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/NotificationServiceExtension/NotificationServiceExtension.entitlements b/ios/NotificationServiceExtension/NotificationServiceExtension.entitlements
new file mode 100644
index 000000000000..f52d3207d6e3
--- /dev/null
+++ b/ios/NotificationServiceExtension/NotificationServiceExtension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.expensify.new
+
+
+
diff --git a/ios/Podfile b/ios/Podfile
index 2ed1752abf4f..e807089c26b9 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -121,4 +121,4 @@ target 'NotificationServiceExtension' do
pod 'AirshipServiceExtension'
end
-pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz'
\ No newline at end of file
+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 0f1a42791d1e..beac64acd083 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -143,8 +143,8 @@ PODS:
- GoogleUtilities/Environment (~> 7.7)
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- fmt (9.1.0)
- - FullStory (1.49.0)
- - fullstory_react-native (1.4.2):
+ - FullStory (1.52.0)
+ - fullstory_react-native (1.7.1):
- DoubleConversion
- FullStory (~> 1.14)
- glog
@@ -2451,7 +2451,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - RNReactNativeHapticFeedback (2.3.1):
+ - RNReactNativeHapticFeedback (2.3.3):
- DoubleConversion
- glog
- hermes-engine
@@ -2700,7 +2700,7 @@ DEPENDENCIES:
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
- - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz\"}`)"
+ - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz\"}`)"
- "fullstory_react-native (from `../node_modules/@fullstory/react-native`)"
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
@@ -2874,7 +2874,7 @@ EXTERNAL SOURCES:
fmt:
:podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec"
FullStory:
- :http: https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz
+ :http: https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz
fullstory_react-native:
:path: "../node_modules/@fullstory/react-native"
glog:
@@ -3089,7 +3089,7 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
FullStory:
- :http: https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz
+ :http: https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz
SPEC CHECKSUMS:
Airship: bb32ff2c5a811352da074480357d9f02dbb8f327
@@ -3116,8 +3116,8 @@ SPEC CHECKSUMS:
FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c
FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
- FullStory: c95f74445f871bc344cdc4a4e4ece61b5554e55d
- fullstory_react-native: 1818ee93dc38801665f26869f7ad68abb698a89a
+ FullStory: c8a10b2358c0d33c57be84d16e4c440b0434b33d
+ fullstory_react-native: 44dc2c85a6316df2713e6cb0048ce5719c3b0bab
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
@@ -3233,7 +3233,7 @@ SPEC CHECKSUMS:
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4
RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28
- RNReactNativeHapticFeedback: 31833c3ef341d716dbbd9d64e940f0c230db46f6
+ RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5
RNReanimated: 76901886830e1032f16bbf820153f7dc3f02d51d
RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2
RNShare: bd4fe9b95d1ee89a200778cc0753ebe650154bb0
@@ -3248,6 +3248,6 @@ SPEC CHECKSUMS:
VisionCamera: c6c8aa4b028501fc87644550fbc35a537d4da3fb
Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8
-PODFILE CHECKSUM: e479ec84cb53e5fd463486d71dfee91708d3fd9a
+PODFILE CHECKSUM: a07e55247056ec5d84d1af31d694506efff3cfe2
COCOAPODS: 1.15.2
diff --git a/package-lock.json b/package-lock.json
index 3c27c44a3bd7..d43c8fee25c5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.39-2",
+ "version": "9.0.40-4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.39-2",
+ "version": "9.0.40-4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -50,7 +50,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "2.0.84",
+ "expensify-common": "2.0.88",
"expo": "51.0.31",
"expo-av": "14.0.7",
"expo-image": "1.12.15",
@@ -85,7 +85,7 @@
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "2.18.0",
"react-native-google-places-autocomplete": "2.5.6",
- "react-native-haptic-feedback": "^2.3.1",
+ "react-native-haptic-feedback": "^2.3.3",
"react-native-image-picker": "^7.0.3",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9",
"react-native-key-command": "^1.0.8",
@@ -24037,9 +24037,9 @@
}
},
"node_modules/expensify-common": {
- "version": "2.0.84",
- "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.84.tgz",
- "integrity": "sha512-VistjMexRz/1u1IqjIZwGRE7aS6QOat7420Dualn+NaqMHGkfeeB4uUR3RQhCtlDbcwFBKTryIGgSrrC0N1YpA==",
+ "version": "2.0.88",
+ "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.88.tgz",
+ "integrity": "sha512-4k6X6BekydYSRWkWRMB/Ts0W5Zx3BskEpLQEuxpq+cW9QIvTyFliho/dMLaXYOqS6nMQuzkjJYqfGPx9agVnOg==",
"dependencies": {
"awesome-phonenumber": "^5.4.0",
"classnames": "2.5.0",
@@ -34543,9 +34543,9 @@
}
},
"node_modules/react-native-haptic-feedback": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.1.tgz",
- "integrity": "sha512-dPfjV4iVHfhVyfG+nRd88ygjahbdup7KFZDM5L2aNIAzqbNtKxHZn5O1pHegwSj1t15VJliu0GyTX7XpBDeXUw==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.3.tgz",
+ "integrity": "sha512-svS4D5PxfNv8o68m9ahWfwje5NqukM3qLS48+WTdhbDkNUkOhP9rDfDSRHzlhk4zq+ISjyw95EhLeh8NkKX5vQ==",
"workspaces": [
"example"
],
diff --git a/package.json b/package.json
index 6f4980f04ee0..e8322960c61c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.39-2",
+ "version": "9.0.40-4",
"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.",
@@ -107,7 +107,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "2.0.84",
+ "expensify-common": "2.0.88",
"expo": "51.0.31",
"expo-av": "14.0.7",
"expo-image": "1.12.15",
@@ -142,7 +142,7 @@
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "2.18.0",
"react-native-google-places-autocomplete": "2.5.6",
- "react-native-haptic-feedback": "^2.3.1",
+ "react-native-haptic-feedback": "^2.3.3",
"react-native-image-picker": "^7.0.3",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9",
"react-native-key-command": "^1.0.8",
diff --git a/patches/react-native-haptic-feedback+2.3.1.patch b/patches/react-native-haptic-feedback+2.3.1.patch
deleted file mode 100644
index 799bdaf7e53e..000000000000
--- a/patches/react-native-haptic-feedback+2.3.1.patch
+++ /dev/null
@@ -1,56 +0,0 @@
-diff --git a/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedback.h b/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedback.h
-index c1498b9..250df1f 100644
---- a/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedback.h
-+++ b/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedback.h
-@@ -1,5 +1,5 @@
- #ifdef RCT_NEW_ARCH_ENABLED
--#import "RNHapticFeedbackSpec.h"
-+#import
-
- @interface RNHapticFeedback : NSObject
- #else
-diff --git a/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedbackSpec.h b/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedbackSpec.h
-deleted file mode 100644
-index 6f0f81d..0000000
---- a/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedbackSpec.h
-+++ /dev/null
-@@ -1,15 +0,0 @@
--//
--// RNHapticFeedbackSpec.h
--// RNHapticFeedback
--//
--// Created by Michael Kuczera on 05.08.24.
--// Copyright © 2024 Facebook. All rights reserved.
--//
--#import
--
--@protocol NativeHapticFeedbackSpec
--
--// Indicates whether the device supports haptic feedback
--- (Boolean)supportsHaptic;
--
--@end
-diff --git a/node_modules/react-native-haptic-feedback/package.json b/node_modules/react-native-haptic-feedback/package.json
-index 86dfaa4..9cec8e4 100644
---- a/node_modules/react-native-haptic-feedback/package.json
-+++ b/node_modules/react-native-haptic-feedback/package.json
-@@ -6,18 +6,7 @@
- "source": "src/index.ts",
- "main": "./lib/commonjs/index.js",
- "module": "./lib/module/index.js",
-- "exports": {
-- ".": {
-- "import": {
-- "types": "./lib/typescript/module/src/index.d.ts",
-- "default": "./lib/module/index.js"
-- },
-- "require": {
-- "types": "./lib/typescript/commonjs/src/index.d.ts",
-- "default": "./lib/commonjs/index.js"
-- }
-- }
-- },
-+ "types": "./lib/typescript/module/src/index.d.ts",
- "scripts": {
- "typecheck": "tsc --noEmit --project tsconfig.test.json",
- "test": "jest",
diff --git a/src/CONST.ts b/src/CONST.ts
index 9ee9ec4d9147..4ca9b45f13df 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -171,7 +171,7 @@ const CONST = {
},
// Note: Group and Self-DM excluded as these are not tied to a Workspace
- WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT],
+ WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT, chatTypes.INVOICE],
ANDROID_PACKAGE_NAME,
WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100,
ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50,
@@ -719,7 +719,9 @@ const CONST = {
PRICING: `https://www.expensify.com/pricing`,
COMPANY_CARDS_HELP: 'https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds',
CUSTOM_REPORT_NAME_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates',
+ CONFIGURE_REIMBURSEMENT_SETTINGS_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings',
COPILOT_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot',
+ 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:',
OLDDOT_URLS: {
@@ -1009,6 +1011,7 @@ const CONST = {
MAX_PREVIEW_AVATARS: 4,
MAX_ROOM_NAME_LENGTH: 99,
LAST_MESSAGE_TEXT_MAX_LENGTH: 200,
+ MIN_LENGTH_LAST_MESSAGE_WITH_ELLIPSIS: 20,
OWNER_EMAIL_FAKE: '__FAKE__',
OWNER_ACCOUNT_ID_FAKE: 0,
DEFAULT_REPORT_NAME: 'Chat Report',
@@ -2097,6 +2100,9 @@ const CONST = {
ACCESS_VARIANTS: {
CREATE: 'create',
},
+ PAGE_INDEX: {
+ CONFIRM: 'confirm',
+ },
PAYMENT_SELECTED: {
BBA: 'BBA',
PBA: 'PBA',
@@ -2180,7 +2186,7 @@ const CONST = {
AUTO_REIMBURSEMENT_MAX_LIMIT_CENTS: 2000000,
AUTO_REIMBURSEMENT_DEFAULT_LIMIT_CENTS: 10000,
AUTO_APPROVE_REPORTS_UNDER_DEFAULT_CENTS: 10000,
- RANDOM_AUDIT_DEFAULT_PERCENTAGE: 5,
+ RANDOM_AUDIT_DEFAULT_PERCENTAGE: 0.05,
AUTO_REPORTING_FREQUENCIES: {
INSTANT: 'instant',
@@ -4556,7 +4562,7 @@ const CONST = {
{
type: 'setupTags',
autoCompleted: false,
- title: 'Set up tags (optional)',
+ title: 'Set up tags',
description: ({workspaceMoreFeaturesLink}) =>
'Tags can be used if you want more details with every expense. Use tags for projects, clients, locations, departments, and more. If you need multiple levels of tags you can upgrade to a control plan.\n' +
'\n' +
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 7fcb675dc191..cb8bf2fdb5d3 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -849,7 +849,7 @@ type OnyxValuesMapping = {
// ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data
[ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot;
- [ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch[];
+ [ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch;
[ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[];
[ONYXKEYS.ACTIVE_CLIENTS]: string[];
[ONYXKEYS.DEVICE_ID]: string;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 3d24e3a2dafc..dfcb42d3c4fe 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -35,7 +35,7 @@ const ROUTES = {
SEARCH_CENTRAL_PANE: {
route: 'search',
- getRoute: ({query}: {query: SearchQueryString}) => `search?q=${encodeURIComponent(query)}` as const,
+ getRoute: ({query, name}: {query: SearchQueryString; name?: string}) => `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const,
},
SEARCH_SAVED_SEARCH_RENAME: {
route: 'search/saved-search/rename',
@@ -72,7 +72,7 @@ const ROUTES = {
SUBMIT_EXPENSE: 'submit-expense',
FLAG_COMMENT: {
route: 'flag/:reportID/:reportActionID',
- getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` as const,
+ getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`flag/${reportID}/${reportActionID}` as const, backTo),
},
CHAT_FINDER: 'chat-finder',
PROFILE: {
@@ -285,11 +285,12 @@ const ROUTES = {
},
EDIT_REPORT_FIELD_REQUEST: {
route: 'r/:reportID/edit/policyField/:policyID/:fieldID',
- getRoute: (reportID: string, policyID: string, fieldID: string) => `r/${reportID}/edit/policyField/${policyID}/${fieldID}` as const,
+ getRoute: (reportID: string, policyID: string, fieldID: string, backTo?: string) =>
+ getUrlWithBackToParam(`r/${reportID}/edit/policyField/${policyID}/${encodeURIComponent(fieldID)}` as const, backTo),
},
REPORT_WITH_ID_DETAILS_SHARE_CODE: {
route: 'r/:reportID/details/shareCode',
- getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details/shareCode` as const, backTo),
},
ATTACHMENTS: {
route: 'attachment',
@@ -298,19 +299,19 @@ const ROUTES = {
},
REPORT_PARTICIPANTS: {
route: 'r/:reportID/participants',
- getRoute: (reportID: string) => `r/${reportID}/participants` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/participants` as const, backTo),
},
REPORT_PARTICIPANTS_INVITE: {
route: 'r/:reportID/participants/invite',
- getRoute: (reportID: string) => `r/${reportID}/participants/invite` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/participants/invite` as const, backTo),
},
REPORT_PARTICIPANTS_DETAILS: {
route: 'r/:reportID/participants/:accountID',
- getRoute: (reportID: string, accountID: number) => `r/${reportID}/participants/${accountID}` as const,
+ getRoute: (reportID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/participants/${accountID}` as const, backTo),
},
REPORT_PARTICIPANTS_ROLE_SELECTION: {
route: 'r/:reportID/participants/:accountID/role',
- getRoute: (reportID: string, accountID: number) => `r/${reportID}/participants/${accountID}/role` as const,
+ getRoute: (reportID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/participants/${accountID}/role` as const, backTo),
},
REPORT_WITH_ID_DETAILS: {
route: 'r/:reportID/details',
@@ -318,71 +319,75 @@ const ROUTES = {
},
REPORT_WITH_ID_DETAILS_EXPORT: {
route: 'r/:reportID/details/export/:connectionName',
- getRoute: (reportID: string, connectionName: ConnectionName) => `r/${reportID}/details/export/${connectionName}` as const,
+ getRoute: (reportID: string, connectionName: ConnectionName, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details/export/${connectionName}` as const, backTo),
},
REPORT_SETTINGS: {
route: 'r/:reportID/settings',
- getRoute: (reportID: string) => `r/${reportID}/settings` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings` as const, backTo),
},
REPORT_SETTINGS_NAME: {
route: 'r/:reportID/settings/name',
- getRoute: (reportID: string) => `r/${reportID}/settings/name` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/name` as const, backTo),
},
REPORT_SETTINGS_NOTIFICATION_PREFERENCES: {
route: 'r/:reportID/settings/notification-preferences',
- getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/notification-preferences` as const, backTo),
},
REPORT_SETTINGS_WRITE_CAPABILITY: {
route: 'r/:reportID/settings/who-can-post',
- getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/who-can-post` as const, backTo),
},
REPORT_SETTINGS_VISIBILITY: {
route: 'r/:reportID/settings/visibility',
- getRoute: (reportID: string) => `r/${reportID}/settings/visibility` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/visibility` as const, backTo),
},
SPLIT_BILL_DETAILS: {
route: 'r/:reportID/split/:reportActionID',
- getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const,
+ getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/split/${reportActionID}` as const, backTo),
},
TASK_TITLE: {
route: 'r/:reportID/title',
- getRoute: (reportID: string) => `r/${reportID}/title` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/title` as const, backTo),
},
REPORT_DESCRIPTION: {
route: 'r/:reportID/description',
- getRoute: (reportID: string) => `r/${reportID}/description` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/description` as const, backTo),
},
TASK_ASSIGNEE: {
route: 'r/:reportID/assignee',
- getRoute: (reportID: string) => `r/${reportID}/assignee` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/assignee` as const, backTo),
},
PRIVATE_NOTES_LIST: {
route: 'r/:reportID/notes',
- getRoute: (reportID: string) => `r/${reportID}/notes` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/notes` as const, backTo),
},
PRIVATE_NOTES_EDIT: {
route: 'r/:reportID/notes/:accountID/edit',
- getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit` as const,
+ getRoute: (reportID: string, accountID: string | number, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/notes/${accountID}/edit` as const, backTo),
},
ROOM_MEMBERS: {
route: 'r/:reportID/members',
- getRoute: (reportID: string) => `r/${reportID}/members` as const,
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/members` as const, backTo),
},
ROOM_MEMBER_DETAILS: {
route: 'r/:reportID/members/:accountID',
- getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/members/${accountID}` as const,
+ getRoute: (reportID: string, accountID: string | number, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/members/${accountID}` as const, backTo),
},
ROOM_INVITE: {
route: 'r/:reportID/invite/:role?',
- getRoute: (reportID: string, role?: string) => {
+ getRoute: (reportID: string, role?: string, backTo?: string) => {
const route = role ? (`r/${reportID}/invite/${role}` as const) : (`r/${reportID}/invite` as const);
- return route;
+ return getUrlWithBackToParam(route, backTo);
},
},
MONEY_REQUEST_HOLD_REASON: {
- route: ':type/edit/reason/:transactionID?',
- getRoute: (type: ValueOf, transactionID: string, reportID: string, backTo: string) =>
- `${type}/edit/reason/${transactionID}?backTo=${backTo}&reportID=${reportID}` as const,
+ route: ':type/edit/reason/:transactionID?/:searchHash?',
+ getRoute: (type: ValueOf, transactionID: string, reportID: string, backTo: string, searchHash?: number) => {
+ const route = searchHash
+ ? (`${type}/edit/reason/${transactionID}/${searchHash}/?backTo=${backTo}&reportID=${reportID}` as const)
+ : (`${type}/edit/reason/${transactionID}/?backTo=${backTo}&reportID=${reportID}` as const);
+ return route;
+ },
},
MONEY_REQUEST_CREATE: {
route: ':action/:iouType/start/:transactionID/:reportID',
@@ -404,9 +409,9 @@ const ROUTES = {
`${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}${participantsAutoAssigned ? '?participantsAutoAssigned=true' : ''}` as const,
},
MONEY_REQUEST_STEP_AMOUNT: {
- route: ':action/:iouType/amount/:transactionID/:reportID',
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action as string}/${iouType as string}/amount/${transactionID}/${reportID}`, backTo),
+ route: ':action/:iouType/amount/:transactionID/:reportID/:pageIndex?',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/amount/${transactionID}/${reportID}/${pageIndex}`, backTo),
},
MONEY_REQUEST_STEP_TAX_RATE: {
route: ':action/:iouType/taxRate/:transactionID/:reportID?',
@@ -493,6 +498,10 @@ 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 = '') =>
@@ -534,12 +543,27 @@ const ROUTES = {
IOU_SEND_ADD_DEBIT_CARD: 'pay/new/add-debit-card',
IOU_SEND_ENABLE_PAYMENTS: 'pay/new/enable-payments',
- NEW_TASK: 'new/task',
- NEW_TASK_ASSIGNEE: 'new/task/assignee',
+ NEW_TASK: {
+ route: 'new/task',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('new/task', backTo),
+ },
+ NEW_TASK_ASSIGNEE: {
+ route: 'new/task/assignee',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('new/task/assignee', backTo),
+ },
NEW_TASK_SHARE_DESTINATION: 'new/task/share-destination',
- NEW_TASK_DETAILS: 'new/task/details',
- NEW_TASK_TITLE: 'new/task/title',
- NEW_TASK_DESCRIPTION: 'new/task/description',
+ NEW_TASK_DETAILS: {
+ route: 'new/task/details',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('new/task/details', backTo),
+ },
+ NEW_TASK_TITLE: {
+ route: 'new/task/title',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('new/task/title', backTo),
+ },
+ NEW_TASK_DESCRIPTION: {
+ route: 'new/task/description',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('new/task/description', backTo),
+ },
TEACHERS_UNITE: 'settings/teachersunite',
I_KNOW_A_TEACHER: 'settings/teachersunite/i-know-a-teacher',
@@ -1095,7 +1119,10 @@ const ROUTES = {
route: 'referral/:contentType',
getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo),
},
- PROCESS_MONEY_REQUEST_HOLD: 'hold-expense-educational',
+ PROCESS_MONEY_REQUEST_HOLD: {
+ route: 'hold-expense-educational',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('hold-expense-educational', backTo),
+ },
TRAVEL_MY_TRIPS: 'travel',
TRAVEL_TCS: 'travel/terms',
TRACK_TRAINING_MODAL: 'track-training',
@@ -1124,39 +1151,39 @@ const ROUTES = {
},
TRANSACTION_DUPLICATE_REVIEW_PAGE: {
route: 'r/:threadReportID/duplicates/review',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review` as const, backTo),
},
TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: {
route: 'r/:threadReportID/duplicates/review/merchant',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/merchant` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/merchant` as const, backTo),
},
TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: {
route: 'r/:threadReportID/duplicates/review/category',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/category` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/category` as const, backTo),
},
TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: {
route: 'r/:threadReportID/duplicates/review/tag',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tag` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/tag` as const, backTo),
},
TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE: {
route: 'r/:threadReportID/duplicates/review/tax-code',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tax-code` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/tax-code` as const, backTo),
},
TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: {
route: 'r/:threadReportID/duplicates/review/description',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/description` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/description` as const, backTo),
},
TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE: {
route: 'r/:threadReportID/duplicates/review/reimbursable',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/reimbursable` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/reimbursable` as const, backTo),
},
TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE: {
route: 'r/:threadReportID/duplicates/review/billable',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/billable` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/billable` as const, backTo),
},
TRANSACTION_DUPLICATE_CONFIRMATION_PAGE: {
route: 'r/:threadReportID/duplicates/confirm',
- getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const,
+ getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/confirm` as const, backTo),
},
POLICY_ACCOUNTING_XERO_IMPORT: {
route: 'settings/workspaces/:policyID/accounting/xero/import',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 920bd48dd42e..395f1c4d5fb1 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -244,6 +244,8 @@ const SCREENS = {
SETTINGS_CATEGORIES_ROOT: 'Settings_Categories',
},
+ SETTINGS_TAGS_ROOT: 'Settings_Tags',
+
REPORT_SETTINGS: {
ROOT: 'Report_Settings_Root',
NAME: 'Report_Settings_Name',
diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx
index 71970b88eac9..8d3e311c7c61 100644
--- a/src/components/AccountSwitcher.tsx
+++ b/src/components/AccountSwitcher.tsx
@@ -108,7 +108,7 @@ function AccountSwitcher() {
const error = ErrorUtils.getLatestErrorField({errorFields}, 'connect');
const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email);
return createBaseMenuItem(personalDetails, error, {
- badgeText: translate('delegate.role', role),
+ badgeText: translate('delegate.role', {role}),
onPress: () => {
if (isOffline) {
Modal.close(() => setShouldShowOfflineModal(true));
diff --git a/src/components/AccountSwitcherSkeletonView/index.tsx b/src/components/AccountSwitcherSkeletonView/index.tsx
index 3faf7e563f3c..379a4094e032 100644
--- a/src/components/AccountSwitcherSkeletonView/index.tsx
+++ b/src/components/AccountSwitcherSkeletonView/index.tsx
@@ -22,7 +22,7 @@ function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.A
const StyleUtils = useStyleUtils();
const avatarPlaceholderSize = StyleUtils.getAvatarSize(avatarSize);
const avatarPlaceholderRadius = avatarPlaceholderSize / 2;
- const startPositionX = 30;
+ const startPositionX = avatarPlaceholderRadius;
return (
diff --git a/src/components/AccountingConnectionConfirmationModal.tsx b/src/components/AccountingConnectionConfirmationModal.tsx
index c472f215b6df..bfacd8c0bf76 100644
--- a/src/components/AccountingConnectionConfirmationModal.tsx
+++ b/src/components/AccountingConnectionConfirmationModal.tsx
@@ -14,11 +14,11 @@ function AccountingConnectionConfirmationModal({integrationToConnect, onCancel,
return (
{
+ ReportUtils.navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute());
+ }, [report]);
+
const showActorDetails = useCallback(() => {
// We should navigate to the details page if the report is a IOU/expense report
if (shouldEnableDetailPageNavigation) {
- return ReportUtils.navigateToDetailsPage(report);
+ goToDetailsPage();
+ return;
}
if (ReportUtils.isExpenseReport(report) && report?.ownerAccountID) {
@@ -107,7 +112,7 @@ function AvatarWithDisplayName({
// Report detail route is added as fallback but based on the current implementation this route won't be executed
Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
}
- }, [report, shouldEnableDetailPageNavigation]);
+ }, [report, shouldEnableDetailPageNavigation, goToDetailsPage]);
const headerView = (
@@ -172,7 +177,7 @@ function AvatarWithDisplayName({
return (
ReportUtils.navigateToDetailsPage(report)}
+ onPress={goToDetailsPage}
style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]}
accessibilityLabel={title}
role={CONST.ROLE.BUTTON}
diff --git a/src/components/Button/validateSubmitShortcut/index.ts b/src/components/Button/validateSubmitShortcut/index.ts
index f8cea44f73d6..29ba071c25f2 100644
--- a/src/components/Button/validateSubmitShortcut/index.ts
+++ b/src/components/Button/validateSubmitShortcut/index.ts
@@ -11,7 +11,7 @@ import type ValidateSubmitShortcut from './types';
const validateSubmitShortcut: ValidateSubmitShortcut = (isDisabled, isLoading, event) => {
const eventTarget = event?.target as HTMLElement;
- if (isDisabled || isLoading || eventTarget.nodeName === 'TEXTAREA') {
+ if (isDisabled || isLoading || eventTarget.nodeName === 'TEXTAREA' || (eventTarget?.contentEditable === 'true' && eventTarget.ariaMultiLine)) {
return false;
}
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
index 876f1a745403..8eb991b17b63 100644
--- a/src/components/EmptyStateComponent/index.tsx
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -22,6 +22,7 @@ function EmptyStateComponent({
buttonAction,
containerStyles,
title,
+ titleStyles,
subtitle,
headerStyles,
headerContentStyles,
@@ -30,7 +31,7 @@ function EmptyStateComponent({
}: EmptyStateComponentProps) {
const styles = useThemeStyles();
const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO);
- const {isSmallScreenWidth} = useResponsiveLayout();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const setAspectRatio = (event: VideoReadyForDisplayEvent | VideoLoadedEventType | undefined) => {
if (!event) {
@@ -82,7 +83,10 @@ function EmptyStateComponent({
}, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo, lottieWebViewStyles]);
return (
-
+
{HeaderComponent}
-
- {title}
- {subtitle}
+
+ {title}
+ {typeof subtitle === 'string' ? {subtitle} : subtitle}
{!!buttonText && !!buttonAction && (
;
type SharedProps = {
SkeletonComponent: ValidSkeletons;
title: string;
+ titleStyles?: StyleProp;
subtitle: string | React.ReactNode;
buttonText?: string;
buttonAction?: () => void;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
index 6acef20cd833..c66b8ae13ee9 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
@@ -61,7 +61,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona
accountID = parseInt(htmlAttribAccountID, 10);
mentionDisplayText = LocalePhoneNumber.formatPhoneNumber(user?.login ?? '') || PersonalDetailsUtils.getDisplayNameOrDefault(user);
mentionDisplayText = getShortMentionIfFound(mentionDisplayText, htmlAttributeAccountID, user?.login ?? '');
- navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID);
+ navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID, Navigation.getReportRHPActiveRoute());
} else if ('data' in tnodeClone && !isEmptyObject(tnodeClone.data)) {
// We need to remove the LTR unicode and leading @ from data as it is not part of the login
mentionDisplayText = tnodeClone.data.replace(CONST.UNICODE.LTR, '').slice(1);
@@ -69,7 +69,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona
asMutable(tnodeClone).data = tnodeClone.data.replace(mentionDisplayText, Str.removeSMSDomain(getShortMentionIfFound(mentionDisplayText, htmlAttributeAccountID)));
accountID = PersonalDetailsUtils.getAccountIDsByLogins([mentionDisplayText])?.[0];
- navigationRoute = ROUTES.PROFILE.getRoute(accountID, undefined, mentionDisplayText);
+ navigationRoute = ROUTES.PROFILE.getRoute(accountID, Navigation.getReportRHPActiveRoute(), mentionDisplayText);
mentionDisplayText = Str.removeSMSDomain(mentionDisplayText);
} else {
// If neither an account ID or email is provided, don't render anything
@@ -94,7 +94,11 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona
}}
onPress={(event) => {
event.preventDefault();
- Navigation.navigate(navigationRoute);
+ if (!isEmpty(htmlAttribAccountID)) {
+ Navigation.navigate(ROUTES.PROFILE.getRoute(htmlAttribAccountID, Navigation.getReportRHPActiveRoute()));
+ return;
+ }
+ Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getReportRHPActiveRoute(), mentionDisplayText));
}}
role={CONST.ROLE.LINK}
accessibilityLabel={`/${navigationRoute}`}
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index 22885b6ceac5..ef480a3a9275 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -3,7 +3,7 @@ import type {StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type {Action} from '@hooks/useSingleExecution';
-import type {StepCounterParams} from '@src/languages/types';
+import type {StepCounterParams} from '@src/languages/params';
import type {AnchorPosition} from '@src/styles';
import type {Policy, Report} from '@src/types/onyx';
import type {Icon} from '@src/types/onyx/OnyxCommon';
diff --git a/src/components/ImportColumn.tsx b/src/components/ImportColumn.tsx
index 49601787a207..37651e58bb79 100644
--- a/src/components/ImportColumn.tsx
+++ b/src/components/ImportColumn.tsx
@@ -172,7 +172,7 @@ function ImportColumn({column, columnName, columnRoles, columnIndex}: ImportColu
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run again
}, []);
- const columnHeader = containsHeader ? column[0] : translate('spreadsheet.column', columnName);
+ const columnHeader = containsHeader ? column[0] : translate('spreadsheet.column', {name: columnName});
return (
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 353bacdc0a25..322f28aa246d 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -187,7 +187,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
shiftHorizontal={variables.gbrTooltipShiftHorizontal}
shiftVertical={variables.composerTooltipShiftVertical}
wrapperStyle={styles.quickActionTooltipWrapper}
- onPressOverlay={() => User.dismissGBRTooltip()}
+ onHideTooltip={() => User.dismissGBRTooltip()}
>
diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx
index 383784a468d7..b677cf1e66e2 100644
--- a/src/components/LocaleContextProvider.tsx
+++ b/src/components/LocaleContextProvider.tsx
@@ -8,7 +8,7 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import * as Localize from '@libs/Localize';
import * as NumberFormatUtils from '@libs/NumberFormatUtils';
import CONST from '@src/CONST';
-import type {TranslationPaths} from '@src/languages/types';
+import type {TranslationParameters, TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails';
@@ -28,7 +28,7 @@ type LocaleContextProviderProps = LocaleContextProviderOnyxProps &
type LocaleContextProps = {
/** Returns translated string for given locale and phrase */
- translate: (phraseKey: TKey, ...phraseParameters: Localize.PhraseParameters>) => string;
+ translate: (path: TPath, ...parameters: TranslationParameters) => string;
/** Formats number formatted according to locale and options */
numberFormat: (number: number, options?: Intl.NumberFormatOptions) => string;
@@ -79,8 +79,8 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails, chi
const translate = useMemo(
() =>
- (phraseKey, ...phraseParameters) =>
- Localize.translate(locale, phraseKey, ...phraseParameters),
+ (path, ...parameters) =>
+ Localize.translate(locale, path, ...parameters),
[locale],
);
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 2524658d6ffc..8dbff4287816 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -330,6 +330,9 @@ type MenuItemBaseProps = {
/** Should selected item be marked with checkmark */
shouldShowSelectedItemCheck?: boolean;
+
+ /** Handles what to do when hiding the tooltip */
+ onHideTooltip?: () => void;
};
type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps;
@@ -416,7 +419,7 @@ function MenuItem(
titleWithTooltips,
displayInDefaultIconColor = false,
contentFit = 'cover',
- isPaneMenu = false,
+ isPaneMenu = true,
shouldPutLeftPaddingWhenNoIcon = false,
onFocus,
onBlur,
@@ -428,6 +431,7 @@ function MenuItem(
tooltipShiftVertical = 0,
renderTooltipContent,
shouldShowSelectedItemCheck = false,
+ onHideTooltip,
}: MenuItemProps,
ref: PressableRef,
) {
@@ -559,6 +563,7 @@ function MenuItem(
shiftHorizontal={tooltipShiftHorizontal}
shiftVertical={tooltipShiftVertical}
shouldAutoDismiss
+ onHideTooltip={onHideTooltip}
>
diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx
index d33a17f90a5e..b2d79b6243ac 100644
--- a/src/components/MenuItemList.tsx
+++ b/src/components/MenuItemList.tsx
@@ -49,20 +49,9 @@ type MenuItemListProps = {
/** Icon Height */
iconHeight?: number;
-
- /** Is this in the Pane */
- isPaneMenu?: boolean;
};
-function MenuItemList({
- menuItems = [],
- shouldUseSingleExecution = false,
- wrapperStyle = {},
- icon = undefined,
- iconWidth = undefined,
- iconHeight = undefined,
- isPaneMenu = false,
-}: MenuItemListProps) {
+function MenuItemList({menuItems = [], shouldUseSingleExecution = false, wrapperStyle = {}, icon = undefined, iconWidth = undefined, iconHeight = undefined}: MenuItemListProps) {
const popoverAnchor = useRef(null);
const {isExecuting, singleExecution} = useSingleExecution();
@@ -99,7 +88,6 @@ function MenuItemList({
icon={icon}
iconWidth={iconWidth}
iconHeight={iconHeight}
- isPaneMenu={isPaneMenu}
// eslint-disable-next-line react/jsx-props-no-spreading
{...menuItemProps}
disabled={!!menuItemProps.disabled || isExecuting}
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index f5e2703b1a47..b1340531c7f2 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -4,6 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CurrencyUtils from '@libs/CurrencyUtils';
@@ -41,7 +42,7 @@ import SettlementButton from './SettlementButton';
type MoneyReportHeaderProps = {
/** The report currently being looked at */
- report: OnyxTypes.Report;
+ report: OnyxEntry;
/** The policy tied to the expense report */
policy: OnyxEntry;
@@ -53,16 +54,14 @@ type MoneyReportHeaderProps = {
// eslint-disable-next-line react/no-unused-prop-types
transactionThreadReportID?: string | null;
- /** Whether we should display the header as in narrow layout */
- shouldUseNarrowLayout?: boolean;
-
/** Method to trigger when pressing close button of the header */
onBackButtonPress: () => void;
};
-function MoneyReportHeader({policy, report: moneyRequestReport, transactionThreadReportID, reportActions, shouldUseNarrowLayout = false, onBackButtonPress}: MoneyReportHeaderProps) {
- const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport.chatReportID}`);
- const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport.reportID}`);
+function MoneyReportHeader({policy, report: moneyRequestReport, transactionThreadReportID, reportActions, onBackButtonPress}: MoneyReportHeaderProps) {
+ const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID ?? '-1'}`);
+ const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID ?? '-1'}`);
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`);
const [session] = useOnyx(ONYXKEYS.SESSION);
const requestParentReportAction = useMemo(() => {
@@ -109,10 +108,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t));
const transactionIDs = allTransactions.map((t) => t.transactionID);
const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']);
- const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID);
+ const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport?.reportID ?? '');
const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction);
const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID);
- const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport.reportID}`, {selector: ReportUtils.getArchiveReason});
+ const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID ?? '-1'}`, {selector: ReportUtils.getArchiveReason});
const shouldShowPayButton = useMemo(
() => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined),
@@ -123,7 +122,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);
- const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations;
+ const shouldShowSubmitButton = !!moneyRequestReport && isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations;
const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(moneyRequestReport);
@@ -137,9 +136,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldShowAnyButton =
shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || hasAllPendingRTERViolations || shouldShowExportIntegrationButton;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
- const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency);
+ const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency);
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy);
- const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID);
+ const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID);
const displayedAmount = isAnyTransactionOnHold && canAllowSettlement ? nonHeldAmount : formattedAmount;
const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout);
const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
@@ -255,14 +254,14 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
return;
}
- if (shouldUseNarrowLayout) {
- if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD) {
+ if (isSmallScreenWidth) {
+ if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD.route) {
Navigation.goBack();
}
} else {
- Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD);
+ Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute()));
}
- }, [shouldUseNarrowLayout, shouldShowHoldMenu]);
+ }, [isSmallScreenWidth, shouldShowHoldMenu]);
const handleHoldRequestClose = () => {
IOU.dismissHoldUseExplanation();
@@ -293,9 +292,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
{shouldShowSettlementButton && !shouldUseNarrowLayout && (
{shouldShowSettlementButton && shouldUseNarrowLayout && (
- {shouldUseNarrowLayout && shouldShowHoldMenu && (
+ {isSmallScreenWidth && shouldShowHoldMenu && (
{
- const activeRoute = Navigation.getActiveRouteWithoutParams();
+ const activeRoute = Navigation.getActiveRoute();
if (option.isSelfDM) {
Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute));
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index 62e4f1e8b589..e6b957f16b60 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -299,7 +299,7 @@ function MoneyRequestConfirmationListFooter({
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transactionID, reportID, CONST.IOU.PAGE_INDEX.CONFIRM, Navigation.getActiveRoute()));
}}
style={[styles.moneyRequestMenuItem, styles.mt2]}
titleStyle={styles.moneyRequestConfirmationAmount}
@@ -326,9 +326,7 @@ function MoneyRequestConfirmationListFooter({
title={iouComment}
description={translate('common.description')}
onPress={() => {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID),
- );
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
}}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
@@ -404,7 +402,7 @@ function MoneyRequestConfirmationListFooter({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
disabled={didConfirm}
interactive={!isReadOnly}
@@ -428,7 +426,7 @@ function MoneyRequestConfirmationListFooter({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
}}
disabled={didConfirm}
interactive={!isReadOnly}
@@ -447,9 +445,7 @@ function MoneyRequestConfirmationListFooter({
title={iouCategory}
description={translate('common.category')}
numberOfLinesTitle={2}
- onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID))
- }
+ onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID))}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
disabled={didConfirm}
@@ -472,9 +468,7 @@ function MoneyRequestConfirmationListFooter({
description={name}
numberOfLinesTitle={2}
onPress={() =>
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID),
- )
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRoute(), reportActionID))
}
style={[styles.moneyRequestMenuItem]}
disabled={didConfirm}
@@ -495,7 +489,7 @@ function MoneyRequestConfirmationListFooter({
description={taxRates?.name}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()))}
disabled={didConfirm}
interactive={canModifyTaxFields}
/>
@@ -512,7 +506,7 @@ function MoneyRequestConfirmationListFooter({
description={translate('iou.taxAmount')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()))}
disabled={didConfirm}
interactive={canModifyTaxFields}
/>
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index cfe0397bbd11..d2db257dc776 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -4,6 +4,7 @@ import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
@@ -27,7 +28,7 @@ import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu';
type MoneyRequestHeaderProps = {
/** The report currently being looked at */
- report: Report;
+ report: OnyxEntry;
/** The policy which the report is tied to */
policy: OnyxEntry;
@@ -35,15 +36,13 @@ type MoneyRequestHeaderProps = {
/** The report action the transaction is tied to from the parent report */
parentReportAction: OnyxEntry;
- /** Whether we should display the header as in narrow layout */
- shouldUseNarrowLayout?: boolean;
-
/** Method to trigger when pressing close button of the header */
onBackButtonPress: () => void;
};
-function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrowLayout = false, onBackButtonPress}: MoneyRequestHeaderProps) {
- const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`);
+function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPress}: MoneyRequestHeaderProps) {
+ const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
+ const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`);
const [transaction] = useOnyx(
`${ONYXKEYS.COLLECTION.TRANSACTION}${
ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? -1 : -1
@@ -58,12 +57,13 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false);
const isOnHold = TransactionUtils.isOnHold(transaction);
const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? '');
+ const reportID = report?.reportID;
const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']);
const markAsCash = useCallback(() => {
- TransactionActions.markAsCash(transaction?.transactionID ?? '-1', report.reportID);
- }, [report.reportID, transaction?.transactionID]);
+ TransactionActions.markAsCash(transaction?.transactionID ?? '-1', reportID ?? '');
+ }, [reportID, transaction?.transactionID]);
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
@@ -106,14 +106,14 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
return;
}
- if (shouldUseNarrowLayout) {
- if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD) {
+ if (isSmallScreenWidth) {
+ if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD.route) {
Navigation.goBack();
}
} else {
- Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD);
+ Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute()));
}
- }, [shouldUseNarrowLayout, shouldShowHoldMenu]);
+ }, [isSmallScreenWidth, shouldShowHoldMenu]);
const handleHoldRequestClose = () => {
IOU.dismissHoldUseExplanation();
@@ -129,6 +129,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
shouldShowPinButton={false}
report={{
...report,
+ reportID: reportID ?? '',
ownerAccountID: parentReport?.ownerAccountID,
}}
policy={policy}
@@ -150,7 +151,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
text={translate('iou.reviewDuplicates')}
style={[styles.p0, styles.ml2]}
onPress={() => {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID ?? '', Navigation.getReportRHPActiveRoute()));
}}
/>
)}
@@ -172,7 +173,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
text={translate('iou.reviewDuplicates')}
style={[styles.w100, styles.pr0]}
onPress={() => {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID ?? '', Navigation.getReportRHPActiveRoute()));
}}
/>
@@ -186,7 +187,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
)}
- {shouldUseNarrowLayout && shouldShowHoldMenu && (
+ {isSmallScreenWidth && shouldShowHoldMenu && (
{
- const cursorPosition = oldSelection.end + (newLength - prevLength);
- return {start: cursorPosition, end: cursorPosition};
-};
-
function PercentageForm({value: amount, errorText, onInputChange, label, ...rest}: PercentageFormProps, forwardedRef: ForwardedRef) {
const {toLocaleDigit, numberFormat} = useLocalize();
@@ -36,13 +27,6 @@ function PercentageForm({value: amount, errorText, onInputChange, label, ...rest
const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]);
- const [selection, setSelection] = useState({
- start: currentAmount.length,
- end: currentAmount.length,
- });
-
- const forwardDeletePressedRef = useRef(false);
-
/**
* Sets the selection and the amount accordingly to the value passed to the input
* @param newAmount - Changed amount from user input
@@ -55,16 +39,13 @@ function PercentageForm({value: amount, errorText, onInputChange, label, ...rest
// Use a shallow copy of selection to trigger setSelection
// More info: https://github.com/Expensify/App/issues/16385
if (!MoneyRequestUtils.validatePercentage(newAmountWithoutSpaces)) {
- setSelection((prevSelection) => ({...prevSelection}));
return;
}
const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces);
- const isForwardDelete = currentAmount.length > strippedAmount.length && forwardDeletePressedRef.current;
- setSelection(getNewSelection(selection, isForwardDelete ? strippedAmount.length : currentAmount.length, strippedAmount.length));
onInputChange?.(strippedAmount);
},
- [currentAmount, onInputChange, selection],
+ [onInputChange],
);
const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit);
@@ -84,10 +65,6 @@ function PercentageForm({value: amount, errorText, onInputChange, label, ...rest
}
textInput.current = ref;
}}
- selection={selection}
- onSelectionChange={(e: NativeSyntheticEvent) => {
- setSelection(e.nativeEvent.selection);
- }}
suffixCharacter="%"
keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD}
// eslint-disable-next-line react/jsx-props-no-spreading
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 3b074bf772e6..e3a04903f5ca 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -14,6 +14,7 @@ import * as Browser from '@libs/Browser';
import * as Modal from '@userActions/Modal';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
+import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import FocusableMenuItem from './FocusableMenuItem';
import FocusTrapForModal from './FocusTrap/FocusTrapForModal';
@@ -21,6 +22,7 @@ import * as Expensicons from './Icon/Expensicons';
import type {MenuItemProps} from './MenuItem';
import MenuItem from './MenuItem';
import type BaseModalProps from './Modal/types';
+import OfflineWithFeedback from './OfflineWithFeedback';
import PopoverWithMeasuredContent from './PopoverWithMeasuredContent';
import ScrollView from './ScrollView';
import Text from './Text';
@@ -48,6 +50,8 @@ type PopoverMenuItem = MenuItemProps & {
/** Whether to close all modals */
shouldCloseAllModals?: boolean;
+
+ pendingAction?: PendingAction;
};
type PopoverModalProps = Pick;
@@ -262,49 +266,53 @@ function PopoverMenu({
{renderHeaderText()}
{enteredSubMenuIndexes.length > 0 && renderBackButtonItem()}
{currentMenuItems.map((item, menuIndex) => (
- selectItem(menuIndex)}
- focused={focusedIndex === menuIndex}
- displayInDefaultIconColor={item.displayInDefaultIconColor}
- shouldShowRightIcon={item.shouldShowRightIcon}
- shouldShowRightComponent={item.shouldShowRightComponent}
- iconRight={item.iconRight}
- rightComponent={item.rightComponent}
- shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon}
- label={item.label}
- style={{backgroundColor: item.isSelected ? theme.activeComponentBG : undefined}}
- isLabelHoverable={item.isLabelHoverable}
- floatRightAvatars={item.floatRightAvatars}
- floatRightAvatarSize={item.floatRightAvatarSize}
- shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar}
- disabled={item.disabled}
- onFocus={() => setFocusedIndex(menuIndex)}
- success={item.success}
- containerStyle={item.containerStyle}
- shouldRenderTooltip={item.shouldRenderTooltip}
- tooltipAnchorAlignment={item.tooltipAnchorAlignment}
- tooltipShiftHorizontal={item.tooltipShiftHorizontal}
- tooltipShiftVertical={item.tooltipShiftVertical}
- tooltipWrapperStyle={item.tooltipWrapperStyle}
- renderTooltipContent={item.renderTooltipContent}
- numberOfLinesTitle={item.numberOfLinesTitle}
- interactive={item.interactive}
- isSelected={item.isSelected}
- badgeText={item.badgeText}
- />
+ pendingAction={item.pendingAction}
+ >
+ selectItem(menuIndex)}
+ focused={focusedIndex === menuIndex}
+ displayInDefaultIconColor={item.displayInDefaultIconColor}
+ shouldShowRightIcon={item.shouldShowRightIcon}
+ shouldShowRightComponent={item.shouldShowRightComponent}
+ iconRight={item.iconRight}
+ rightComponent={item.rightComponent}
+ shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon}
+ label={item.label}
+ style={{backgroundColor: item.isSelected ? theme.activeComponentBG : undefined}}
+ isLabelHoverable={item.isLabelHoverable}
+ floatRightAvatars={item.floatRightAvatars}
+ floatRightAvatarSize={item.floatRightAvatarSize}
+ shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar}
+ disabled={item.disabled}
+ onFocus={() => setFocusedIndex(menuIndex)}
+ success={item.success}
+ containerStyle={item.containerStyle}
+ shouldRenderTooltip={item.shouldRenderTooltip}
+ tooltipAnchorAlignment={item.tooltipAnchorAlignment}
+ tooltipShiftHorizontal={item.tooltipShiftHorizontal}
+ tooltipShiftVertical={item.tooltipShiftVertical}
+ tooltipWrapperStyle={item.tooltipWrapperStyle}
+ renderTooltipContent={item.renderTooltipContent}
+ numberOfLinesTitle={item.numberOfLinesTitle}
+ interactive={item.interactive}
+ isSelected={item.isSelected}
+ badgeText={item.badgeText}
+ />
+
))}
diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx
index 4a63714b6157..8cbbd1199b33 100644
--- a/src/components/ProcessMoneyReportHoldMenu.tsx
+++ b/src/components/ProcessMoneyReportHoldMenu.tsx
@@ -25,7 +25,7 @@ type ProcessMoneyReportHoldMenuProps = {
isVisible: boolean;
/** The report currently being looked at */
- moneyRequestReport: OnyxTypes.Report;
+ moneyRequestReport: OnyxEntry;
/** Not held amount of expense report */
nonHeldAmount?: string;
@@ -62,8 +62,8 @@ function ProcessMoneyReportHoldMenu({
const onSubmit = (full: boolean) => {
if (isApprove) {
IOU.approveMoneyRequest(moneyRequestReport, full);
- if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport.reportID)) {
- Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport.reportID));
+ if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport?.reportID ?? '')) {
+ Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport?.reportID ?? ''));
}
} else if (chatReport && paymentType) {
IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport, full);
diff --git a/src/components/PromotedActionsBar.tsx b/src/components/PromotedActionsBar.tsx
index a2e3566b159d..85437a9ae3fd 100644
--- a/src/components/PromotedActionsBar.tsx
+++ b/src/components/PromotedActionsBar.tsx
@@ -24,17 +24,20 @@ type PromotedAction = {
key: string;
} & ThreeDotsMenuItem;
-type BasePromotedActions = typeof CONST.PROMOTED_ACTIONS.PIN | typeof CONST.PROMOTED_ACTIONS.SHARE | typeof CONST.PROMOTED_ACTIONS.JOIN;
+type BasePromotedActions = typeof CONST.PROMOTED_ACTIONS.PIN | typeof CONST.PROMOTED_ACTIONS.JOIN;
type PromotedActionsType = Record PromotedAction> & {
- message: (params: {reportID?: string; accountID?: number; login?: string}) => PromotedAction;
+ [CONST.PROMOTED_ACTIONS.SHARE]: (report: OnyxReport, backTo?: string) => PromotedAction;
} & {
- hold: (params: {
+ [CONST.PROMOTED_ACTIONS.MESSAGE]: (params: {reportID?: string; accountID?: number; login?: string}) => PromotedAction;
+} & {
+ [CONST.PROMOTED_ACTIONS.HOLD]: (params: {
isTextHold: boolean;
reportAction: ReportAction | undefined;
reportID?: string;
isDelegateAccessRestricted: boolean;
setIsNoDelegateAccessMenuVisible: (isVisible: boolean) => void;
+ currentSearchHash?: number;
}) => PromotedAction;
};
@@ -43,9 +46,9 @@ const PromotedActions = {
key: CONST.PROMOTED_ACTIONS.PIN,
...HeaderUtils.getPinMenuItem(report),
}),
- share: (report) => ({
+ share: (report, backTo) => ({
key: CONST.PROMOTED_ACTIONS.SHARE,
- ...HeaderUtils.getShareMenuItem(report),
+ ...HeaderUtils.getShareMenuItem(report, backTo),
}),
join: (report) => ({
key: CONST.PROMOTED_ACTIONS.JOIN,
@@ -76,7 +79,7 @@ const PromotedActions = {
}
},
}),
- hold: ({isTextHold, reportAction, reportID, isDelegateAccessRestricted, setIsNoDelegateAccessMenuVisible}) => ({
+ hold: ({isTextHold, reportAction, reportID, isDelegateAccessRestricted, setIsNoDelegateAccessMenuVisible, currentSearchHash}) => ({
key: CONST.PROMOTED_ACTIONS.HOLD,
icon: Expensicons.Stopwatch,
text: Localize.translateLocal(`iou.${isTextHold ? 'hold' : 'unhold'}`),
diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx
index bb704def1836..29439911e221 100644
--- a/src/components/ReceiptAudit.tsx
+++ b/src/components/ReceiptAudit.tsx
@@ -22,7 +22,7 @@ function ReceiptAudit({notes, shouldShowAuditResult}: ReceiptAuditProps) {
let auditText = '';
if (notes.length > 0 && shouldShowAuditResult) {
- auditText = translate('iou.receiptIssuesFound', notes.length);
+ auditText = translate('iou.receiptIssuesFound', {count: notes.length});
} else if (!notes.length && shouldShowAuditResult) {
auditText = translate('common.verified');
}
diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
index aff868a74bc5..2f01bb0f9f46 100644
--- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
+++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
@@ -59,7 +59,7 @@ function ExportWithDropdownMenu({
const options = [
{
value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION,
- text: translate('workspace.common.exportIntegrationSelected', connectionName),
+ text: translate('workspace.common.exportIntegrationSelected', {connectionName}),
...optionTemplate,
},
{
@@ -126,7 +126,7 @@ function ExportWithDropdownMenu({
title={translate('workspace.exportAgainModal.title')}
onConfirm={confirmExport}
onCancel={() => setModalStatus(null)}
- prompt={translate('workspace.exportAgainModal.description', report?.reportName ?? '', connectionName)}
+ prompt={translate('workspace.exportAgainModal.description', {connectionName, reportName: report?.reportName ?? ''})}
confirmText={translate('workspace.exportAgainModal.confirmText')}
cancelText={translate('workspace.exportAgainModal.cancelText')}
isVisible={!!modalStatus}
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index 8546aa8165c9..19ee17bba3b4 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -127,7 +127,16 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '-1', reportField.fieldID))}
+ onPress={() =>
+ Navigation.navigate(
+ ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(
+ report.reportID,
+ report.policyID ?? '-1',
+ reportField.fieldID,
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
+ }
shouldShowRightIcon
disabled={isFieldDisabled}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx
index 15f9cee3705c..af54e2940d3f 100644
--- a/src/components/ReportActionItem/MoneyRequestAction.tsx
+++ b/src/components/ReportActionItem/MoneyRequestAction.tsx
@@ -89,7 +89,7 @@ function MoneyRequestAction({
const onMoneyRequestPreviewPressed = () => {
if (isSplitBillAction) {
const reportActionID = action.reportActionID ?? '-1';
- Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, reportActionID));
+ Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, reportActionID, Navigation.getReportRHPActiveRoute()));
return;
}
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index f2460cae71ed..0a740e65747f 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -51,25 +51,19 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {MoneyRequestPreviewProps, PendingMessageProps} from './types';
function MoneyRequestPreviewContent({
- iouReport,
isBillSplit,
- session,
action,
- personalDetails,
- chatReport,
- transaction,
contextMenuAnchor,
chatReportID,
reportID,
onPreviewPressed,
containerStyles,
- walletTerms,
checkIfContextMenuActive = () => {},
shouldShowPendingConversionMessage = false,
isHovered = false,
isWhisper = false,
- transactionViolations,
shouldDisplayContextMenu = true,
+ iouReportID,
}: MoneyRequestPreviewProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -78,6 +72,16 @@ function MoneyRequestPreviewContent({
const {windowWidth} = useWindowDimensions();
const route = useRoute>();
const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || '-1'}`);
+ const [session] = useOnyx(ONYXKEYS.SESSION);
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || '-1'}`);
+
+ const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action);
+ const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : '-1';
+ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
+ const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
+ const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const sessionAccountID = session?.accountID;
const managerID = iouReport?.managerID ?? -1;
@@ -283,24 +287,25 @@ function MoneyRequestPreviewContent({
);
const navigateToReviewFields = () => {
+ const backTo = route.params.backTo;
const comparisonResult = TransactionUtils.compareDuplicateTransactionFields(reviewingTransactionID);
Transaction.setReviewDuplicatesKey({...comparisonResult.keep, duplicates, transactionID: transaction?.transactionID ?? ''});
if ('merchant' in comparisonResult.change) {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(route.params?.threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(route.params?.threadReportID, backTo));
} else if ('category' in comparisonResult.change) {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(route.params?.threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(route.params?.threadReportID, backTo));
} else if ('tag' in comparisonResult.change) {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(route.params?.threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(route.params?.threadReportID, backTo));
} else if ('description' in comparisonResult.change) {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(route.params?.threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(route.params?.threadReportID, backTo));
} else if ('taxCode' in comparisonResult.change) {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE.getRoute(route.params?.threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE.getRoute(route.params?.threadReportID, backTo));
} else if ('billable' in comparisonResult.change) {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(route.params?.threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(route.params?.threadReportID, backTo));
} else if ('reimbursable' in comparisonResult.change) {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(route.params?.threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(route.params?.threadReportID, backTo));
} else {
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(route.params?.threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(route.params?.threadReportID, backTo));
}
};
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx
index c01206f83f55..f902948b2cb5 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx
@@ -1,44 +1,18 @@
import lodashIsEmpty from 'lodash/isEmpty';
import React from 'react';
-import {withOnyx} from 'react-native-onyx';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import {useOnyx} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import MoneyRequestPreviewContent from './MoneyRequestPreviewContent';
-import type {MoneyRequestPreviewOnyxProps, MoneyRequestPreviewProps} from './types';
+import type {MoneyRequestPreviewProps} from './types';
function MoneyRequestPreview(props: MoneyRequestPreviewProps) {
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${props.iouReportID || '-1'}`);
// We should not render the component if there is no iouReport and it's not a split or track expense.
// Moved outside of the component scope to allow for easier use of hooks in the main component.
// eslint-disable-next-line react/jsx-props-no-spreading
- return lodashIsEmpty(props.iouReport) && !(props.isBillSplit || props.isTrackExpense) ? null : ;
+ return lodashIsEmpty(iouReport) && !(props.isBillSplit || props.isTrackExpense) ? null : ;
}
MoneyRequestPreview.displayName = 'MoneyRequestPreview';
-export default withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- chatReport: {
- key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`,
- },
- iouReport: {
- key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- transaction: {
- key: ({action}) => {
- const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action);
- const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : 0;
- return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
- },
- },
- walletTerms: {
- key: ONYXKEYS.WALLET_TERMS,
- },
- transactionViolations: {
- key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
- },
-})(MoneyRequestPreview);
+export default MoneyRequestPreview;
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts
index 021ae5d188d9..c40b45c6d2bd 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts
+++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts
@@ -1,33 +1,9 @@
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import type * as OnyxTypes from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';
-type MoneyRequestPreviewOnyxProps = {
- /** All of the personal details for everyone */
- personalDetails: OnyxEntry;
-
- /** Chat report associated with iouReport */
- chatReport: OnyxEntry;
-
- /** IOU report data object */
- iouReport: OnyxEntry;
-
- /** Session info for the currently logged in user. */
- session: OnyxEntry;
-
- /** The transaction attached to the action.message.iouTransactionID */
- transaction: OnyxEntry;
-
- /** The transaction violations attached to the action.message.iouTransactionID */
- transactionViolations: OnyxCollection;
-
- /** Information about the user accepting the terms for payments */
- walletTerms: OnyxEntry;
-};
-
-type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & {
+type MoneyRequestPreviewProps = {
/** The active IOUReport, used for Onyx subscription */
// The iouReportID is used inside withOnyx HOC
// eslint-disable-next-line react/no-unused-prop-types
@@ -90,4 +66,4 @@ type PendingProps = {
type PendingMessageProps = PendingProps | NoPendingProps;
-export type {MoneyRequestPreviewProps, MoneyRequestPreviewOnyxProps, PendingMessageProps};
+export type {MoneyRequestPreviewProps, PendingMessageProps};
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 9559a19d08a6..549fd55ac1bb 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -328,7 +328,15 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
shouldShowRightIcon={canEditDistance}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
/>
@@ -340,7 +348,15 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
shouldShowRightIcon={canEditDistanceRate}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
brickRoadIndicator={getErrorForField('customUnitRateID') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('customUnitRateID')}
@@ -355,7 +371,17 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
interactive={canEditDistance}
shouldShowRightIcon={canEditDistance}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))}
+ onPress={() =>
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
+ }
/>
);
@@ -423,7 +449,16 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report?.reportID ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ orderWeight,
+ transaction?.transactionID ?? '',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
brickRoadIndicator={tagError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={tagError}
@@ -500,7 +535,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
iouType,
transaction?.transactionID ?? '-1',
report?.reportID ?? '-1',
- Navigation.getActiveRouteWithoutParams(),
+ Navigation.getReportRHPActiveRoute(),
),
)
}
@@ -518,7 +553,16 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
interactive={canEditAmount}
shouldShowRightIcon={canEditAmount}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ '',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
brickRoadIndicator={getErrorForField('amount') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('amount')}
@@ -533,7 +577,15 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
brickRoadIndicator={getErrorForField('comment') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
@@ -552,7 +604,15 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
shouldShowRightIcon={canEditMerchant}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
wrapperStyle={[styles.taskDescriptionMenuItem]}
brickRoadIndicator={getErrorForField('merchant') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
@@ -569,7 +629,15 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
shouldShowRightIcon={canEditDate}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1' ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1' ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
brickRoadIndicator={getErrorForField('date') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('date')}
@@ -584,7 +652,15 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
brickRoadIndicator={getErrorForField('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('category')}
@@ -611,7 +687,15 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
shouldShowRightIcon={canEditTaxFields}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
+ )
}
brickRoadIndicator={getErrorForField('tax') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('tax')}
@@ -628,7 +712,13 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
titleStyle={styles.flex1}
onPress={() =>
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'),
+ ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '-1',
+ report?.reportID ?? '-1',
+ Navigation.getReportRHPActiveRoute(),
+ ),
)
}
/>
diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx
index 7a6e4942b178..a2ea7487df02 100644
--- a/src/components/ReportActionItem/TaskPreview.tsx
+++ b/src/components/ReportActionItem/TaskPreview.tsx
@@ -1,12 +1,13 @@
import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
-import {useOnyx, withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Avatar from '@components/Avatar';
import Checkbox from '@components/Checkbox';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import {usePersonalDetails} from '@components/OnyxProvider';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import RenderHTML from '@components/RenderHTML';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
@@ -14,6 +15,7 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -27,44 +29,38 @@ import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {Report, ReportAction} from '@src/types/onyx';
+import type {ReportAction} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
-type TaskPreviewOnyxProps = {
- /* Onyx Props */
+type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & {
+ /** The ID of the associated policy */
+ // eslint-disable-next-line react/no-unused-prop-types
+ policyID: string;
+ /** The ID of the associated taskReport */
+ taskReportID: string;
- /* current report of TaskPreview */
- taskReport: OnyxEntry;
-};
-
-type TaskPreviewProps = WithCurrentUserPersonalDetailsProps &
- TaskPreviewOnyxProps & {
- /** The ID of the associated policy */
- // eslint-disable-next-line react/no-unused-prop-types
- policyID: string;
- /** The ID of the associated taskReport */
- taskReportID: string;
+ /** Whether the task preview is hovered so we can modify its style */
+ isHovered: boolean;
- /** Whether the task preview is hovered so we can modify its style */
- isHovered: boolean;
+ /** The linked reportAction */
+ action: OnyxEntry;
- /** The linked reportAction */
- action: OnyxEntry;
+ /** The chat report associated with taskReport */
+ chatReportID: string;
- /** The chat report associated with taskReport */
- chatReportID: string;
+ /** Popover context menu anchor, used for showing context menu */
+ contextMenuAnchor: ContextMenuAnchor;
- /** Popover context menu anchor, used for showing context menu */
- contextMenuAnchor: ContextMenuAnchor;
-
- /** Callback for updating context menu active state, used for showing context menu */
- checkIfContextMenuActive: () => void;
- };
+ /** Callback for updating context menu active state, used for showing context menu */
+ checkIfContextMenuActive: () => void;
+};
-function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatReportID, checkIfContextMenuActive, currentUserPersonalDetails, isHovered = false}: TaskPreviewProps) {
+function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, checkIfContextMenuActive, currentUserPersonalDetails, isHovered = false}: TaskPreviewProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
+ const theme = useTheme();
+ const [taskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`);
// The reportAction might not contain details regarding the taskReport
// Only the direct parent reportAction will contain details about the taskReport
@@ -74,10 +70,11 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR
: action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? ''));
const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? -1;
- const [avatar] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: (personalDetails) => personalDetails?.[taskAssigneeAccountID]?.avatar});
+ const personalDetails = usePersonalDetails();
+ const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? Expensicons.FallbackAvatar;
const htmlForTaskPreview = `${taskTitle} `;
const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action);
-
+ const shouldShowGreenDotIndicator = ReportUtils.isOpenTaskReport(taskReport, action) && ReportUtils.isReportManager(taskReport);
if (isDeletedParentAction) {
return ${translate('parentReportAction.deletedTask')}`} />;
}
@@ -123,6 +120,14 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR
${htmlForTaskPreview}` : htmlForTaskPreview} />
+ {shouldShowGreenDotIndicator && (
+
+
+
+ )}
({
- taskReport: {
- key: ({taskReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`,
- },
- })(TaskPreview),
-);
+export default withCurrentUserPersonalDetails(TaskPreview);
diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx
index 2822e34a3d04..9a2906aa7d62 100644
--- a/src/components/ReportActionItem/TaskView.tsx
+++ b/src/components/ReportActionItem/TaskView.tsx
@@ -81,7 +81,7 @@ function TaskView({report, ...props}: TaskViewProps) {
(e.currentTarget as HTMLElement).blur();
}
- Navigation.navigate(ROUTES.TASK_TITLE.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.TASK_TITLE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()));
})}
style={({pressed}) => [
styles.ph5,
@@ -144,7 +144,7 @@ function TaskView({report, ...props}: TaskViewProps) {
shouldRenderAsHTML
description={translate('task.description')}
title={report.description ?? ''}
- onPress={() => Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID))}
+ onPress={() => Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
@@ -162,7 +162,7 @@ function TaskView({report, ...props}: TaskViewProps) {
iconType={CONST.ICON_TYPE_AVATAR}
avatarSize={CONST.AVATAR_SIZE.SMALLER}
titleStyle={styles.assigneeTextStyle}
- onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID))}
+ onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2]}
@@ -174,7 +174,7 @@ function TaskView({report, ...props}: TaskViewProps) {
) : (
Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID))}
+ onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))}
shouldShowRightIcon={isOpen}
disabled={disableState}
wrapperStyle={[styles.pv2]}
diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx
index 96f705ea2d52..68f060d22e6c 100644
--- a/src/components/ReportWelcomeText.tsx
+++ b/src/components/ReportWelcomeText.tsx
@@ -57,7 +57,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
return;
}
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()));
};
const welcomeHeroText = useMemo(() => {
@@ -113,11 +113,12 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
(welcomeMessage?.messageHtml ? (
{
+ const activeRoute = Navigation.getReportRHPActiveRoute();
if (ReportUtils.canEditReportDescription(report, policy)) {
- Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID ?? '-1'));
+ Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID ?? '-1', activeRoute));
return;
}
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '-1'));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '-1', activeRoute));
}}
style={styles.renderHTML}
accessibilityLabel={translate('reportDescriptionPage.roomDescription')}
@@ -161,7 +162,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
) : (
Navigation.navigate(ROUTES.PROFILE.getRoute(accountID))}
+ onPress={() => Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getReportRHPActiveRoute()))}
suppressHighlighting
>
{displayName}
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
index 704f72055410..023e7315e115 100644
--- a/src/components/Search/SearchPageHeader.tsx
+++ b/src/components/Search/SearchPageHeader.tsx
@@ -59,7 +59,7 @@ function HeaderWrapper({icon, title, subtitle, children, subtitleStyles = {}}: H
}
subtitle={
{subtitle}
diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx
index da72135c6035..e44d57ab18e2 100644
--- a/src/components/ThreeDotsMenu/index.tsx
+++ b/src/components/ThreeDotsMenu/index.tsx
@@ -1,7 +1,6 @@
import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PopoverMenu from '@components/PopoverMenu';
@@ -13,14 +12,8 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as Browser from '@libs/Browser';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Modal} from '@src/types/onyx';
import type ThreeDotsMenuProps from './types';
-type ThreeDotsMenuOnyxProps = {
- /** Details about any modals being used */
- modal: OnyxEntry;
-};
-
function ThreeDotsMenu({
iconTooltip = 'common.more',
icon = Expensicons.ThreeDots,
@@ -36,8 +29,9 @@ function ThreeDotsMenu({
shouldOverlay = false,
shouldSetModalVisibility = true,
disabled = false,
- modal = {},
}: ThreeDotsMenuProps) {
+ const [modal] = useOnyx(ONYXKEYS.MODAL);
+
const theme = useTheme();
const styles = useThemeStyles();
const [isPopupMenuVisible, setPopupMenuVisible] = useState(false);
@@ -114,8 +108,4 @@ function ThreeDotsMenu({
ThreeDotsMenu.displayName = 'ThreeDotsMenu';
-export default withOnyx({
- modal: {
- key: ONYXKEYS.MODAL,
- },
-})(ThreeDotsMenu);
+export default ThreeDotsMenu;
diff --git a/src/components/ThreeDotsMenu/types.ts b/src/components/ThreeDotsMenu/types.ts
index 6c3618ffc3ce..86a10d08d449 100644
--- a/src/components/ThreeDotsMenu/types.ts
+++ b/src/components/ThreeDotsMenu/types.ts
@@ -1,18 +1,11 @@
import type {StyleProp, ViewStyle} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type {TranslationPaths} from '@src/languages/types';
import type {AnchorPosition} from '@src/styles';
-import type {Modal} from '@src/types/onyx';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import type IconAsset from '@src/types/utils/IconAsset';
-type ThreeDotsMenuOnyxProps = {
- /** Details about any modals being used */
- modal: OnyxEntry;
-};
-
-type ThreeDotsMenuProps = ThreeDotsMenuOnyxProps & {
+type ThreeDotsMenuProps = {
/** Tooltip for the popup icon */
iconTooltip?: TranslationPaths;
diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
index e42f95874b42..f586c20cba49 100644
--- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
+++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
@@ -34,7 +34,7 @@ function BaseGenericTooltip({
},
wrapperStyle = {},
shouldUseOverlay = false,
- onPressOverlay = () => {},
+ onHideTooltip = () => {},
}: BaseGenericTooltipProps) {
// The width of tooltip's inner content. Has to be undefined in the beginning
// as a width of 0 will cause the content to be rendered of a width of 0,
@@ -102,7 +102,7 @@ function BaseGenericTooltip({
return (
- {shouldUseOverlay && }
+ {shouldUseOverlay && }
{},
+ onHideTooltip = () => {},
}: BaseGenericTooltipProps) {
// The width of tooltip's inner content. Has to be undefined in the beginning
// as a width of 0 will cause the content to be rendered of a width of 0,
@@ -119,7 +119,7 @@ function BaseGenericTooltip({
return ReactDOM.createPortal(
<>
- {shouldUseOverlay && }
+ {shouldUseOverlay && }
void;
} & Pick<
SharedTooltipProps,
- 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'anchorAlignment' | 'shouldUseOverlay' | 'onPressOverlay'
+ 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'anchorAlignment' | 'shouldUseOverlay' | 'onHideTooltip'
>;
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
index d0ff254324ae..ef5327feba31 100644
--- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
+++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
@@ -1,7 +1,9 @@
import React, {memo, useEffect, useRef, useState} from 'react';
import type {LayoutRectangle, NativeSyntheticEvent} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import GenericTooltip from '@components/Tooltip/GenericTooltip';
import type {EducationalTooltipProps} from '@components/Tooltip/types';
+import ONYXKEYS from '@src/ONYXKEYS';
import measureTooltipCoordinate from './measureTooltipCoordinate';
type LayoutChangeEventWithTarget = NativeSyntheticEvent<{layout: LayoutRectangle; target: HTMLElement}>;
@@ -10,11 +12,14 @@ type LayoutChangeEventWithTarget = NativeSyntheticEvent<{layout: LayoutRectangle
* A component used to wrap an element intended for displaying a tooltip.
* This tooltip would show immediately without user's interaction and hide after 5 seconds.
*/
-function BaseEducationalTooltip({children, shouldAutoDismiss = false, shouldRender = false, ...props}: EducationalTooltipProps) {
+function BaseEducationalTooltip({children, onHideTooltip, shouldAutoDismiss = false, ...props}: EducationalTooltipProps) {
const hideTooltipRef = useRef<() => void>();
const [shouldMeasure, setShouldMeasure] = useState(false);
const show = useRef<() => void>();
+ const [modal] = useOnyx(ONYXKEYS.MODAL);
+
+ const shouldShow = !modal?.willAlertModalBecomeVisible && !modal?.isVisible;
useEffect(
() => () => {
@@ -33,27 +38,38 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, shouldRend
return;
}
- const timerID = setTimeout(hideTooltipRef.current, 5000);
+ // If the modal is open, hide the tooltip immediately and clear the timeout
+ if (!shouldShow) {
+ hideTooltipRef.current();
+ return;
+ }
+
+ // Automatically hide tooltip after 5 seconds if shouldAutoDismiss is true
+ const timerID = setTimeout(() => {
+ hideTooltipRef.current?.();
+ onHideTooltip?.();
+ }, 5000);
return () => {
clearTimeout(timerID);
};
- }, [shouldAutoDismiss]);
+ }, [shouldAutoDismiss, shouldShow, onHideTooltip]);
useEffect(() => {
- if (!shouldRender || !shouldMeasure) {
+ if (!shouldMeasure || !shouldShow) {
return;
}
// When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content.
setTimeout(() => {
show.current?.();
}, 500);
- }, [shouldMeasure, shouldRender]);
+ }, [shouldMeasure, shouldShow]);
return (
{({showTooltip, hideTooltip, updateTargetBounds}) => {
// eslint-disable-next-line react-compiler/react-compiler
diff --git a/src/components/Tooltip/EducationalTooltip/index.tsx b/src/components/Tooltip/EducationalTooltip/index.tsx
index 03500f768dd9..a97e36a5904c 100644
--- a/src/components/Tooltip/EducationalTooltip/index.tsx
+++ b/src/components/Tooltip/EducationalTooltip/index.tsx
@@ -2,7 +2,11 @@ import React from 'react';
import type {TooltipExtendedProps} from '@components/Tooltip/types';
import BaseEducationalTooltip from './BaseEducationalTooltip';
-function EducationalTooltip({children, ...props}: TooltipExtendedProps) {
+function EducationalTooltip({children, shouldRender = false, ...props}: TooltipExtendedProps) {
+ if (!shouldRender) {
+ return children;
+ }
+
return (
{},
+ onHideTooltip = () => {},
}: GenericTooltipProps) {
const {preferredLocale} = useLocalize();
const {windowWidth} = useWindowDimensions();
@@ -150,8 +150,8 @@ function GenericTooltip({
}
setShouldUseOverlay(false);
hideTooltip();
- onPressOverlayProp();
- }, [shouldUseOverlay, onPressOverlayProp, hideTooltip]);
+ onHideTooltip();
+ }, [shouldUseOverlay, onHideTooltip, hideTooltip]);
useImperativeHandle(TooltipRefManager.ref, () => ({hideTooltip}), [hideTooltip]);
@@ -183,7 +183,7 @@ function GenericTooltip({
wrapperStyle={wrapperStyle}
anchorAlignment={anchorAlignment}
shouldUseOverlay={shouldUseOverlay}
- onPressOverlay={onPressOverlay}
+ onHideTooltip={onPressOverlay}
/>
)}
diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts
index 0462b36fa524..0924f5d46a28 100644
--- a/src/components/Tooltip/types.ts
+++ b/src/components/Tooltip/types.ts
@@ -40,8 +40,8 @@ type SharedTooltipProps = {
/** Should render a fullscreen transparent overlay */
shouldUseOverlay?: boolean;
- /** Callback to fire when the transparent overlay is pressed */
- onPressOverlay?: () => void;
+ /** Handles what to do when hiding the tooltip */
+ onHideTooltip?: () => void;
};
type GenericTooltipState = {
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx
index 14943a42a9d8..84eb988d0758 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.tsx
+++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx
@@ -441,7 +441,7 @@ function BaseVideoPlayer({
)}
- {((isLoading && !isOffline) || isBuffering) && }
+ {((isLoading && !isOffline) || (isBuffering && !isPlaying)) && }
{isLoading && (isOffline || !isBuffering) && }
{controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && (
(currentScreenName);
+function useReviewDuplicatesNavigation(stepNames: string[], currentScreenName: StepName, threadReportID: string, backTo?: string) {
+ const [nextScreen, setNextScreen] = useState();
+ const [prevScreen, setPrevScreen] = useState();
const [currentScreenIndex, setCurrentScreenIndex] = useState(0);
const intersection = useMemo(() => CONST.REVIEW_DUPLICATES_ORDER.filter((element) => stepNames.includes(element)), [stepNames]);
useEffect(() => {
+ if (currentScreenName === 'confirmation') {
+ setPrevScreen(intersection[intersection.length - 1] ?? '');
+ return;
+ }
const currentIndex = intersection.indexOf(currentScreenName);
const nextScreenIndex = currentIndex + 1;
+ const prevScreenIndex = currentIndex - 1;
setCurrentScreenIndex(currentIndex);
setNextScreen(intersection[nextScreenIndex] ?? '');
+ setPrevScreen(intersection[prevScreenIndex] ?? '');
}, [currentScreenName, intersection]);
+ const goBack = () => {
+ switch (prevScreen) {
+ case 'merchant':
+ Navigation.goBack(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(threadReportID, backTo));
+ break;
+ case 'category':
+ Navigation.goBack(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(threadReportID, backTo));
+ break;
+ case 'tag':
+ Navigation.goBack(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(threadReportID, backTo));
+ break;
+ case 'description':
+ Navigation.goBack(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(threadReportID, backTo));
+ break;
+ case 'taxCode':
+ Navigation.goBack(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE.getRoute(threadReportID, backTo));
+ break;
+ case 'reimbursable':
+ Navigation.goBack(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(threadReportID, backTo));
+ break;
+ case 'billable':
+ Navigation.goBack(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(threadReportID, backTo));
+ break;
+ default:
+ Navigation.goBack(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(threadReportID, backTo));
+ break;
+ }
+ };
+
const navigateToNextScreen = () => {
switch (nextScreen) {
case 'merchant':
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(threadReportID, backTo));
break;
case 'category':
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(threadReportID, backTo));
break;
case 'tag':
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(threadReportID, backTo));
break;
case 'description':
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(threadReportID, backTo));
break;
case 'taxCode':
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE.getRoute(threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE.getRoute(threadReportID, backTo));
break;
case 'reimbursable':
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(threadReportID, backTo));
break;
case 'billable':
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(threadReportID, backTo));
break;
default:
- Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(threadReportID));
+ Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(threadReportID, backTo));
break;
}
};
- return {navigateToNextScreen, currentScreenIndex};
+ return {navigateToNextScreen, goBack, currentScreenIndex};
}
export default useReviewDuplicatesNavigation;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 7844d12e764a..6a90b1f8302a 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2,45 +2,91 @@ import {CONST as COMMON_CONST, Str} from 'expensify-common';
import startCase from 'lodash/startCase';
import CONST from '@src/CONST';
import type {Country} from '@src/CONST';
-import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy';
import type {
AccountOwnerParams,
+ ActionsAreCurrentlyRestricted,
+ AddEmployeeParams,
AddressLineParams,
AdminCanceledRequestParams,
+ AgeParams,
AlreadySignedInParams,
ApprovalWorkflowErrorParams,
ApprovedAmountParams,
AssignCardParams,
+ AssignedYouCardParams,
+ AssigneeParams,
+ AuthenticationErrorParams,
+ AutoPayApprovedReportsLimitErrorParams,
+ BadgeFreeTrialParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartTwo,
BeginningOfChatHistoryDomainRoomPartOneParams,
+ BillingBannerCardAuthenticationRequiredParams,
+ BillingBannerCardExpiredParams,
+ BillingBannerCardOnDisputeParams,
+ BillingBannerDisputePendingParams,
+ BillingBannerInsufficientFundsParams,
+ BillingBannerSubtitleWithDateParams,
CanceledRequestParams,
+ CardEndingParams,
+ CardInfoParams,
+ CardNextPaymentParams,
+ CategoryNameParams,
ChangeFieldParams,
+ ChangeOwnerDuplicateSubscriptionParams,
+ ChangeOwnerHasFailedSettlementsParams,
+ ChangeOwnerSubscriptionParams,
ChangePolicyParams,
ChangeTypeParams,
+ CharacterLengthLimitParams,
CharacterLimitParams,
CompanyCardFeedNameParams,
ConfirmHoldExpenseParams,
ConfirmThatParams,
+ ConnectionNameParams,
+ ConnectionParams,
+ CustomersOrJobsLabelParams,
+ DateParams,
DateShouldBeAfterParams,
DateShouldBeBeforeParams,
+ DefaultAmountParams,
+ DefaultVendorDescriptionParams,
+ DelegateRoleParams,
DelegateSubmitParams,
+ DelegatorParams,
DeleteActionParams,
DeleteConfirmationParams,
DeleteExpenseTranslationParams,
DidSplitAmountMessageParams,
+ DimensionsCountParams,
DistanceRateOperationsParams,
EditActionParams,
ElectronicFundsParams,
EnterMagicCodeParams,
+ ExportAgainModalDescriptionParams,
ExportedToIntegrationParams,
+ ExportIntegrationSelectedParams,
+ FeatureNameParams,
+ FiltersAmountBetweenParams,
FormattedMaxLengthParams,
ForwardedAmountParams,
GoBackMessageParams,
GoToRoomParams,
+ ImportedTagsMessageParams,
+ ImportFieldParams,
+ ImportMembersSuccessfullDescriptionParams,
+ ImportTagsSuccessfullDescriptionParams,
+ IncorrectZipFormatParams,
InstantSummaryParams,
+ IntacctMappingTitleParams,
+ IntegrationExportParams,
+ IntegrationSyncFailedParams,
+ InvalidPropertyParams,
+ InvalidValueParams,
IssueVirtualCardParams,
+ LastSyncAccountingParams,
+ LastSyncDateParams,
LocalTimeParams,
LoggedInAsParams,
LogSizeParams,
@@ -48,12 +94,15 @@ import type {
ManagerApprovedParams,
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
+ MissingPropertyParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
OOOEventSummaryFullDayParams,
OOOEventSummaryPartialDayParams,
+ OptionalParam,
OurEmailProviderParams,
+ OwnerOwesAmountParams,
PaidElsewhereWithAmountParams,
PaidWithExpensifyWithAmountParams,
ParentNavigationSummaryParams,
@@ -63,21 +112,28 @@ import type {
PayerPaidParams,
PayerSettledParams,
PaySomeoneParams,
+ ReconciliationWorksParams,
ReimbursementRateParams,
+ RemovedFromApprovalWorkflowParams,
RemovedTheRequestParams,
+ RemoveMemberPromptParams,
RemoveMembersWarningPrompt,
RenamedRoomActionParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
ReportArchiveReasonsMergedParams,
- ReportArchiveReasonsPolicyDeletedParams,
ReportArchiveReasonsRemovedFromPolicyParams,
+ ReportPolicyNameParams,
RequestAmountParams,
RequestCountParams,
RequestedAmountMessageParams,
+ RequiredFieldParams,
ResolutionConstraintsParams,
+ RoleNamesParams,
RoomNameReservedErrorParams,
RoomRenamedToParams,
+ SecondaryLoginParams,
+ SelectedNumberParams,
SetTheDistanceMerchantParams,
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
@@ -86,20 +142,32 @@ import type {
SignUpNewFaceCodeParams,
SizeExceededParams,
SplitAmountParams,
+ SpreadCategoriesParams,
+ SpreadFieldNameParams,
+ SpreadSheetColumnParams,
+ StatementTitleParams,
StepCounterParams,
StripePaidParams,
+ SubscriptionCommitmentParams,
+ SubscriptionSettingsRenewsOnParams,
+ SubscriptionSettingsSaveUpToParams,
+ SubscriptionSizeParams,
+ SyncStageNameConnectionsParams,
TaskCreatedActionParams,
+ TaxAmountParams,
TermsParams,
ThreadRequestReportNameParams,
ThreadSentMoneyReportNameParams,
ToValidateLoginParams,
TransferParams,
- TranslationBase,
+ TrialStartedTitleParams,
UnapprovedParams,
+ UnapproveWithIntegrationWarningParams,
UnshareParams,
UntilTimeParams,
UpdatedTheDistanceMerchantParams,
UpdatedTheRequestParams,
+ UpdateRoleParams,
UsePlusButtonParams,
UserIsAlreadyMemberParams,
UserSplitParams,
@@ -123,8 +191,11 @@ import type {
WelcomeNoteParams,
WelcomeToRoomParams,
WeSentYouMagicSignInLinkParams,
+ WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams,
+ YourPlanPriceParams,
ZipCodeExampleFormatParams,
-} from './types';
+} from './params';
+import type {TranslationDeepObject} from './types';
type StateValue = {
stateISO: string;
@@ -136,7 +207,7 @@ type States = Record;
type AllCountries = Record;
/* eslint-disable max-len */
-export default {
+const translations = {
common: {
cancel: 'Cancel',
dismiss: 'Dismiss',
@@ -264,7 +335,7 @@ export default {
fieldRequired: 'This field is required.',
requestModified: 'This request is being modified by another member.',
characterLimit: ({limit}: CharacterLimitParams) => `Exceeds the maximum length of ${limit} characters`,
- characterLimitExceedCounter: ({length, limit}) => `Character limit exceeded (${length}/${limit})`,
+ characterLimitExceedCounter: ({length, limit}: CharacterLengthLimitParams) => `Character limit exceeded (${length}/${limit})`,
dateInvalid: 'Please select a valid date.',
invalidDateShouldBeFuture: 'Please choose today or a future date.',
invalidTimeShouldBeFuture: 'Please choose a time at least one minute ahead.',
@@ -643,7 +714,7 @@ export default {
shouldUseYou
? `This chat is no longer active because you are no longer a member of the ${policyName} workspace.`
: `This chat is no longer active because ${displayName} is no longer a member of the ${policyName} workspace.`,
- [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsPolicyDeletedParams) =>
+ [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) =>
`This chat is no longer active because ${policyName} is no longer an active workspace.`,
[CONST.REPORT.ARCHIVE_REASON.INVOICE_RECEIVER_POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) =>
`This chat is no longer active because ${policyName} is no longer an active workspace.`,
@@ -684,13 +755,13 @@ export default {
dragAndDrop: 'Drag and drop your spreadsheet here, or choose a file below. Supported formats: .csv, .txt, .xls, and .xlsx.',
chooseSpreadsheet: 'Select a spreadsheet file to import. Supported formats: .csv, .txt, .xls, and .xlsx.',
fileContainsHeader: 'File contains column headers',
- column: (name: string) => `Column ${name}`,
- fieldNotMapped: (fieldName: string) => `Oops! A required field ("${fieldName}") hasn't been mapped. Please review and try again.`,
- singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`,
+ column: ({name}: SpreadSheetColumnParams) => `Column ${name}`,
+ fieldNotMapped: ({fieldName}: SpreadFieldNameParams) => `Oops! A required field ("${fieldName}") hasn't been mapped. Please review and try again.`,
+ singleFieldMultipleColumns: ({fieldName}: SpreadFieldNameParams) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`,
importSuccessfullTitle: 'Import successful',
- importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'),
- importMembersSuccessfullDescription: (members: number) => (members > 1 ? `${members} members have been added.` : '1 member has been added.'),
- importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'),
+ importCategoriesSuccessfullDescription: ({categories}: SpreadCategoriesParams) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'),
+ importMembersSuccessfullDescription: ({members}: ImportMembersSuccessfullDescriptionParams) => (members > 1 ? `${members} members have been added.` : '1 member has been added.'),
+ importTagsSuccessfullDescription: ({tags}: ImportTagsSuccessfullDescriptionParams) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'),
importFailedTitle: 'Import failed',
importFailedDescription: 'Please ensure all fields are filled out correctly and try again. If the problem persists, please reach out to Concierge.',
importDescription: 'Choose which fields to map from your spreadsheet by clicking the dropdown next to each imported column below.',
@@ -729,7 +800,7 @@ export default {
splitBill: 'Split expense',
splitScan: 'Split receipt',
splitDistance: 'Split distance',
- paySomeone: (name: string) => `Pay ${name ?? 'someone'}`,
+ paySomeone: ({name}: PaySomeoneParams = {}) => `Pay ${name ?? 'someone'}`,
assignTask: 'Assign task',
header: 'Quick action',
trackManual: 'Track expense',
@@ -753,7 +824,7 @@ export default {
original: 'Original',
split: 'Split',
splitExpense: 'Split expense',
- paySomeone: ({name}: PaySomeoneParams) => `Pay ${name ?? 'someone'}`,
+ paySomeone: ({name}: PaySomeoneParams = {}) => `Pay ${name ?? 'someone'}`,
expense: 'Expense',
categorize: 'Categorize',
share: 'Share',
@@ -775,7 +846,7 @@ export default {
receiptScanning: 'Receipt scanning...',
receiptScanInProgress: 'Receipt scan in progress',
receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.',
- receiptIssuesFound: (count: number) => `${count === 1 ? 'Issue' : 'Issues'} found`,
+ receiptIssuesFound: ({count}: DistanceRateOperationsParams) => `${count === 1 ? 'Issue' : 'Issues'} found`,
fieldPending: 'Pending...',
defaultRate: 'Default rate',
receiptMissingDetails: 'Receipt missing details',
@@ -819,15 +890,17 @@ export default {
sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`,
submitAmount: ({amount}: RequestAmountParams) => `submit ${amount}`,
submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `submitted ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
+ automaticallySubmittedAmount: ({formattedAmount}: RequestedAmountMessageParams) =>
+ `automatically submitted ${formattedAmount} via delayed submission `,
trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`,
didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
yourSplit: ({amount}: UserSplitParams) => `Your split ${amount}`,
payerOwesAmount: ({payer, amount, comment}: PayerOwesAmountParams) => `${payer} owes ${amount}${comment ? ` for ${comment}` : ''}`,
payerOwes: ({payer}: PayerOwesParams) => `${payer} owes: `,
- payerPaidAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer ? `${payer} ` : ''}paid ${amount}`,
+ payerPaidAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer ? `${payer} ` : ''}paid ${amount}`,
payerPaid: ({payer}: PayerPaidParams) => `${payer} paid: `,
- payerSpentAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer} spent ${amount}`,
+ payerSpentAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer} spent ${amount}`,
payerSpent: ({payer}: PayerPaidParams) => `${payer} spent: `,
managerApproved: ({manager}: ManagerApprovedParams) => `${manager} approved:`,
managerApprovedAmount: ({manager, amount}: ManagerApprovedAmountParams) => `${manager} approved ${amount}`,
@@ -926,7 +999,7 @@ export default {
unapprove: 'Unapprove',
unapproveReport: 'Unapprove report',
headsUp: 'Heads up!',
- unapproveWithIntegrationWarning: (accountingIntegration: string) =>
+ unapproveWithIntegrationWarning: ({accountingIntegration}: UnapproveWithIntegrationWarningParams) =>
`This report has already been exported to ${accountingIntegration}. Changes to this report in Expensify may lead to data discrepancies and Expensify Card reconciliation issues. Are you sure you want to unapprove this report?`,
reimbursable: 'reimbursable',
nonReimbursable: 'non-reimbursable',
@@ -1310,15 +1383,16 @@ export default {
availableSpend: 'Remaining limit',
smartLimit: {
name: 'Smart limit',
- title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card, and the limit will reset as your submitted expenses are approved.`,
+ title: ({formattedLimit}: ViolationsOverLimitParams) => `You can spend up to ${formattedLimit} on this card, and the limit will reset as your submitted expenses are approved.`,
},
fixedLimit: {
name: 'Fixed limit',
- title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card, and then it will deactivate.`,
+ title: ({formattedLimit}: ViolationsOverLimitParams) => `You can spend up to ${formattedLimit} on this card, and then it will deactivate.`,
},
monthlyLimit: {
name: 'Monthly limit',
- title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card per month. The limit will reset on the 1st day of each calendar month.`,
+ title: ({formattedLimit}: ViolationsOverLimitParams) =>
+ `You can spend up to ${formattedLimit} on this card per month. The limit will reset on the 1st day of each calendar month.`,
},
virtualCardNumber: 'Virtual card number',
physicalCardNumber: 'Physical card number',
@@ -1519,7 +1593,7 @@ export default {
},
},
reportDetailsPage: {
- inWorkspace: ({policyName}) => `in ${policyName}`,
+ inWorkspace: ({policyName}: ReportPolicyNameParams) => `in ${policyName}`,
},
reportDescriptionPage: {
roomDescription: 'Room description',
@@ -1532,7 +1606,7 @@ export default {
groupChat: {
lastMemberTitle: 'Heads up!',
lastMemberWarning: "Since you're the last person here, leaving will make this chat inaccessible to all users. Are you sure you want to leave?",
- defaultReportName: ({displayName}: {displayName: string}) => `${displayName}'s group chat`,
+ defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `${displayName}'s group chat`,
},
languagePage: {
language: 'Language',
@@ -1664,7 +1738,7 @@ export default {
dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `Date should be before ${dateString}.`,
dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `Date should be after ${dateString}.`,
hasInvalidCharacter: 'Name can only include Latin characters.',
- incorrectZipFormat: (zipFormat?: string) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`,
+ incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams = {}) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`,
},
},
resendValidationForm: {
@@ -1681,8 +1755,8 @@ export default {
succesfullyUnlinkedLogin: 'Secondary login successfully unlinked!',
},
emailDeliveryFailurePage: {
- ourEmailProvider: (user: OurEmailProviderParams) =>
- `Our email provider has temporarily suspended emails to ${user.login} due to delivery issues. To unblock your login, please follow these steps:`,
+ ourEmailProvider: ({login}: OurEmailProviderParams) =>
+ `Our email provider has temporarily suspended emails to ${login} due to delivery issues. To unblock your login, please follow these steps:`,
confirmThat: ({login}: ConfirmThatParams) => `Confirm that ${login} is spelled correctly and is a real, deliverable email address. `,
emailAliases: 'Email aliases such as "expenses@domain.com" must have access to their own email inbox for it to be a valid Expensify login.',
ensureYourEmailClient: 'Ensure your email client allows expensify.com emails. ',
@@ -2140,6 +2214,8 @@ export default {
},
bookTravel: 'Book travel',
bookDemo: 'Book demo',
+ bookADemo: 'Book a demo',
+ toLearnMore: ' to learn more.',
termsAndConditions: {
header: 'Before we continue...',
title: 'Please read the Terms & Conditions for travel',
@@ -2197,7 +2273,7 @@ export default {
testTransactions: 'Test transactions',
issueAndManageCards: 'Issue and manage cards',
reconcileCards: 'Reconcile cards',
- selected: ({selectedNumber}) => `${selectedNumber} selected`,
+ selected: ({selectedNumber}: SelectedNumberParams) => `${selectedNumber} selected`,
settlementFrequency: 'Settlement frequency',
deleteConfirmation: 'Are you sure you want to delete this workspace?',
unavailable: 'Unavailable workspace',
@@ -2216,7 +2292,7 @@ export default {
`You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`,
subscription: 'Subscription',
markAsExported: 'Mark as manually entered',
- exportIntegrationSelected: (connectionName: ConnectionName) => `Export to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
+ exportIntegrationSelected: ({connectionName}: ExportIntegrationSelectedParams) => `Export to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
letsDoubleCheck: "Let's double check that everything looks right.",
lineItemLevel: 'Line-item level',
reportLevel: 'Report level',
@@ -2233,13 +2309,13 @@ export default {
createNewConnection: 'Create new connection',
reuseExistingConnection: 'Reuse existing connection',
existingConnections: 'Existing connections',
- lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Last synced ${formattedDate}`,
- authenticationError: (connectionName: string) => `Can’t connect to ${connectionName} due to an authentication error.`,
+ lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} - Last synced ${formattedDate}`,
+ authenticationError: ({connectionName}: AuthenticationErrorParams) => `Can’t connect to ${connectionName} due to an authentication error.`,
learnMore: 'Learn more.',
memberAlternateText: 'Members can submit and approve reports.',
adminAlternateText: 'Admins have full edit access to all reports and workspace settings.',
auditorAlternateText: 'Auditors can view and comment on reports.',
- roleName: (role?: string): string => {
+ roleName: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
return 'Admin';
@@ -2364,8 +2440,8 @@ export default {
accountsSwitchDescription: 'Enabled categories will be available for members to select when creating their expenses.',
trackingCategories: 'Tracking categories',
trackingCategoriesDescription: 'Choose how to handle Xero tracking categories in Expensify.',
- mapTrackingCategoryTo: ({categoryName}) => `Map Xero ${categoryName} to`,
- mapTrackingCategoryToDescription: ({categoryName}) => `Choose where to map ${categoryName} when exporting to Xero.`,
+ mapTrackingCategoryTo: ({categoryName}: CategoryNameParams) => `Map Xero ${categoryName} to`,
+ mapTrackingCategoryToDescription: ({categoryName}: CategoryNameParams) => `Choose where to map ${categoryName} when exporting to Xero.`,
customers: 'Re-bill customers',
customersDescription: 'Choose whether to re-bill customers in Expensify. Your Xero customer contacts can be tagged to expenses, and will export to Xero as a sales invoice.',
taxesDescription: 'Choose how to handle Xero taxes in Expensify.',
@@ -2462,7 +2538,7 @@ export default {
},
creditCardAccount: 'Credit card account',
defaultVendor: 'Default vendor',
- defaultVendorDescription: (isReimbursable: boolean): string =>
+ defaultVendorDescription: ({isReimbursable}: DefaultVendorDescriptionParams) =>
`Set a default vendor that will apply to ${isReimbursable ? '' : 'non-'}reimbursable expenses that don't have a matching vendor in Sage Intacct.`,
exportDescription: 'Configure how Expensify data exports to Sage Intacct.',
exportPreferredExporterNote:
@@ -2676,12 +2752,12 @@ export default {
importJobs: 'Import projects',
customers: 'customers',
jobs: 'projects',
- label: (importFields: string[], importType: string) => `${importFields.join(' and ')}, ${importType}`,
+ label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join(' and ')}, ${importType}`,
},
importTaxDescription: 'Import tax groups from NetSuite.',
importCustomFields: {
chooseOptionBelow: 'Choose an option below:',
- requiredFieldError: (fieldName: string) => `Please enter the ${fieldName}`,
+ requiredFieldError: ({fieldName}: RequiredFieldParams) => `Please enter the ${fieldName}`,
customSegments: {
title: 'Custom segments/records',
addText: 'Add custom segment/record',
@@ -2722,7 +2798,7 @@ export default {
customRecordMappingTitle: 'How should this custom record be displayed in Expensify?',
},
errors: {
- uniqueFieldError: (fieldName: string) => `A custom segment/record with this ${fieldName?.toLowerCase()} already exists.`,
+ uniqueFieldError: ({fieldName}: RequiredFieldParams) => `A custom segment/record with this ${fieldName?.toLowerCase()} already exists.`,
},
},
customLists: {
@@ -2756,18 +2832,18 @@ export default {
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: {
label: 'NetSuite employee default',
description: 'Not imported into Expensify, applied on export',
- footerContent: (importField: string) =>
+ footerContent: ({importField}: ImportFieldParams) =>
`If you use ${importField} in NetSuite, we'll apply the default set on the employee record upon export to Expense Report or Journal Entry.`,
},
[CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: {
label: 'Tags',
description: 'Line-item level',
- footerContent: (importField: string) => `${startCase(importField)} will be selectable for each individual expense on an employee's report.`,
+ footerContent: ({importField}: ImportFieldParams) => `${startCase(importField)} will be selectable for each individual expense on an employee's report.`,
},
[CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: {
label: 'Report fields',
description: 'Report level',
- footerContent: (importField: string) => `${startCase(importField)} selection will apply to all expense on an employee's report.`,
+ footerContent: ({importField}: ImportFieldParams) => `${startCase(importField)} selection will apply to all expense on an employee's report.`,
},
},
},
@@ -2798,8 +2874,8 @@ export default {
addAUserDefinedDimension: 'Add a user-defined dimension',
detailedInstructionsLink: 'View detailed instructions',
detailedInstructionsRestOfSentence: ' on adding user-defined dimensions.',
- userDimensionsAdded: (dimensionsCount: number) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} added`,
- mappingTitle: (mappingName: SageIntacctMappingName): string => {
+ userDimensionsAdded: ({dimensionsCount}: DimensionsCountParams) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} added`,
+ mappingTitle: ({mappingName}: IntacctMappingTitleParams) => {
switch (mappingName) {
case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS:
return 'departments';
@@ -2833,7 +2909,7 @@ export default {
},
yourCardProvider: `Who's your card provider?`,
enableFeed: {
- title: (provider: string) => `Enable your ${provider} feed`,
+ 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.*`,
@@ -2880,7 +2956,7 @@ export default {
card: 'Card',
startTransactionDate: 'Start transaction date',
cardName: 'Card name',
- assignedYouCard: (assigner: string) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`,
+ assignedYouCard: ({assigner}: AssignedYouCardParams) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`,
chooseCardFeed: 'Choose card feed',
},
expensifyCard: {
@@ -2926,20 +3002,21 @@ export default {
deactivate: 'Deactivate card',
changeCardLimit: 'Change card limit',
changeLimit: 'Change limit',
- smartLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined until you approve more expenses on the card.`,
- monthlyLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined until next month.`,
- fixedLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined.`,
+ smartLimitWarning: ({limit}: CharacterLimitParams) =>
+ `If you change this card’s limit to ${limit}, new transactions will be declined until you approve more expenses on the card.`,
+ monthlyLimitWarning: ({limit}: CharacterLimitParams) => `If you change this card’s limit to ${limit}, new transactions will be declined until next month.`,
+ fixedLimitWarning: ({limit}: CharacterLimitParams) => `If you change this card’s limit to ${limit}, new transactions will be declined.`,
changeCardLimitType: 'Change card limit type',
changeLimitType: 'Change limit type',
- changeCardSmartLimitTypeWarning: (limit: string) =>
+ changeCardSmartLimitTypeWarning: ({limit}: CharacterLimitParams) =>
`If you change this card's limit type to Smart Limit, new transactions will be declined because the ${limit} unapproved limit has already been reached.`,
- changeCardMonthlyLimitTypeWarning: (limit: string) =>
+ changeCardMonthlyLimitTypeWarning: ({limit}: CharacterLimitParams) =>
`If you change this card's limit type to Monthly, new transactions will be declined because the ${limit} monthly limit has already been reached.`,
addShippingDetails: 'Add shipping details',
- issuedCard: (assignee: string) => `issued ${assignee} an Expensify Card! The card will arrive in 2-3 business days.`,
- issuedCardNoShippingDetails: (assignee: string) => `issued ${assignee} an Expensify Card! The card will be shipped once shipping details are added.`,
+ issuedCard: ({assignee}: AssigneeParams) => `issued ${assignee} an Expensify Card! The card will arrive in 2-3 business days.`,
+ issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `issued ${assignee} an Expensify Card! The card will be shipped once shipping details are added.`,
issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `issued ${assignee} a virtual ${link}! The card can be used right away.`,
- addedShippingDetails: (assignee: string) => `${assignee} added shipping details. Expensify Card will arrive in 2-3 business days.`,
+ addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} added shipping details. Expensify Card will arrive in 2-3 business days.`,
},
categories: {
deleteCategories: 'Delete categories',
@@ -3038,8 +3115,8 @@ export default {
cardNumber: 'Card number',
cardholder: 'Cardholder',
cardName: 'Card name',
- integrationExport: (integration: string, type: string) => `${integration} ${type} export`,
- integrationExportTitleFirstPart: (integration: string) => `Choose the ${integration} account where transactions should be exported. Select a different`,
+ integrationExport: ({integration, type}: IntegrationExportParams) => `${integration} ${type} export`,
+ integrationExportTitleFirstPart: ({integration}: IntegrationExportParams) => `Choose the ${integration} account where transactions should be exported. Select a different`,
integrationExportTitleLinkPart: 'export option',
integrationExportTitleSecondPart: 'to change the available accounts.',
lastUpdated: 'Last updated',
@@ -3072,7 +3149,7 @@ export default {
giveItNameInstruction: 'Give the card a name that sets it apart from the others.',
updating: 'Updating...',
noAccountsFound: 'No accounts found',
- noAccountsFoundDescription: (connection: string) => `Please add the account in ${connection} and sync the connection again.`,
+ noAccountsFoundDescription: ({connection}: ConnectionParams) => `Please add the account in ${connection} and sync the connection again.`,
},
workflows: {
title: 'Workflows',
@@ -3172,6 +3249,7 @@ export default {
disableTags: 'Disable tags',
addTag: 'Add tag',
editTag: 'Edit tag',
+ editTags: 'Edit tags',
subtitle: 'Tags add more detailed ways to classify costs.',
emptyTags: {
title: "You haven't created any tags",
@@ -3192,7 +3270,7 @@ export default {
tagRules: 'Tag rules',
approverDescription: 'Approver',
importTags: 'Import tags',
- importedTagsMessage: (columnCounts: number) =>
+ importedTagsMessage: ({columnCounts}: ImportedTagsMessageParams) =>
`We found *${columnCounts} columns* in your spreadsheet. Select *Name* next to the column that contains tags names. You can also select *Enabled* next to the column that sets tags status.`,
},
taxes: {
@@ -3215,7 +3293,7 @@ export default {
updateTaxClaimableFailureMessage: 'The reclaimable portion must be less than the distance rate amount.',
},
deleteTaxConfirmation: 'Are you sure you want to delete this tax?',
- deleteMultipleTaxConfirmation: ({taxAmount}) => `Are you sure you want to delete ${taxAmount} taxes?`,
+ deleteMultipleTaxConfirmation: ({taxAmount}: TaxAmountParams) => `Are you sure you want to delete ${taxAmount} taxes?`,
actions: {
delete: 'Delete rate',
deleteMultiple: 'Delete rates',
@@ -3258,7 +3336,7 @@ export default {
removeWorkspaceMemberButtonTitle: 'Remove from workspace',
removeGroupMemberButtonTitle: 'Remove from group',
removeRoomMemberButtonTitle: 'Remove from chat',
- removeMemberPrompt: ({memberName}: {memberName: string}) => `Are you sure you want to remove ${memberName}?`,
+ removeMemberPrompt: ({memberName}: RemoveMemberPromptParams) => `Are you sure you want to remove ${memberName}?`,
removeMemberTitle: 'Remove member',
transferOwner: 'Transfer owner',
makeMember: 'Make member',
@@ -3271,7 +3349,7 @@ export default {
genericRemove: 'There was a problem removing that workspace member.',
},
addedWithPrimary: 'Some members were added with their primary logins.',
- invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`,
+ invitedBySecondaryLogin: ({secondaryLogin}: SecondaryLoginParams) => `Added by secondary login ${secondaryLogin}.`,
membersListTitle: 'Directory of all workspace members.',
importMembers: 'Import members',
},
@@ -3319,8 +3397,8 @@ export default {
xero: 'Xero',
netsuite: 'NetSuite',
intacct: 'Sage Intacct',
- connectionName: (integration: ConnectionName) => {
- switch (integration) {
+ connectionName: ({connectionName}: ConnectionNameParams) => {
+ switch (connectionName) {
case CONST.POLICY.CONNECTIONS.NAME.QBO:
return 'Quickbooks Online';
case CONST.POLICY.CONNECTIONS.NAME.XERO:
@@ -3337,21 +3415,22 @@ export default {
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.',
setup: 'Connect',
- lastSync: (relativeDate: string) => `Last synced ${relativeDate}`,
+ lastSync: ({relativeDate}: LastSyncAccountingParams) => `Last synced ${relativeDate}`,
import: 'Import',
export: 'Export',
advanced: 'Advanced',
other: 'Other integrations',
syncNow: 'Sync now',
disconnect: 'Disconnect',
- disconnectTitle: (integration?: ConnectionName): string => {
- const integrationName = integration && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration] : 'integration';
+ disconnectTitle: ({connectionName}: OptionalParam = {}) => {
+ const integrationName =
+ connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'integration';
return `Disconnect ${integrationName}`;
},
- connectTitle: (integrationToConnect: ConnectionName): string => `Connect ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integrationToConnect] ?? 'accounting integration'}`,
+ connectTitle: ({connectionName}: ConnectionNameParams) => `Connect ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'accounting integration'}`,
- syncError: (integration?: ConnectionName): string => {
- switch (integration) {
+ syncError: ({connectionName}: ConnectionNameParams) => {
+ switch (connectionName) {
case CONST.POLICY.CONNECTIONS.NAME.QBO:
return "Can't connect to QuickBooks Online.";
case CONST.POLICY.CONNECTIONS.NAME.XERO:
@@ -3377,20 +3456,18 @@ export default {
[CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Imported as report fields',
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite employee default',
},
- disconnectPrompt: (currentIntegration?: ConnectionName): string => {
+ disconnectPrompt: ({connectionName}: OptionalParam = {}) => {
const integrationName =
- currentIntegration && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[currentIntegration]
- ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[currentIntegration]
- : 'this integration';
+ connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'this integration';
return `Are you sure you want to disconnect ${integrationName}?`;
},
- connectPrompt: (integrationToConnect: ConnectionName): string =>
+ connectPrompt: ({connectionName}: ConnectionNameParams) =>
`Are you sure you want to connect ${
- CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integrationToConnect] ?? 'this accounting integration'
+ CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'this accounting integration'
}? This will remove any existing acounting connections.`,
enterCredentials: 'Enter your credentials',
connections: {
- syncStageName: (stage: PolicyConnectionSyncStage) => {
+ syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
case 'quickbooksOnlineImportCustomers':
return 'Importing customers';
@@ -3527,7 +3604,7 @@ export default {
chooseBankAccount: 'Choose the bank account that your Expensify Card payments will be reconciled against.',
accountMatches: 'Make sure this account matches your ',
settlementAccount: 'Expensify Card settlement account ',
- reconciliationWorks: (lastFourPAN: string) => `(ending in ${lastFourPAN}) so Continuous Reconciliation works properly.`,
+ reconciliationWorks: ({lastFourPAN}: ReconciliationWorksParams) => `(ending in ${lastFourPAN}) so Continuous Reconciliation works properly.`,
},
},
export: {
@@ -3587,7 +3664,10 @@ export default {
rate: 'Rate',
addRate: 'Add rate',
trackTax: 'Track tax',
- deleteRates: ({count}: DistanceRateOperationsParams) => `Delete ${Str.pluralize('rate', 'rates', count)}`,
+ deleteRates: () => ({
+ one: 'Delete rate',
+ other: 'Delete rates',
+ }),
enableRates: ({count}: DistanceRateOperationsParams) => `Enable ${Str.pluralize('rate', 'rates', count)}`,
disableRates: ({count}: DistanceRateOperationsParams) => `Disable ${Str.pluralize('rate', 'rates', count)}`,
enableRate: 'Enable rate',
@@ -3656,19 +3736,19 @@ export default {
amountOwedText: 'This account has an outstanding balance from a previous month.\n\nDo you want to clear the balance and take over billing of this workspace?',
ownerOwesAmountTitle: 'Outstanding balance',
ownerOwesAmountButtonText: 'Transfer balance',
- ownerOwesAmountText: ({email, amount}) =>
+ ownerOwesAmountText: ({email, amount}: OwnerOwesAmountParams) =>
`The account owning this workspace (${email}) has an outstanding balance from a previous month.\n\nDo you want to transfer this amount (${amount}) in order to take over billing for this workspace? Your payment card will be charged immediately.`,
subscriptionTitle: 'Take over annual subscription',
subscriptionButtonText: 'Transfer subscription',
- subscriptionText: ({usersCount, finalCount}) =>
+ subscriptionText: ({usersCount, finalCount}: ChangeOwnerSubscriptionParams) =>
`Taking over this workspace will merge its annual subscription with your current subscription. This will increase your subscription size by ${usersCount} members making your new subscription size ${finalCount}. Would you like to continue?`,
duplicateSubscriptionTitle: 'Duplicate subscription alert',
duplicateSubscriptionButtonText: 'Continue',
- duplicateSubscriptionText: ({email, workspaceName}) =>
+ duplicateSubscriptionText: ({email, workspaceName}: ChangeOwnerDuplicateSubscriptionParams) =>
`It looks like you may be trying to take over billing for ${email}'s workspaces, but to do that, you need to be an admin on all their workspaces first.\n\nClick "Continue" if you only want to take over billing for the workspace ${workspaceName}.\n\nIf you want to take over billing for their entire subscription, please have them add you as an admin to all their workspaces first before taking over billing.`,
hasFailedSettlementsTitle: 'Cannot transfer ownership',
hasFailedSettlementsButtonText: 'Got it',
- hasFailedSettlementsText: ({email}) =>
+ hasFailedSettlementsText: ({email}: ChangeOwnerHasFailedSettlementsParams) =>
`You can't take over billing because ${email} has an overdue expensify Expensify Card settlement. Please ask them to reach out to concierge@expensify.com to resolve the issue. Then, you can take over billing for this workspace.`,
failedToClearBalanceTitle: 'Failed to clear balance',
failedToClearBalanceButtonText: 'OK',
@@ -3682,7 +3762,7 @@ export default {
},
exportAgainModal: {
title: 'Careful!',
- description: (reportName: string, connectionName: ConnectionName) =>
+ description: ({reportName, connectionName}: ExportAgainModalDescriptionParams) =>
`The following reports have already been exported to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}:\n\n${reportName}\n\nAre you sure you want to export them again?`,
confirmText: 'Yes, export again',
cancelText: 'Cancel',
@@ -3745,7 +3825,7 @@ export default {
upgradeToUnlock: 'Unlock this feature',
completed: {
headline: `You've upgraded your workspace!`,
- successMessage: (policyName: string) => `You've successfully upgraded your ${policyName} workspace to the Control plan!`,
+ successMessage: ({policyName}: ReportPolicyNameParams) => `You've successfully upgraded your ${policyName} workspace to the Control plan!`,
viewSubscription: 'View your subscription',
moreDetails: 'for more details.',
gotIt: 'Got it, thanks',
@@ -3753,8 +3833,8 @@ export default {
},
restrictedAction: {
restricted: 'Restricted',
- actionsAreCurrentlyRestricted: ({workspaceName}) => `Actions on the ${workspaceName} workspace are currently restricted`,
- workspaceOwnerWillNeedToAddOrUpdatePaymentCard: ({workspaceOwnerName}) =>
+ actionsAreCurrentlyRestricted: ({workspaceName}: ActionsAreCurrentlyRestricted) => `Actions on the ${workspaceName} workspace are currently restricted`,
+ workspaceOwnerWillNeedToAddOrUpdatePaymentCard: ({workspaceOwnerName}: WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams) =>
`Workspace owner, ${workspaceOwnerName} will need to add or update the payment card on file to unlock new workspace activity.`,
youWillNeedToAddOrUpdatePaymentCard: "You'll need to add or update the payment card on file to unlock new workspace activity.",
addPaymentCardToUnlock: 'Add a payment card to unlock!',
@@ -3775,7 +3855,7 @@ export default {
maxAge: 'Max age',
maxExpenseAge: 'Max expense age',
maxExpenseAgeDescription: 'Flag spend older than a specific number of days.',
- maxExpenseAgeDays: (age: number) => `${age} ${Str.pluralize('day', 'days', age)}`,
+ maxExpenseAgeDays: ({age}: AgeParams) => `${age} ${Str.pluralize('day', 'days', age)}`,
billableDefault: 'Billable default',
billableDefaultDescription: 'Choose whether cash and credit card expenses should be billable by default. Billable expenses are enabled or disabled in',
billable: 'Billable',
@@ -3812,26 +3892,26 @@ export default {
randomReportAuditDescription: 'Require that some reports be manually approved, even if eligible for auto-approval.',
autoPayApprovedReportsTitle: 'Auto-pay approved reports',
autoPayApprovedReportsSubtitle: 'Configure which expense reports are eligible for auto-pay.',
- autoPayApprovedReportsLimitError: (currency?: string) => `Please enter an amount less than ${currency ?? ''}20,000`,
+ autoPayApprovedReportsLimitError: ({currency}: AutoPayApprovedReportsLimitErrorParams = {}) => `Please enter an amount less than ${currency ?? ''}20,000`,
autoPayApprovedReportsLockedSubtitle: 'Go to more features and enable workflows, then add payments to unlock this feature.',
autoPayReportsUnderTitle: 'Auto-pay reports under',
autoPayReportsUnderDescription: 'Fully compliant expense reports under this amount will be automatically paid. ',
unlockFeatureGoToSubtitle: 'Go to',
- unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `and enable workflows, then add ${featureName} to unlock this feature.`,
- enableFeatureSubtitle: (featureName: string) => `and enable ${featureName} to unlock this feature.`,
+ unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `and enable workflows, then add ${featureName} to unlock this feature.`,
+ enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `and enable ${featureName} to unlock this feature.`,
},
categoryRules: {
title: 'Category rules',
approver: 'Approver',
requireDescription: 'Require description',
descriptionHint: 'Description hint',
- descriptionHintDescription: (categoryName: string) =>
+ descriptionHintDescription: ({categoryName}: CategoryNameParams) =>
`Remind employees to provide additional information for “${categoryName}” spend. This hint appears in the description field on expenses.`,
descriptionHintLabel: 'Hint',
descriptionHintSubtitle: 'Pro-tip: The shorter the better!',
maxAmount: 'Max amount',
flagAmountsOver: 'Flag amounts over',
- flagAmountsOverDescription: (categoryName) => `Applies to the category “${categoryName}”.`,
+ flagAmountsOverDescription: ({categoryName}: CategoryNameParams) => `Applies to the category “${categoryName}”.`,
flagAmountsOverSubtitle: 'This overrides the max amount for all expenses.',
expenseLimitTypes: {
expense: 'Individual expense',
@@ -3841,7 +3921,7 @@ export default {
},
requireReceiptsOver: 'Require receipts over',
requireReceiptsOverList: {
- default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Default`,
+ default: ({defaultAmount}: DefaultAmountParams) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Default`,
never: 'Never require receipts',
always: 'Always require receipts',
},
@@ -3904,8 +3984,8 @@ export default {
},
},
workspaceActions: {
- renamedWorkspaceNameAction: ({oldName, newName}) => `updated the name of this workspace from ${oldName} to ${newName}`,
- removedFromApprovalWorkflow: ({submittersNames}: {submittersNames: string[]}) => {
+ renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `updated the name of this workspace from ${oldName} to ${newName}`,
+ removedFromApprovalWorkflow: ({submittersNames}: RemovedFromApprovalWorkflowParams) => {
let joinedNames = '';
if (submittersNames.length === 1) {
joinedNames = submittersNames[0];
@@ -3958,7 +4038,7 @@ export default {
deleteConfirmation: 'Are you sure you want to delete this task?',
},
statementPage: {
- title: (year, monthName) => `${monthName} ${year} statement`,
+ title: ({year, monthName}: StatementTitleParams) => `${monthName} ${year} statement`,
generatingPDF: "We're generating your PDF right now. Please check back soon!",
},
keyboardShortcutsPage: {
@@ -4008,8 +4088,8 @@ export default {
filtersHeader: 'Filters',
filters: {
date: {
- before: (date?: string) => `Before ${date ?? ''}`,
- after: (date?: string) => `After ${date ?? ''}`,
+ before: ({date}: OptionalParam = {}) => `Before ${date ?? ''}`,
+ after: ({date}: OptionalParam = {}) => `After ${date ?? ''}`,
},
status: 'Status',
keyword: 'Keyword',
@@ -4019,9 +4099,9 @@ export default {
pinned: 'Pinned',
unread: 'Unread',
amount: {
- lessThan: (amount?: string) => `Less than ${amount ?? ''}`,
- greaterThan: (amount?: string) => `Greater than ${amount ?? ''}`,
- between: (greaterThan: string, lessThan: string) => `Between ${greaterThan} and ${lessThan}`,
+ lessThan: ({amount}: OptionalParam = {}) => `Less than ${amount ?? ''}`,
+ greaterThan: ({amount}: OptionalParam = {}) => `Greater than ${amount ?? ''}`,
+ between: ({greaterThan, lessThan}: FiltersAmountBetweenParams) => `Between ${greaterThan} and ${lessThan}`,
},
current: 'Current',
past: 'Past',
@@ -4141,7 +4221,7 @@ export default {
nonReimbursableLink: 'View company card expenses.',
pending: ({label}: ExportedToIntegrationParams) => `started exporting this report to ${label}...`,
},
- integrationsMessage: (errorMessage: string, label: string) => `failed to export this report to ${label} ("${errorMessage}").`,
+ integrationsMessage: ({errorMessage, label}: IntegrationSyncFailedParams) => `failed to export this report to ${label} ("${errorMessage}").`,
managerAttachReceipt: `added a receipt`,
managerDetachReceipt: `removed a receipt`,
markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `paid ${currency}${amount} elsewhere`,
@@ -4158,10 +4238,10 @@ export default {
stripePaid: ({amount, currency}: StripePaidParams) => `paid ${currency}${amount}`,
takeControl: `took control`,
unapproved: ({amount, currency}: UnapprovedParams) => `unapproved ${currency}${amount}`,
- integrationSyncFailed: (label: string, errorMessage: string) => `failed to sync with ${label} ("${errorMessage}")`,
- addEmployee: (email: string, role: string) => `added ${email} as ${role === 'user' ? 'member' : 'admin'}`,
- updateRole: (email: string, currentRole: string, newRole: string) => `updated the role of ${email} from ${currentRole} to ${newRole}`,
- removeMember: (email: string, role: string) => `removed ${role} ${email}`,
+ integrationSyncFailed: ({label, errorMessage}: IntegrationSyncFailedParams) => `failed to sync with ${label} ("${errorMessage}")`,
+ addEmployee: ({email, role}: AddEmployeeParams) => `added ${email} as ${role === 'user' ? 'member' : 'admin'}`,
+ updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `updated the role of ${email} from ${currentRole} to ${newRole}`,
+ removeMember: ({email, role}: AddEmployeeParams) => `removed ${role} ${email}`,
},
},
},
@@ -4382,7 +4462,7 @@ export default {
allTagLevelsRequired: 'All tags required',
autoReportedRejectedExpense: ({rejectReason, rejectedBy}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`,
billableExpense: 'Billable no longer valid',
- cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams) => `Receipt required${formattedLimit ? ` over ${formattedLimit}` : ''}`,
+ cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams = {}) => `Receipt required${formattedLimit ? ` over ${formattedLimit}` : ''}`,
categoryOutOfPolicy: 'Category no longer valid',
conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `Applied ${surcharge}% conversion surcharge`,
customUnitOutOfPolicy: 'Rate not valid for this workspace',
@@ -4393,8 +4473,8 @@ export default {
maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`,
missingCategory: 'Missing category',
missingComment: 'Description required for selected category',
- missingTag: ({tagName}: ViolationsMissingTagParams) => `Missing ${tagName ?? 'tag'}`,
- modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams): string => {
+ missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Missing ${tagName ?? 'tag'}`,
+ modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => {
switch (type) {
case 'distance':
return 'Amount differs from calculated distance';
@@ -4442,10 +4522,10 @@ export default {
return '';
},
smartscanFailed: 'Receipt scanning failed. Enter details manually.',
- someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams) => `Missing ${tagName ?? 'Tag'}`,
- tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? 'Tag'} no longer valid`,
+ someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `Missing ${tagName ?? 'Tag'}`,
+ tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `${tagName ?? 'Tag'} no longer valid`,
taxAmountChanged: 'Tax amount was modified',
- taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'Tax'} no longer valid`,
+ taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams = {}) => `${taxName ?? 'Tax'} no longer valid`,
taxRateChanged: 'Tax rate was modified',
taxRequired: 'Missing tax rate',
none: 'None',
@@ -4462,7 +4542,7 @@ export default {
hold: 'Hold',
},
reportViolations: {
- [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is required`,
+ [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} is required`,
},
violationDismissal: {
rter: {
@@ -4517,12 +4597,12 @@ export default {
authenticatePaymentCard: 'Authenticate payment card',
mobileReducedFunctionalityMessage: 'You can’t make changes to your subscription in the mobile app.',
badge: {
- freeTrial: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`,
+ freeTrial: ({numOfDays}: BadgeFreeTrialParams) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`,
},
billingBanner: {
policyOwnerAmountOwed: {
title: 'Your payment info is outdated',
- subtitle: ({date}) => `Update your payment card by ${date} to continue using all of your favorite features.`,
+ subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Update your payment card by ${date} to continue using all of your favorite features.`,
},
policyOwnerAmountOwedOverdue: {
title: 'Your payment info is outdated',
@@ -4530,7 +4610,7 @@ export default {
},
policyOwnerUnderInvoicing: {
title: 'Your payment info is outdated',
- subtitle: ({date}) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption.`,
+ subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption.`,
},
policyOwnerUnderInvoicingOverdue: {
title: 'Your payment info is outdated',
@@ -4538,22 +4618,22 @@ export default {
},
billingDisputePending: {
title: 'Your card couldn’t be charged',
- subtitle: ({amountOwed, cardEnding}) =>
+ subtitle: ({amountOwed, cardEnding}: BillingBannerDisputePendingParams) =>
`You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`,
},
cardAuthenticationRequired: {
title: 'Your card couldn’t be charged',
- subtitle: ({cardEnding}) =>
+ subtitle: ({cardEnding}: BillingBannerCardAuthenticationRequiredParams) =>
`Your payment card hasn’t been fully authenticated. Please complete the authentication process to activate your payment card ending in ${cardEnding}.`,
},
insufficientFunds: {
title: 'Your card couldn’t be charged',
- subtitle: ({amountOwed}) =>
+ subtitle: ({amountOwed}: BillingBannerInsufficientFundsParams) =>
`Your payment card was declined due to insufficient funds. Please retry or add a new payment card to clear your ${amountOwed} outstanding balance.`,
},
cardExpired: {
title: 'Your card couldn’t be charged',
- subtitle: ({amountOwed}) => `Your payment card expired. Please add a new payment card to clear your ${amountOwed} outstanding balance.`,
+ subtitle: ({amountOwed}: BillingBannerCardExpiredParams) => `Your payment card expired. Please add a new payment card to clear your ${amountOwed} outstanding balance.`,
},
cardExpireSoon: {
title: 'Your card is expiring soon',
@@ -4567,7 +4647,7 @@ export default {
title: 'Your card couldn’t be charged',
subtitle: 'Before retrying, please call your bank directly to authorize Expensify charges and remove any holds. Otherwise, try adding a different payment card.',
},
- cardOnDispute: ({amountOwed, cardEnding}) =>
+ cardOnDispute: ({amountOwed, cardEnding}: BillingBannerCardOnDisputeParams) =>
`You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`,
preTrial: {
title: 'Start a free trial',
@@ -4576,7 +4656,7 @@ export default {
subtitleEnd: 'so your team can start expensing.',
},
trialStarted: {
- title: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left!`,
+ title: ({numOfDays}: TrialStartedTitleParams) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left!`,
subtitle: 'Add a payment card to continue using all of your favorite features.',
},
trialEnded: {
@@ -4588,9 +4668,9 @@ export default {
title: 'Payment',
subtitle: 'Add a card to pay for your Expensify subscription.',
addCardButton: 'Add payment card',
- cardNextPayment: ({nextPaymentDate}) => `Your next payment date is ${nextPaymentDate}.`,
- cardEnding: ({cardNumber}) => `Card ending in ${cardNumber}`,
- cardInfo: ({name, expiration, currency}) => `Name: ${name}, Expiration: ${expiration}, Currency: ${currency}`,
+ cardNextPayment: ({nextPaymentDate}: CardNextPaymentParams) => `Your next payment date is ${nextPaymentDate}.`,
+ cardEnding: ({cardNumber}: CardEndingParams) => `Card ending in ${cardNumber}`,
+ cardInfo: ({name, expiration, currency}: CardInfoParams) => `Name: ${name}, Expiration: ${expiration}, Currency: ${currency}`,
changeCard: 'Change payment card',
changeCurrency: 'Change payment currency',
cardNotFound: 'No payment card added',
@@ -4609,8 +4689,8 @@ export default {
title: 'Your plan',
collect: {
title: 'Collect',
- priceAnnual: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
- pricePayPerUse: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
+ priceAnnual: ({lower, upper}: YourPlanPriceParams) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
+ pricePayPerUse: ({lower, upper}: YourPlanPriceParams) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
benefit1: 'Unlimited SmartScans and distance tracking',
benefit2: 'Expensify Cards with Smart Limits',
benefit3: 'Bill pay and invoicing',
@@ -4621,8 +4701,8 @@ export default {
},
control: {
title: 'Control',
- priceAnnual: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
- pricePayPerUse: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
+ priceAnnual: ({lower, upper}: YourPlanPriceParams) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
+ pricePayPerUse: ({lower, upper}: YourPlanPriceParams) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
benefit1: 'Everything in Collect, plus:',
benefit2: 'NetSuite and Sage Intacct integrations',
benefit3: 'Certinia and Workday sync',
@@ -4653,10 +4733,10 @@ export default {
note: 'Note: An active member is anyone who has created, edited, submitted, approved, reimbursed, or exported expense data tied to your company workspace.',
confirmDetails: 'Confirm your new annual subscription details:',
subscriptionSize: 'Subscription size',
- activeMembers: ({size}) => `${size} active members/month`,
+ activeMembers: ({size}: SubscriptionSizeParams) => `${size} active members/month`,
subscriptionRenews: 'Subscription renews',
youCantDowngrade: 'You can’t downgrade during your annual subscription.',
- youAlreadyCommitted: ({size, date}) =>
+ youAlreadyCommitted: ({size, date}: SubscriptionCommitmentParams) =>
`You already committed to an annual subscription size of ${size} active members per month until ${date}. You can switch to a pay-per-use subscription on ${date} by disabling auto-renew.`,
error: {
size: 'Please enter a valid subscription size.',
@@ -4673,13 +4753,13 @@ export default {
title: 'Subscription settings',
autoRenew: 'Auto-renew',
autoIncrease: 'Auto-increase annual seats',
- saveUpTo: ({amountWithCurrency}) => `Save up to ${amountWithCurrency}/month per active member`,
+ saveUpTo: ({amountWithCurrency}: SubscriptionSettingsSaveUpToParams) => `Save up to ${amountWithCurrency}/month per active member`,
automaticallyIncrease:
'Automatically increase your annual seats to accommodate for active members that exceed your subscription size. Note: This will extend your annual subscription end date.',
disableAutoRenew: 'Disable auto-renew',
helpUsImprove: 'Help us improve Expensify',
whatsMainReason: "What's the main reason you're disabling auto-renew?",
- renewsOn: ({date}) => `Renews on ${date}.`,
+ renewsOn: ({date}: SubscriptionSettingsRenewsOnParams) => `Renews on ${date}.`,
},
requestEarlyCancellation: {
title: 'Request early cancellation',
@@ -4728,7 +4808,7 @@ export default {
addCopilot: 'Add copilot',
membersCanAccessYourAccount: 'These members can access your account:',
youCanAccessTheseAccounts: 'You can access these accounts via the account switcher:',
- role: (role?: string): string => {
+ role: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.DELEGATE_ROLE.ALL:
return 'Full';
@@ -4739,10 +4819,11 @@ export default {
}
},
genericError: 'Oops, something went wrong. Please try again.',
+ onBehalfOfMessage: ({delegator}: DelegatorParams) => `on behalf of ${delegator}`,
accessLevel: 'Access level',
confirmCopilot: 'Confirm your copilot below.',
accessLevelDescription: 'Choose an access level below. Both Full and Limited access allow copilots to view all conversations and expenses.',
- roleDescription: (role?: string): string => {
+ roleDescription: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.DELEGATE_ROLE.ALL:
return 'Allow another member to take all actions in your account, on your behalf. Includes chat, submissions, approvals, payments, settings updates, and more.';
@@ -4768,9 +4849,9 @@ export default {
nothingToPreview: 'Nothing to preview',
editJson: 'Edit JSON:',
preview: 'Preview:',
- missingProperty: ({propertyName}) => `Missing ${propertyName}`,
- invalidProperty: ({propertyName, expectedType}) => `Invalid property: ${propertyName} - Expected: ${expectedType}`,
- invalidValue: ({expectedValues}) => `Invalid value - Expected: ${expectedValues}`,
+ missingProperty: ({propertyName}: MissingPropertyParams) => `Missing ${propertyName}`,
+ invalidProperty: ({propertyName, expectedType}: InvalidPropertyParams) => `Invalid property: ${propertyName} - Expected: ${expectedType}`,
+ invalidValue: ({expectedValues}: InvalidValueParams) => `Invalid value - Expected: ${expectedValues}`,
missingValue: 'Missing value',
createReportAction: 'Create Report Action',
reportAction: 'Report Action',
@@ -4785,4 +4866,6 @@ export default {
time: 'Time',
none: 'None',
},
-} satisfies TranslationBase;
+};
+
+export default translations satisfies TranslationDeepObject;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 362fe5575dd0..8d8b33ca57dd 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1,45 +1,91 @@
import {Str} from 'expensify-common';
import CONST from '@src/CONST';
-import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy';
+import type en from './en';
import type {
AccountOwnerParams,
+ ActionsAreCurrentlyRestricted,
+ AddEmployeeParams,
AddressLineParams,
AdminCanceledRequestParams,
+ AgeParams,
AlreadySignedInParams,
ApprovalWorkflowErrorParams,
ApprovedAmountParams,
AssignCardParams,
+ AssignedYouCardParams,
+ AssigneeParams,
+ AuthenticationErrorParams,
+ AutoPayApprovedReportsLimitErrorParams,
+ BadgeFreeTrialParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartTwo,
BeginningOfChatHistoryDomainRoomPartOneParams,
+ BillingBannerCardAuthenticationRequiredParams,
+ BillingBannerCardExpiredParams,
+ BillingBannerCardOnDisputeParams,
+ BillingBannerDisputePendingParams,
+ BillingBannerInsufficientFundsParams,
+ BillingBannerSubtitleWithDateParams,
CanceledRequestParams,
+ CardEndingParams,
+ CardInfoParams,
+ CardNextPaymentParams,
+ CategoryNameParams,
ChangeFieldParams,
+ ChangeOwnerDuplicateSubscriptionParams,
+ ChangeOwnerHasFailedSettlementsParams,
+ ChangeOwnerSubscriptionParams,
ChangePolicyParams,
ChangeTypeParams,
+ CharacterLengthLimitParams,
CharacterLimitParams,
CompanyCardFeedNameParams,
ConfirmHoldExpenseParams,
ConfirmThatParams,
+ ConnectionNameParams,
+ ConnectionParams,
+ CustomersOrJobsLabelParams,
+ DateParams,
DateShouldBeAfterParams,
DateShouldBeBeforeParams,
+ DefaultAmountParams,
+ DefaultVendorDescriptionParams,
+ DelegateRoleParams,
DelegateSubmitParams,
+ DelegatorParams,
DeleteActionParams,
DeleteConfirmationParams,
DeleteExpenseTranslationParams,
DidSplitAmountMessageParams,
+ DimensionsCountParams,
DistanceRateOperationsParams,
EditActionParams,
ElectronicFundsParams,
- EnglishTranslation,
EnterMagicCodeParams,
+ ExportAgainModalDescriptionParams,
ExportedToIntegrationParams,
+ ExportIntegrationSelectedParams,
+ FeatureNameParams,
+ FiltersAmountBetweenParams,
FormattedMaxLengthParams,
ForwardedAmountParams,
GoBackMessageParams,
GoToRoomParams,
+ ImportedTagsMessageParams,
+ ImportFieldParams,
+ ImportMembersSuccessfullDescriptionParams,
+ ImportTagsSuccessfullDescriptionParams,
+ IncorrectZipFormatParams,
InstantSummaryParams,
+ IntacctMappingTitleParams,
+ IntegrationExportParams,
+ IntegrationSyncFailedParams,
+ InvalidPropertyParams,
+ InvalidValueParams,
IssueVirtualCardParams,
+ LastSyncAccountingParams,
+ LastSyncDateParams,
LocalTimeParams,
LoggedInAsParams,
LogSizeParams,
@@ -47,12 +93,15 @@ import type {
ManagerApprovedParams,
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
+ MissingPropertyParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
OOOEventSummaryFullDayParams,
OOOEventSummaryPartialDayParams,
+ OptionalParam,
OurEmailProviderParams,
+ OwnerOwesAmountParams,
PaidElsewhereWithAmountParams,
PaidWithExpensifyWithAmountParams,
ParentNavigationSummaryParams,
@@ -62,21 +111,28 @@ import type {
PayerPaidParams,
PayerSettledParams,
PaySomeoneParams,
+ ReconciliationWorksParams,
ReimbursementRateParams,
+ RemovedFromApprovalWorkflowParams,
RemovedTheRequestParams,
+ RemoveMemberPromptParams,
RemoveMembersWarningPrompt,
RenamedRoomActionParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
ReportArchiveReasonsMergedParams,
- ReportArchiveReasonsPolicyDeletedParams,
ReportArchiveReasonsRemovedFromPolicyParams,
+ ReportPolicyNameParams,
RequestAmountParams,
RequestCountParams,
RequestedAmountMessageParams,
+ RequiredFieldParams,
ResolutionConstraintsParams,
+ RoleNamesParams,
RoomNameReservedErrorParams,
RoomRenamedToParams,
+ SecondaryLoginParams,
+ SelectedNumberParams,
SetTheDistanceMerchantParams,
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
@@ -85,19 +141,32 @@ import type {
SignUpNewFaceCodeParams,
SizeExceededParams,
SplitAmountParams,
+ SpreadCategoriesParams,
+ SpreadFieldNameParams,
+ SpreadSheetColumnParams,
+ StatementTitleParams,
StepCounterParams,
StripePaidParams,
+ SubscriptionCommitmentParams,
+ SubscriptionSettingsRenewsOnParams,
+ SubscriptionSettingsSaveUpToParams,
+ SubscriptionSizeParams,
+ SyncStageNameConnectionsParams,
TaskCreatedActionParams,
+ TaxAmountParams,
TermsParams,
ThreadRequestReportNameParams,
ThreadSentMoneyReportNameParams,
ToValidateLoginParams,
TransferParams,
+ TrialStartedTitleParams,
UnapprovedParams,
+ UnapproveWithIntegrationWarningParams,
UnshareParams,
UntilTimeParams,
UpdatedTheDistanceMerchantParams,
UpdatedTheRequestParams,
+ UpdateRoleParams,
UsePlusButtonParams,
UserIsAlreadyMemberParams,
UserSplitParams,
@@ -122,11 +191,14 @@ import type {
WelcomeNoteParams,
WelcomeToRoomParams,
WeSentYouMagicSignInLinkParams,
+ WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams,
+ YourPlanPriceParams,
ZipCodeExampleFormatParams,
-} from './types';
+} from './params';
+import type {TranslationDeepObject} from './types';
/* eslint-disable max-len */
-export default {
+const translations = {
common: {
cancel: 'Cancelar',
dismiss: 'Descartar',
@@ -254,7 +326,7 @@ export default {
fieldRequired: 'Este campo es obligatorio.',
requestModified: 'Esta solicitud está siendo modificada por otro miembro.',
characterLimit: ({limit}: CharacterLimitParams) => `Supera el límite de ${limit} caracteres`,
- characterLimitExceedCounter: ({length, limit}) => `Se superó el límite de caracteres (${length}/${limit})`,
+ characterLimitExceedCounter: ({length, limit}: CharacterLengthLimitParams) => `Se superó el límite de caracteres (${length}/${limit})`,
dateInvalid: 'Por favor, selecciona una fecha válida.',
invalidDateShouldBeFuture: 'Por favor, elige una fecha igual o posterior a hoy.',
invalidTimeShouldBeFuture: 'Por favor, elige una hora al menos un minuto en el futuro.',
@@ -636,7 +708,7 @@ export default {
shouldUseYou
? `Este chat ya no está activo porque tu ya no eres miembro del espacio de trabajo ${policyName}.`
: `Este chat está desactivado porque ${displayName} ha dejado de ser miembro del espacio de trabajo ${policyName}.`,
- [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsPolicyDeletedParams) =>
+ [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) =>
`Este chat está desactivado porque el espacio de trabajo ${policyName} se ha eliminado.`,
[CONST.REPORT.ARCHIVE_REASON.INVOICE_RECEIVER_POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) =>
`Este chat está desactivado porque el espacio de trabajo ${policyName} se ha eliminado.`,
@@ -677,14 +749,14 @@ export default {
dragAndDrop: 'Arrastra y suelta un archivo de hoja de cálculo aquí',
chooseSpreadsheet: 'Subir',
fileContainsHeader: 'El archivo contiene encabezados',
- column: (name: string) => `Columna ${name}`,
- fieldNotMapped: (fieldName: string) => `¡Vaya! Un campo obligatorio ("${fieldName}") no ha sido mapeado. Por favor, revisa e inténtalo de nuevo.`,
- singleFieldMultipleColumns: (fieldName: string) => `¡Vaya! Has mapeado un solo campo ("${fieldName}") a varias columnas. Por favor, revisa e inténtalo de nuevo.`,
+ column: ({name}: SpreadSheetColumnParams) => `Columna ${name}`,
+ fieldNotMapped: ({fieldName}: SpreadFieldNameParams) => `¡Vaya! Un campo obligatorio ("${fieldName}") no ha sido mapeado. Por favor, revisa e inténtalo de nuevo.`,
+ singleFieldMultipleColumns: ({fieldName}: SpreadFieldNameParams) => `¡Vaya! Has mapeado un solo campo ("${fieldName}") a varias columnas. Por favor, revisa e inténtalo de nuevo.`,
importFailedTitle: 'Fallo en la importación',
importFailedDescription: 'Por favor, asegúrate de que todos los campos estén llenos correctamente e inténtalo de nuevo. Si el problema persiste, por favor contacta a Concierge.',
- importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'),
- importMembersSuccessfullDescription: (members: number) => (members > 1 ? `Se han agregado ${members} miembros.` : 'Se ha agregado 1 miembro.'),
- importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'),
+ importCategoriesSuccessfullDescription: ({categories}: SpreadCategoriesParams) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'),
+ importMembersSuccessfullDescription: ({members}: ImportMembersSuccessfullDescriptionParams) => (members > 1 ? `Se han agregado ${members} miembros.` : 'Se ha agregado 1 miembro.'),
+ importTagsSuccessfullDescription: ({tags}: ImportTagsSuccessfullDescriptionParams) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'),
importSuccessfullTitle: 'Importar categorías',
importDescription: 'Elige qué campos mapear desde tu hoja de cálculo haciendo clic en el menú desplegable junto a cada columna importada a continuación.',
sizeNotMet: 'El archivo adjunto debe ser más grande que 0 bytes.',
@@ -722,7 +794,7 @@ export default {
splitBill: 'Dividir gasto',
splitScan: 'Dividir recibo',
splitDistance: 'Dividir distancia',
- paySomeone: (name: string) => `Pagar a ${name ?? 'alguien'}`,
+ paySomeone: ({name}: PaySomeoneParams = {}) => `Pagar a ${name ?? 'alguien'}`,
assignTask: 'Assignar tarea',
header: 'Acción rápida',
trackManual: 'Crear gasto',
@@ -751,7 +823,7 @@ export default {
share: 'Compartir',
participants: 'Participantes',
submitExpense: 'Presentar gasto',
- paySomeone: ({name}: PaySomeoneParams) => `Pagar a ${name ?? 'alguien'}`,
+ paySomeone: ({name}: PaySomeoneParams = {}) => `Pagar a ${name ?? 'alguien'}`,
trackExpense: 'Seguimiento de gastos',
pay: 'Pagar',
cancelPayment: 'Cancelar el pago',
@@ -765,7 +837,7 @@ export default {
pendingMatchWithCreditCardDescription: 'Recibo pendiente de adjuntar con la transacción de la tarjeta. Márcalo como efectivo para cancelar.',
markAsCash: 'Marcar como efectivo',
routePending: 'Ruta pendiente...',
- receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`,
+ receiptIssuesFound: ({count}: DistanceRateOperationsParams) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`,
fieldPending: 'Pendiente...',
receiptScanning: 'Escaneando recibo...',
receiptScanInProgress: 'Escaneado de recibo en proceso',
@@ -812,6 +884,8 @@ export default {
sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`,
submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`,
submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicitó ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
+ automaticallySubmittedAmount: ({formattedAmount}: RequestedAmountMessageParams) =>
+ `se enviaron automáticamente ${formattedAmount} mediante envío diferido `,
trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`,
didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
@@ -820,7 +894,7 @@ export default {
payerOwes: ({payer}: PayerOwesParams) => `${payer} debe: `,
payerPaidAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer ? `${payer} ` : ''}pagó ${amount}`,
payerPaid: ({payer}: PayerPaidParams) => `${payer} pagó: `,
- payerSpentAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer} gastó ${amount}`,
+ payerSpentAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer} gastó ${amount}`,
payerSpent: ({payer}: PayerPaidParams) => `${payer} gastó: `,
managerApproved: ({manager}: ManagerApprovedParams) => `${manager} aprobó:`,
managerApprovedAmount: ({manager, amount}: ManagerApprovedAmountParams) => `${manager} aprobó ${amount}`,
@@ -928,7 +1002,7 @@ export default {
unapprove: 'Desaprobar',
unapproveReport: 'Anular la aprobación del informe',
headsUp: 'Atención!',
- unapproveWithIntegrationWarning: (accountingIntegration: string) =>
+ unapproveWithIntegrationWarning: ({accountingIntegration}: UnapproveWithIntegrationWarningParams) =>
`Este informe ya se ha exportado a ${accountingIntegration}. Los cambios realizados en este informe en Expensify pueden provocar discrepancias en los datos y problemas de conciliación de la tarjeta Expensify. ¿Está seguro de que desea anular la aprobación de este informe?`,
reimbursable: 'reembolsable',
nonReimbursable: 'no reembolsable',
@@ -1315,15 +1389,15 @@ export default {
availableSpend: 'Límite restante',
smartLimit: {
name: 'Límite inteligente',
- title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta al mes. El límite se restablecerá el primer día del mes.`,
+ title: ({formattedLimit}: ViolationsOverLimitParams) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta al mes. El límite se restablecerá el primer día del mes.`,
},
fixedLimit: {
name: 'Límite fijo',
- title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta, luego se desactivará.`,
+ title: ({formattedLimit}: ViolationsOverLimitParams) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta, luego se desactivará.`,
},
monthlyLimit: {
name: 'Límite mensual',
- title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta y el límite se restablecerá a medida que se aprueben tus gastos.`,
+ title: ({formattedLimit}: ViolationsOverLimitParams) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta y el límite se restablecerá a medida que se aprueben tus gastos.`,
},
virtualCardNumber: 'Número de la tarjeta virtual',
physicalCardNumber: 'Número de la tarjeta física',
@@ -1527,7 +1601,7 @@ export default {
},
},
reportDetailsPage: {
- inWorkspace: ({policyName}) => `en ${policyName}`,
+ inWorkspace: ({policyName}: ReportPolicyNameParams) => `en ${policyName}`,
},
reportDescriptionPage: {
roomDescription: 'Descripción de la sala de chat',
@@ -1540,7 +1614,7 @@ export default {
groupChat: {
lastMemberTitle: '¡Atención!',
lastMemberWarning: 'Ya que eres la última persona aquí, si te vas, este chat quedará inaccesible para todos los miembros. ¿Estás seguro de que quieres salir del chat?',
- defaultReportName: ({displayName}: {displayName: string}) => `Chat de grupo de ${displayName}`,
+ defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Chat de grupo de ${displayName}`,
},
languagePage: {
language: 'Idioma',
@@ -1671,7 +1745,7 @@ export default {
error: {
dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `La fecha debe ser anterior a ${dateString}.`,
dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `La fecha debe ser posterior a ${dateString}.`,
- incorrectZipFormat: (zipFormat?: string) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`,
+ incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams = {}) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`,
hasInvalidCharacter: 'El nombre sólo puede incluir caracteres latinos.',
},
},
@@ -2170,6 +2244,8 @@ export default {
},
bookTravel: 'Reservar viajes',
bookDemo: 'Pedir demostración',
+ bookADemo: 'Reserva una demo',
+ toLearnMore: ' para obtener más información.',
termsAndConditions: {
header: 'Antes de continuar...',
title: 'Por favor, lee los Términos y condiciones para reservar viajes',
@@ -2226,7 +2302,7 @@ export default {
testTransactions: 'Transacciones de prueba',
issueAndManageCards: 'Emitir y gestionar tarjetas',
reconcileCards: 'Reconciliar tarjetas',
- selected: ({selectedNumber}) => `${selectedNumber} seleccionados`,
+ selected: ({selectedNumber}: SelectedNumberParams) => `${selectedNumber} seleccionados`,
settlementFrequency: 'Frecuencia de liquidación',
deleteConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo?',
unavailable: 'Espacio de trabajo no disponible',
@@ -2245,7 +2321,7 @@ export default {
`¡Has sido invitado a ${workspaceName}! Descargue la aplicación móvil Expensify en use.expensify.com/download para comenzar a rastrear sus gastos.`,
subscription: 'Suscripción',
markAsExported: 'Marcar como introducido manualmente',
- exportIntegrationSelected: (connectionName: ConnectionName) => `Exportar a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
+ exportIntegrationSelected: ({connectionName}: ExportIntegrationSelectedParams) => `Exportar a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
letsDoubleCheck: 'Verifiquemos que todo esté correcto',
reportField: 'Campo del informe',
lineItemLevel: 'Nivel de partida',
@@ -2262,14 +2338,14 @@ export default {
createNewConnection: 'Crear una nueva conexión',
reuseExistingConnection: 'Reutilizar la conexión existente',
existingConnections: 'Conexiones existentes',
- lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Última sincronización ${formattedDate}`,
+ lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} - Última sincronización ${formattedDate}`,
topLevel: 'Nivel superior',
- authenticationError: (connectionName: string) => `No se puede conectar a ${connectionName} debido a un error de autenticación.`,
+ authenticationError: ({connectionName}: AuthenticationErrorParams) => `No se puede conectar a ${connectionName} debido a un error de autenticación.`,
learnMore: 'Más información.',
memberAlternateText: 'Los miembros pueden presentar y aprobar informes.',
adminAlternateText: 'Los administradores tienen acceso total para editar todos los informes y la configuración del área de trabajo.',
auditorAlternateText: 'Los auditores pueden ver y comentar los informes.',
- roleName: (role?: string): string => {
+ roleName: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
return 'Administrador';
@@ -2400,8 +2476,8 @@ export default {
accountsSwitchDescription: 'Las categorías activas estarán disponibles para ser escogidas cuando se crea un gasto.',
trackingCategories: 'Categorías de seguimiento',
trackingCategoriesDescription: 'Elige cómo gestionar categorías de seguimiento de Xero en Expensify.',
- mapTrackingCategoryTo: ({categoryName}) => `Asignar ${categoryName} de Xero a`,
- mapTrackingCategoryToDescription: ({categoryName}) => `Elige dónde mapear ${categoryName} al exportar a Xero.`,
+ mapTrackingCategoryTo: ({categoryName}: CategoryNameParams) => `Asignar ${categoryName} de Xero a`,
+ mapTrackingCategoryToDescription: ({categoryName}: CategoryNameParams) => `Elige dónde mapear ${categoryName} al exportar a Xero.`,
customers: 'Volver a facturar a los clientes',
customersDescription:
'Elige si quieres volver a facturar a los clientes en Expensify. Tus contactos de clientes de Xero se pueden etiquetar como gastos, y se exportarán a Xero como una factura de venta.',
@@ -2502,7 +2578,7 @@ export default {
},
creditCardAccount: 'Cuenta de tarjeta de crédito',
defaultVendor: 'Proveedor por defecto',
- defaultVendorDescription: (isReimbursable: boolean): string =>
+ defaultVendorDescription: ({isReimbursable}: DefaultVendorDescriptionParams) =>
`Establezca un proveedor predeterminado que se aplicará a los gastos ${isReimbursable ? '' : 'no '}reembolsables que no tienen un proveedor coincidente en Sage Intacct.`,
exportDescription: 'Configure cómo se exportan los datos de Expensify a Sage Intacct.',
exportPreferredExporterNote:
@@ -2720,12 +2796,12 @@ export default {
importJobs: 'Importar proyectos',
customers: 'clientes',
jobs: 'proyectos',
- label: (importFields: string[], importType: string) => `${importFields.join(' y ')}, ${importType}`,
+ label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join(' y ')}, ${importType}`,
},
importTaxDescription: 'Importar grupos de impuestos desde NetSuite.',
importCustomFields: {
chooseOptionBelow: 'Elija una de las opciones siguientes:',
- requiredFieldError: (fieldName: string) => `Por favor, introduzca el ${fieldName}`,
+ requiredFieldError: ({fieldName}: RequiredFieldParams) => `Por favor, introduzca el ${fieldName}`,
customSegments: {
title: 'Segmentos/registros personalizados',
addText: 'Añadir segmento/registro personalizado',
@@ -2766,7 +2842,7 @@ export default {
customRecordMappingTitle: '¿Cómo debería mostrarse este registro de segmento personalizado en Expensify?',
},
errors: {
- uniqueFieldError: (fieldName: string) => `Ya existe un segmento/registro personalizado con este ${fieldName?.toLowerCase()}.`,
+ uniqueFieldError: ({fieldName}: RequiredFieldParams) => `Ya existe un segmento/registro personalizado con este ${fieldName?.toLowerCase()}.`,
},
},
customLists: {
@@ -2800,18 +2876,18 @@ export default {
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: {
label: 'Predeterminado del empleado NetSuite',
description: 'No importado a Expensify, aplicado en exportación',
- footerContent: (importField: string) =>
+ footerContent: ({importField}: ImportFieldParams) =>
`Si usa ${importField} en NetSuite, aplicaremos el conjunto predeterminado en el registro del empleado al exportarlo a Informe de gastos o Entrada de diario.`,
},
[CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: {
label: 'Etiquetas',
description: 'Nivel de línea de pedido',
- footerContent: (importField: string) => `Se podrán seleccionar ${importField} para cada gasto individual en el informe de un empleado.`,
+ footerContent: ({importField}: ImportFieldParams) => `Se podrán seleccionar ${importField} para cada gasto individual en el informe de un empleado.`,
},
[CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: {
label: 'Campos de informe',
description: 'Nivel de informe',
- footerContent: (importField: string) => `La selección de ${importField} se aplicará a todos los gastos en el informe de un empleado.`,
+ footerContent: ({importField}: ImportFieldParams) => `La selección de ${importField} se aplicará a todos los gastos en el informe de un empleado.`,
},
},
},
@@ -2842,8 +2918,8 @@ export default {
addAUserDefinedDimension: 'Añadir una dimensión definida por el usuario',
detailedInstructionsLink: 'Ver instrucciones detalladas',
detailedInstructionsRestOfSentence: ' para añadir dimensiones definidas por el usuario.',
- userDimensionsAdded: (dimensionsCount: number) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} añadido`,
- mappingTitle: (mappingName: SageIntacctMappingName): string => {
+ userDimensionsAdded: ({dimensionsCount}: DimensionsCountParams) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} añadido`,
+ mappingTitle: ({mappingName}: IntacctMappingTitleParams) => {
switch (mappingName) {
case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS:
return 'departamentos';
@@ -2877,7 +2953,7 @@ export default {
},
yourCardProvider: `¿Quién es su proveedor de tarjetas?`,
enableFeed: {
- title: (provider: string) => `Habilita tu feed ${provider}`,
+ 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.*`,
@@ -2925,7 +3001,7 @@ export default {
card: 'Tarjeta',
startTransactionDate: 'Fecha de inicio de transacciones',
cardName: 'Nombre de la tarjeta',
- assignedYouCard: (assigner: string) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`,
+ assignedYouCard: ({assigner}: AssignedYouCardParams) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`,
chooseCardFeed: 'Elige feed de tarjetas',
},
expensifyCard: {
@@ -2973,21 +3049,21 @@ export default {
deactivate: 'Desactivar tarjeta',
changeCardLimit: 'Modificar el límite de la tarjeta',
changeLimit: 'Modificar límite',
- smartLimitWarning: (limit: string) =>
+ smartLimitWarning: ({limit}: CharacterLimitParams) =>
`Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta que apruebes antiguos gastos de la tarjeta.`,
- monthlyLimitWarning: (limit: string) => `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta el próximo mes.`,
- fixedLimitWarning: (limit: string) => `Si cambias el límite de esta tarjeta a ${limit}, se rechazarán las nuevas transacciones.`,
+ monthlyLimitWarning: ({limit}: CharacterLimitParams) => `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta el próximo mes.`,
+ fixedLimitWarning: ({limit}: CharacterLimitParams) => `Si cambias el límite de esta tarjeta a ${limit}, se rechazarán las nuevas transacciones.`,
changeCardLimitType: 'Modificar el tipo de límite de la tarjeta',
changeLimitType: 'Modificar el tipo de límite',
- changeCardSmartLimitTypeWarning: (limit: string) =>
+ changeCardSmartLimitTypeWarning: ({limit}: CharacterLimitParams) =>
`Si cambias el tipo de límite de esta tarjeta a Límite inteligente, las nuevas transacciones serán rechazadas porque ya se ha alcanzado el límite de ${limit} no aprobado.`,
- changeCardMonthlyLimitTypeWarning: (limit: string) =>
+ changeCardMonthlyLimitTypeWarning: ({limit}: CharacterLimitParams) =>
`Si cambias el tipo de límite de esta tarjeta a Mensual, las nuevas transacciones serán rechazadas porque ya se ha alcanzado el límite de ${limit} mensual.`,
addShippingDetails: 'Añadir detalles de envío',
- issuedCard: (assignee: string) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta llegará en 2-3 días laborables.`,
- issuedCardNoShippingDetails: (assignee: string) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta se enviará una vez que se agreguen los detalles de envío.`,
+ issuedCard: ({assignee}: AssigneeParams) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta llegará en 2-3 días laborables.`,
+ issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta se enviará una vez que se agreguen los detalles de envío.`,
issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `¡emitió a ${assignee} una ${link} virtual! La tarjeta puede utilizarse inmediatamente.`,
- addedShippingDetails: (assignee: string) => `${assignee} agregó los detalles de envío. La Tarjeta Expensify llegará en 2-3 días hábiles.`,
+ addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} agregó los detalles de envío. La Tarjeta Expensify llegará en 2-3 días hábiles.`,
},
categories: {
deleteCategories: 'Eliminar categorías',
@@ -3086,8 +3162,9 @@ export default {
cardNumber: 'Número de la tarjeta',
cardholder: 'Titular de la tarjeta',
cardName: 'Nombre de la tarjeta',
- integrationExport: (integration: string, type: string) => `Exportación a ${integration} ${type}`,
- integrationExportTitleFirstPart: (integration: string) => `Seleccione la cuenta ${integration} donde se deben exportar las transacciones. Seleccione una cuenta diferente`,
+ integrationExport: ({integration, type}: IntegrationExportParams) => `Exportación a ${integration} ${type}`,
+ integrationExportTitleFirstPart: ({integration}: IntegrationExportParams) =>
+ `Seleccione la cuenta ${integration} donde se deben exportar las transacciones. Seleccione una cuenta diferente`,
integrationExportTitleLinkPart: 'opción de exportación',
integrationExportTitleSecondPart: 'para cambiar las cuentas disponibles.',
lastUpdated: 'Última actualización',
@@ -3121,7 +3198,7 @@ export default {
giveItNameInstruction: 'Nombra la tarjeta para distingirla de las demás.',
updating: 'Actualizando...',
noAccountsFound: 'No se han encontrado cuentas',
- noAccountsFoundDescription: (connection: string) => `Añade la cuenta en ${connection} y sincroniza la conexión de nuevo.`,
+ noAccountsFoundDescription: ({connection}: ConnectionParams) => `Añade la cuenta en ${connection} y sincroniza la conexión de nuevo.`,
},
workflows: {
title: 'Flujos de trabajo',
@@ -3221,6 +3298,7 @@ export default {
disableTags: 'Desactivar etiquetas',
addTag: 'Añadir etiqueta',
editTag: 'Editar etiqueta',
+ editTags: 'Editar etiquetas',
subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.',
emptyTags: {
title: 'No has creado ninguna etiqueta',
@@ -3241,7 +3319,7 @@ export default {
tagRules: 'Reglas de etiquetas',
approverDescription: 'Aprobador',
importTags: 'Importar categorías',
- importedTagsMessage: (columnCounts: number) =>
+ importedTagsMessage: ({columnCounts}: ImportedTagsMessageParams) =>
`Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Nombre* junto a la columna que contiene los nombres de las etiquetas. También puede seleccionar *Habilitado* junto a la columna que establece el estado de la etiqueta.`,
},
taxes: {
@@ -3264,7 +3342,7 @@ export default {
updateTaxClaimableFailureMessage: 'La porción recuperable debe ser menor al monto del importe por distancia.',
},
deleteTaxConfirmation: '¿Estás seguro de que quieres eliminar este impuesto?',
- deleteMultipleTaxConfirmation: ({taxAmount}) => `¿Estás seguro de que quieres eliminar ${taxAmount} impuestos?`,
+ deleteMultipleTaxConfirmation: ({taxAmount}: TaxAmountParams) => `¿Estás seguro de que quieres eliminar ${taxAmount} impuestos?`,
actions: {
delete: 'Eliminar tasa',
deleteMultiple: 'Eliminar tasas',
@@ -3307,7 +3385,7 @@ export default {
removeWorkspaceMemberButtonTitle: 'Eliminar del espacio de trabajo',
removeGroupMemberButtonTitle: 'Eliminar del grupo',
removeRoomMemberButtonTitle: 'Eliminar del chat',
- removeMemberPrompt: ({memberName}: {memberName: string}) => `¿Estás seguro de que deseas eliminar a ${memberName}?`,
+ removeMemberPrompt: ({memberName}: RemoveMemberPromptParams) => `¿Estás seguro de que deseas eliminar a ${memberName}?`,
removeMemberTitle: 'Eliminar miembro',
transferOwner: 'Transferir la propiedad',
makeMember: 'Hacer miembro',
@@ -3320,7 +3398,7 @@ export default {
genericRemove: 'Ha ocurrido un problema al eliminar al miembro del espacio de trabajo.',
},
addedWithPrimary: 'Se agregaron algunos miembros con sus nombres de usuario principales.',
- invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`,
+ invitedBySecondaryLogin: ({secondaryLogin}: SecondaryLoginParams) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`,
membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.',
importMembers: 'Importar miembros',
},
@@ -3332,8 +3410,8 @@ export default {
xero: 'Xero',
netsuite: 'NetSuite',
intacct: 'Sage Intacct',
- connectionName: (integration: ConnectionName) => {
- switch (integration) {
+ connectionName: ({connectionName}: ConnectionNameParams) => {
+ switch (connectionName) {
case CONST.POLICY.CONNECTIONS.NAME.QBO:
return 'Quickbooks Online';
case CONST.POLICY.CONNECTIONS.NAME.XERO:
@@ -3350,20 +3428,21 @@ export default {
errorODIntegration: 'Hay un error con una conexión que se ha configurado en Expensify Classic. ',
goToODToFix: 'Ve a Expensify Classic para solucionar este problema.',
setup: 'Configurar',
- lastSync: (relativeDate: string) => `Recién sincronizado ${relativeDate}`,
+ lastSync: ({relativeDate}: LastSyncAccountingParams) => `Recién sincronizado ${relativeDate}`,
import: 'Importar',
export: 'Exportar',
advanced: 'Avanzado',
other: 'Otras integraciones',
syncNow: 'Sincronizar ahora',
disconnect: 'Desconectar',
- disconnectTitle: (integration?: ConnectionName): string => {
- const integrationName = integration && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration] : 'integración';
+ disconnectTitle: ({connectionName}: OptionalParam = {}) => {
+ const integrationName =
+ connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'integración';
return `Desconectar ${integrationName}`;
},
- connectTitle: (integrationToConnect: ConnectionName): string => `Conectar ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integrationToConnect] ?? 'accounting integration'}`,
- syncError: (integration?: ConnectionName): string => {
- switch (integration) {
+ connectTitle: ({connectionName}: ConnectionNameParams) => `Conectar ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'accounting integration'}`,
+ syncError: ({connectionName}: OptionalParam = {}) => {
+ switch (connectionName) {
case CONST.POLICY.CONNECTIONS.NAME.QBO:
return 'No se puede conectar a QuickBooks Online.';
case CONST.POLICY.CONNECTIONS.NAME.XERO:
@@ -3389,18 +3468,18 @@ export default {
[CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Importado como campos de informe',
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Predeterminado del empleado NetSuite',
},
- disconnectPrompt: (currentIntegration?: ConnectionName): string => {
+ disconnectPrompt: ({connectionName}: OptionalParam = {}) => {
const integrationName =
- currentIntegration && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[currentIntegration] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[currentIntegration] : 'integración';
+ connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'integración';
return `¿Estás seguro de que quieres desconectar ${integrationName}?`;
},
- connectPrompt: (integrationToConnect: ConnectionName): string =>
+ connectPrompt: ({connectionName}: ConnectionNameParams) =>
`¿Estás seguro de que quieres conectar a ${
- CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integrationToConnect] ?? 'esta integración contable'
+ CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'esta integración contable'
}? Esto eliminará cualquier conexión contable existente.`,
enterCredentials: 'Ingresa tus credenciales',
connections: {
- syncStageName: (stage: PolicyConnectionSyncStage) => {
+ syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
case 'quickbooksOnlineImportCustomers':
return 'Importando clientes';
@@ -3537,7 +3616,7 @@ export default {
chooseBankAccount: 'Elige la cuenta bancaria con la que se conciliarán los pagos de tu Tarjeta Expensify.',
accountMatches: 'Asegúrate de que esta cuenta coincide con ',
settlementAccount: 'la cuenta de liquidación de tu Tarjeta Expensify ',
- reconciliationWorks: (lastFourPAN: string) => `(que termina en ${lastFourPAN}) para que la conciliación continua funcione correctamente.`,
+ reconciliationWorks: ({lastFourPAN}: ReconciliationWorksParams) => `(que termina en ${lastFourPAN}) para que la conciliación continua funcione correctamente.`,
},
},
card: {
@@ -3635,7 +3714,10 @@ export default {
rate: 'Tasa',
addRate: 'Agregar tasa',
trackTax: 'Impuesto de seguimiento',
- deleteRates: ({count}: DistanceRateOperationsParams) => `Eliminar ${Str.pluralize('tasa', 'tasas', count)}`,
+ deleteRates: () => ({
+ one: 'Eliminar tasa',
+ other: 'Eliminar tasas',
+ }),
enableRates: ({count}: DistanceRateOperationsParams) => `Activar ${Str.pluralize('tasa', 'tasas', count)}`,
disableRates: ({count}: DistanceRateOperationsParams) => `Desactivar ${Str.pluralize('tasa', 'tasas', count)}`,
enableRate: 'Activar tasa',
@@ -3705,19 +3787,19 @@ export default {
amountOwedText: 'Esta cuenta tiene un saldo pendiente de un mes anterior.\n\n¿Quiere liquidar el saldo y hacerse cargo de la facturación de este espacio de trabajo?',
ownerOwesAmountTitle: 'Saldo pendiente',
ownerOwesAmountButtonText: 'Transferir saldo',
- ownerOwesAmountText: ({email, amount}) =>
+ ownerOwesAmountText: ({email, amount}: OwnerOwesAmountParams) =>
`La cuenta propietaria de este espacio de trabajo (${email}) tiene un saldo pendiente de un mes anterior.\n\n¿Desea transferir este monto (${amount}) para hacerse cargo de la facturación de este espacio de trabajo? tu tarjeta de pago se cargará inmediatamente.`,
subscriptionTitle: 'Asumir la suscripción anual',
subscriptionButtonText: 'Transferir suscripción',
- subscriptionText: ({usersCount, finalCount}) =>
+ subscriptionText: ({usersCount, finalCount}: ChangeOwnerSubscriptionParams) =>
`Al hacerse cargo de este espacio de trabajo se fusionará tu suscripción anual asociada con tu suscripción actual. Esto aumentará el tamaño de tu suscripción en ${usersCount} miembros, lo que hará que tu nuevo tamaño de suscripción sea ${finalCount}. ¿Te gustaria continuar?`,
duplicateSubscriptionTitle: 'Alerta de suscripción duplicada',
duplicateSubscriptionButtonText: 'Continuar',
- duplicateSubscriptionText: ({email, workspaceName}) =>
+ duplicateSubscriptionText: ({email, workspaceName}: ChangeOwnerDuplicateSubscriptionParams) =>
`Parece que estás intentando hacerte cargo de la facturación de los espacios de trabajo de ${email}, pero para hacerlo, primero debes ser administrador de todos sus espacios de trabajo.\n\nHaz clic en "Continuar" si solo quieres tomar sobrefacturación para el espacio de trabajo ${workspaceName}.\n\nSi desea hacerse cargo de la facturación de toda tu suscripción, pídales que lo agreguen como administrador a todos sus espacios de trabajo antes de hacerse cargo de la facturación.`,
hasFailedSettlementsTitle: 'No se puede transferir la propiedad',
hasFailedSettlementsButtonText: 'Entiendo',
- hasFailedSettlementsText: ({email}) =>
+ hasFailedSettlementsText: ({email}: ChangeOwnerHasFailedSettlementsParams) =>
`No puede hacerse cargo de la facturación porque ${email} tiene una liquidación vencida de la tarjeta Expensify. Avíseles que se comuniquen con concierge@expensify.com para resolver el problema. Luego, podrá hacerse cargo de la facturación de este espacio de trabajo.`,
failedToClearBalanceTitle: 'Fallo al liquidar el saldo',
failedToClearBalanceButtonText: 'OK',
@@ -3732,7 +3814,7 @@ export default {
exportAgainModal: {
title: '¡Cuidado!',
- description: (reportName: string, connectionName: ConnectionName) =>
+ description: ({reportName, connectionName}: ExportAgainModalDescriptionParams) =>
`Los siguientes informes ya se han exportado a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}:\n\n${reportName}\n\n¿Estás seguro de que deseas exportarlos de nuevo?`,
confirmText: 'Sí, exportar de nuevo',
cancelText: 'Cancelar',
@@ -3795,7 +3877,7 @@ export default {
upgradeToUnlock: 'Desbloquear esta función',
completed: {
headline: 'Has mejorado tu espacio de trabajo.',
- successMessage: (policyName: string) => `Ha mejorado correctamente su espacio de trabajo ${policyName} al plan Control.`,
+ successMessage: ({policyName}: ReportPolicyNameParams) => `Ha mejorado correctamente su espacio de trabajo ${policyName} al plan Control.`,
viewSubscription: 'Ver su suscripción',
moreDetails: 'para obtener más información.',
gotIt: 'Entendido, gracias.',
@@ -3803,8 +3885,8 @@ export default {
},
restrictedAction: {
restricted: 'Restringido',
- actionsAreCurrentlyRestricted: ({workspaceName}) => `Las acciones en el espacio de trabajo ${workspaceName} están actualmente restringidas`,
- workspaceOwnerWillNeedToAddOrUpdatePaymentCard: ({workspaceOwnerName}) =>
+ actionsAreCurrentlyRestricted: ({workspaceName}: ActionsAreCurrentlyRestricted) => `Las acciones en el espacio de trabajo ${workspaceName} están actualmente restringidas`,
+ workspaceOwnerWillNeedToAddOrUpdatePaymentCard: ({workspaceOwnerName}: WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams) =>
`El propietario del espacio de trabajo, ${workspaceOwnerName} tendrá que añadir o actualizar la tarjeta de pago registrada para desbloquear nueva actividad en el espacio de trabajo.`,
youWillNeedToAddOrUpdatePaymentCard: 'Debes añadir o actualizar la tarjeta de pago registrada para desbloquear nueva actividad en el espacio de trabajo.',
addPaymentCardToUnlock: 'Añade una tarjeta para desbloquearlo!',
@@ -3825,7 +3907,7 @@ export default {
maxAge: 'Antigüedad máxima',
maxExpenseAge: 'Antigüedad máxima de los gastos',
maxExpenseAgeDescription: 'Marca los gastos de más de un número determinado de días.',
- maxExpenseAgeDays: (age: number) => `${age} ${Str.pluralize('día', 'días', age)}`,
+ maxExpenseAgeDays: ({age}: AgeParams) => `${age} ${Str.pluralize('día', 'días', age)}`,
billableDefault: 'Valor predeterminado facturable',
billableDefaultDescription: 'Elige si los gastos en efectivo y con tarjeta de crédito deben ser facturables por defecto. Los gastos facturables se activan o desactivan en',
billable: 'Facturable',
@@ -3862,26 +3944,26 @@ export default {
randomReportAuditDescription: 'Requiere que algunos informes sean aprobados manualmente, incluso si son elegibles para la aprobación automática.',
autoPayApprovedReportsTitle: 'Pago automático de informes aprobados',
autoPayApprovedReportsSubtitle: 'Configura qué informes de gastos pueden pagarse de forma automática.',
- autoPayApprovedReportsLimitError: (currency?: string) => `Por favor, introduce un monto menor a ${currency ?? ''}20,000`,
+ autoPayApprovedReportsLimitError: ({currency}: AutoPayApprovedReportsLimitErrorParams = {}) => `Por favor, introduce un monto menor a ${currency ?? ''}20,000`,
autoPayApprovedReportsLockedSubtitle: 'Ve a más funciones y habilita flujos de trabajo, luego agrega pagos para desbloquear esta función.',
autoPayReportsUnderTitle: 'Pagar automáticamente informes por debajo de',
autoPayReportsUnderDescription: 'Los informes de gastos totalmente conformes por debajo de esta cantidad se pagarán automáticamente.',
unlockFeatureGoToSubtitle: 'Ir a',
- unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `y habilita flujos de trabajo, luego agrega ${featureName} para desbloquear esta función.`,
- enableFeatureSubtitle: (featureName: string) => `y habilita ${featureName} para desbloquear esta función.`,
+ unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `y habilita flujos de trabajo, luego agrega ${featureName} para desbloquear esta función.`,
+ enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `y habilita ${featureName} para desbloquear esta función.`,
},
categoryRules: {
title: 'Reglas de categoría',
approver: 'Aprobador',
requireDescription: 'Requerir descripción',
descriptionHint: 'Sugerencia de descripción',
- descriptionHintDescription: (categoryName: string) =>
+ descriptionHintDescription: ({categoryName}: CategoryNameParams) =>
`Recuerda a los empleados que deben proporcionar información adicional para los gastos de “${categoryName}”. Esta sugerencia aparece en el campo de descripción en los gastos.`,
descriptionHintLabel: 'Sugerencia',
descriptionHintSubtitle: 'Consejo: ¡Cuanto más corta, mejor!',
maxAmount: 'Importe máximo',
flagAmountsOver: 'Señala importes superiores a',
- flagAmountsOverDescription: (categoryName: string) => `Aplica a la categoría “${categoryName}”.`,
+ flagAmountsOverDescription: ({categoryName}: CategoryNameParams) => `Aplica a la categoría “${categoryName}”.`,
flagAmountsOverSubtitle: 'Esto anula el importe máximo para todos los gastos.',
expenseLimitTypes: {
expense: 'Gasto individual',
@@ -3891,7 +3973,7 @@ export default {
},
requireReceiptsOver: 'Requerir recibos para importes superiores a',
requireReceiptsOverList: {
- default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Predeterminado`,
+ default: ({defaultAmount}: DefaultAmountParams) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Predeterminado`,
never: 'Nunca requerir recibos',
always: 'Requerir recibos siempre',
},
@@ -3955,8 +4037,8 @@ export default {
},
},
workspaceActions: {
- renamedWorkspaceNameAction: ({oldName, newName}) => `actualizó el nombre de este espacio de trabajo de ${oldName} a ${newName}`,
- removedFromApprovalWorkflow: ({submittersNames}: {submittersNames: string[]}) => {
+ renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `actualizó el nombre de este espacio de trabajo de ${oldName} a ${newName}`,
+ removedFromApprovalWorkflow: ({submittersNames}: RemovedFromApprovalWorkflowParams) => {
let joinedNames = '';
if (submittersNames.length === 1) {
joinedNames = submittersNames[0];
@@ -4009,7 +4091,7 @@ export default {
deleteConfirmation: '¿Estás seguro de que quieres eliminar esta tarea?',
},
statementPage: {
- title: (year, monthName) => `Estado de cuenta de ${monthName} ${year}`,
+ title: ({year, monthName}: StatementTitleParams) => `Estado de cuenta de ${monthName} ${year}`,
generatingPDF: 'Estamos generando tu PDF ahora mismo. ¡Por favor, vuelve más tarde!',
},
keyboardShortcutsPage: {
@@ -4059,8 +4141,8 @@ export default {
filtersHeader: 'Filtros',
filters: {
date: {
- before: (date?: string) => `Antes de ${date ?? ''}`,
- after: (date?: string) => `Después de ${date ?? ''}`,
+ before: ({date}: OptionalParam = {}) => `Antes de ${date ?? ''}`,
+ after: ({date}: OptionalParam = {}) => `Después de ${date ?? ''}`,
},
status: 'Estado',
keyword: 'Palabra clave',
@@ -4070,9 +4152,9 @@ export default {
pinned: 'Fijado',
unread: 'No leído',
amount: {
- lessThan: (amount?: string) => `Menos de ${amount ?? ''}`,
- greaterThan: (amount?: string) => `Más que ${amount ?? ''}`,
- between: (greaterThan: string, lessThan: string) => `Entre ${greaterThan} y ${lessThan}`,
+ lessThan: ({amount}: OptionalParam = {}) => `Menos de ${amount ?? ''}`,
+ greaterThan: ({amount}: OptionalParam = {}) => `Más que ${amount ?? ''}`,
+ between: ({greaterThan, lessThan}: FiltersAmountBetweenParams) => `Entre ${greaterThan} y ${lessThan}`,
},
current: 'Actual',
past: 'Anterior',
@@ -4193,7 +4275,7 @@ export default {
nonReimbursableLink: 'Ver los gastos de la tarjeta de empresa.',
pending: ({label}: ExportedToIntegrationParams) => `comenzó a exportar este informe a ${label}...`,
},
- integrationsMessage: (errorMessage: string, label: string) => `no se pudo exportar este informe a ${label} ("${errorMessage}").`,
+ integrationsMessage: ({label, errorMessage}: IntegrationSyncFailedParams) => `no se pudo exportar este informe a ${label} ("${errorMessage}").`,
managerAttachReceipt: `agregó un recibo`,
managerDetachReceipt: `quitó un recibo`,
markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `pagó ${currency}${amount} en otro lugar`,
@@ -4210,11 +4292,11 @@ export default {
stripePaid: ({amount, currency}: StripePaidParams) => `pagado ${currency}${amount}`,
takeControl: `tomó el control`,
unapproved: ({amount, currency}: UnapprovedParams) => `no aprobado ${currency}${amount}`,
- integrationSyncFailed: (label: string, errorMessage: string) => `no se pudo sincronizar con ${label} ("${errorMessage}")`,
- addEmployee: (email: string, role: string) => `agregó a ${email} como ${role === 'user' ? 'miembro' : 'administrador'}`,
- updateRole: (email: string, currentRole: string, newRole: string) =>
+ integrationSyncFailed: ({label, errorMessage}: IntegrationSyncFailedParams) => `no se pudo sincronizar con ${label} ("${errorMessage}")`,
+ addEmployee: ({email, role}: AddEmployeeParams) => `agregó a ${email} como ${role === 'user' ? 'miembro' : 'administrador'}`,
+ updateRole: ({email, currentRole, newRole}: UpdateRoleParams) =>
`actualicé el rol ${email} de ${currentRole === 'user' ? 'miembro' : 'administrador'} a ${newRole === 'user' ? 'miembro' : 'administrador'}`,
- removeMember: (email: string, role: string) => `eliminado ${role === 'user' ? 'miembro' : 'administrador'} ${email}`,
+ removeMember: ({email, role}: AddEmployeeParams) => `eliminado ${role === 'user' ? 'miembro' : 'administrador'} ${email}`,
},
},
},
@@ -4895,9 +4977,9 @@ export default {
allTagLevelsRequired: 'Todas las etiquetas son obligatorias',
autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`,
billableExpense: 'La opción facturable ya no es válida',
- cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para cantidades mayores de ${formattedLimit}`,
+ cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams = {}) => `Recibo obligatorio para cantidades mayores de ${formattedLimit}`,
categoryOutOfPolicy: 'La categoría ya no es válida',
- conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams = {}) => `${surcharge}% de recargo aplicado`,
+ conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `${surcharge}% de recargo aplicado`,
customUnitOutOfPolicy: 'Tasa inválida para este espacio de trabajo',
duplicatedTransaction: 'Duplicado',
fieldRequired: 'Los campos del informe son obligatorios',
@@ -4906,7 +4988,7 @@ export default {
maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Fecha de más de ${maxAge} días`,
missingCategory: 'Falta categoría',
missingComment: 'Descripción obligatoria para la categoría seleccionada',
- missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName ?? 'etiqueta'}`,
+ missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Falta ${tagName ?? 'etiqueta'}`,
modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => {
switch (type) {
case 'distance':
@@ -4957,10 +5039,10 @@ export default {
return '';
},
smartscanFailed: 'No se pudo escanear el recibo. Introduce los datos manualmente',
- someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams) => `Falta ${tagName ?? 'Tag'}`,
- tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `La etiqueta ${tagName ? `${tagName} ` : ''}ya no es válida`,
+ someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `Falta ${tagName ?? 'Tag'}`,
+ tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `La etiqueta ${tagName ? `${tagName} ` : ''}ya no es válida`,
taxAmountChanged: 'El importe del impuesto fue modificado',
- taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'El impuesto'} ya no es válido`,
+ taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams = {}) => `${taxName ?? 'El impuesto'} ya no es válido`,
taxRateChanged: 'La tasa de impuesto fue modificada',
taxRequired: 'Falta la tasa de impuesto',
none: 'Ninguno',
@@ -4977,7 +5059,7 @@ export default {
hold: 'Bloqueado',
},
reportViolations: {
- [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} es obligatorio`,
+ [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} es obligatorio`,
},
violationDismissal: {
rter: {
@@ -5032,12 +5114,12 @@ export default {
authenticatePaymentCard: 'Autenticar tarjeta de pago',
mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.',
badge: {
- freeTrial: ({numOfDays}) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`,
+ freeTrial: ({numOfDays}: BadgeFreeTrialParams) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`,
},
billingBanner: {
policyOwnerAmountOwed: {
title: 'Tu información de pago está desactualizada',
- subtitle: ({date}) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`,
+ subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`,
},
policyOwnerAmountOwedOverdue: {
title: 'Tu información de pago está desactualizada',
@@ -5045,7 +5127,7 @@ export default {
},
policyOwnerUnderInvoicing: {
title: 'Tu información de pago está desactualizada',
- subtitle: ({date}) => `Tu pago está vencido. Por favor, paga tu factura antes del ${date} para evitar la interrupción del servicio.`,
+ subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Tu pago está vencido. Por favor, paga tu factura antes del ${date} para evitar la interrupción del servicio.`,
},
policyOwnerUnderInvoicingOverdue: {
title: 'Tu información de pago está desactualizada',
@@ -5053,22 +5135,23 @@ export default {
},
billingDisputePending: {
title: 'No se ha podido realizar el cobro a tu tarjeta',
- subtitle: ({amountOwed, cardEnding}) =>
+ subtitle: ({amountOwed, cardEnding}: BillingBannerDisputePendingParams) =>
`Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`,
},
cardAuthenticationRequired: {
title: 'No se ha podido realizar el cobro a tu tarjeta',
- subtitle: ({cardEnding}) =>
+ subtitle: ({cardEnding}: BillingBannerCardAuthenticationRequiredParams) =>
`Tu tarjeta de pago no ha sido autenticada completamente. Por favor, completa el proceso de autenticación para activar tu tarjeta de pago que termina en ${cardEnding}.`,
},
insufficientFunds: {
title: 'No se ha podido realizar el cobro a tu tarjeta',
- subtitle: ({amountOwed}) =>
+ subtitle: ({amountOwed}: BillingBannerInsufficientFundsParams) =>
`Tu tarjeta de pago fue rechazada por falta de fondos. Vuelve a intentarlo o añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`,
},
cardExpired: {
title: 'No se ha podido realizar el cobro a tu tarjeta',
- subtitle: ({amountOwed}) => `Tu tarjeta de pago ha expirado. Por favor, añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`,
+ subtitle: ({amountOwed}: BillingBannerCardExpiredParams) =>
+ `Tu tarjeta de pago ha expirado. Por favor, añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`,
},
cardExpireSoon: {
title: 'Tu tarjeta caducará pronto',
@@ -5084,7 +5167,7 @@ export default {
subtitle:
'Antes de volver a intentarlo, llama directamente a tu banco para que autorice los cargos de Expensify y elimine las retenciones. De lo contrario, añade una tarjeta de pago diferente.',
},
- cardOnDispute: ({amountOwed, cardEnding}) =>
+ cardOnDispute: ({amountOwed, cardEnding}: BillingBannerCardOnDisputeParams) =>
`Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`,
preTrial: {
title: 'Iniciar una prueba gratuita',
@@ -5093,7 +5176,7 @@ export default {
subtitleEnd: 'para que tu equipo pueda empezar a enviar gastos.',
},
trialStarted: {
- title: ({numOfDays}) => `Prueba gratuita: ¡${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}!`,
+ title: ({numOfDays}: TrialStartedTitleParams) => `Prueba gratuita: ¡${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}!`,
subtitle: 'Añade una tarjeta de pago para seguir utilizando tus funciones favoritas.',
},
trialEnded: {
@@ -5105,9 +5188,9 @@ export default {
title: 'Pago',
subtitle: 'Añade una tarjeta para pagar tu suscripción a Expensify.',
addCardButton: 'Añade tarjeta de pago',
- cardNextPayment: ({nextPaymentDate}) => `Tu próxima fecha de pago es ${nextPaymentDate}.`,
- cardEnding: ({cardNumber}) => `Tarjeta terminada en ${cardNumber}`,
- cardInfo: ({name, expiration, currency}) => `Nombre: ${name}, Expiración: ${expiration}, Moneda: ${currency}`,
+ cardNextPayment: ({nextPaymentDate}: CardNextPaymentParams) => `Tu próxima fecha de pago es ${nextPaymentDate}.`,
+ cardEnding: ({cardNumber}: CardEndingParams) => `Tarjeta terminada en ${cardNumber}`,
+ cardInfo: ({name, expiration, currency}: CardInfoParams) => `Nombre: ${name}, Expiración: ${expiration}, Moneda: ${currency}`,
changeCard: 'Cambiar tarjeta de pago',
changeCurrency: 'Cambiar moneda de pago',
cardNotFound: 'No se ha añadido ninguna tarjeta de pago',
@@ -5126,8 +5209,8 @@ export default {
title: 'Tu plan',
collect: {
title: 'Recolectar',
- priceAnnual: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
- pricePayPerUse: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
+ priceAnnual: ({lower, upper}: YourPlanPriceParams) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
+ pricePayPerUse: ({lower, upper}: YourPlanPriceParams) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
benefit1: 'SmartScans ilimitados y seguimiento de la distancia',
benefit2: 'Tarjetas Expensify con Límites Inteligentes',
benefit3: 'Pago de facturas y facturación',
@@ -5138,8 +5221,8 @@ export default {
},
control: {
title: 'Control',
- priceAnnual: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
- pricePayPerUse: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
+ priceAnnual: ({lower, upper}: YourPlanPriceParams) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
+ pricePayPerUse: ({lower, upper}: YourPlanPriceParams) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
benefit1: 'Todo en Recolectar, más:',
benefit2: 'Integraciones con NetSuite y Sage Intacct',
benefit3: 'Sincronización de Certinia y Workday',
@@ -5170,10 +5253,10 @@ export default {
note: 'Nota: Un miembro activo es cualquiera que haya creado, editado, enviado, aprobado, reembolsado, o exportado datos de gastos vinculados al espacio de trabajo de tu empresa.',
confirmDetails: 'Confirma los datos de tu nueva suscripción anual:',
subscriptionSize: 'Tamaño de suscripción',
- activeMembers: ({size}) => `${size} miembros activos/mes`,
+ activeMembers: ({size}: SubscriptionSizeParams) => `${size} miembros activos/mes`,
subscriptionRenews: 'Renovación de la suscripción',
youCantDowngrade: 'No puedes bajar de categoría durante tu suscripción anual.',
- youAlreadyCommitted: ({size, date}) =>
+ youAlreadyCommitted: ({size, date}: SubscriptionCommitmentParams) =>
`Ya se ha comprometido a un tamaño de suscripción anual de ${size} miembros activos al mes hasta el ${date}. Puede cambiar a una suscripción de pago por uso en ${date} desactivando la auto-renovación.`,
error: {
size: 'Por favor ingrese un tamaño de suscripción valido.',
@@ -5190,13 +5273,13 @@ export default {
title: 'Configuración de suscripción',
autoRenew: 'Auto-renovación',
autoIncrease: 'Auto-incremento',
- saveUpTo: ({amountWithCurrency}) => `Ahorre hasta ${amountWithCurrency} al mes por miembro activo`,
+ saveUpTo: ({amountWithCurrency}: SubscriptionSettingsSaveUpToParams) => `Ahorre hasta ${amountWithCurrency} al mes por miembro activo`,
automaticallyIncrease:
'Aumenta automáticamente tus plazas anuales para dar lugar a los miembros activos que superen el tamaño de tu suscripción. Nota: Esto ampliará la fecha de finalización de tu suscripción anual.',
disableAutoRenew: 'Desactivar auto-renovación',
helpUsImprove: 'Ayúdanos a mejorar Expensify',
whatsMainReason: '¿Cuál es la razón principal por la que deseas desactivar la auto-renovación?',
- renewsOn: ({date}) => `Se renovará el ${date}.`,
+ renewsOn: ({date}: SubscriptionSettingsRenewsOnParams) => `Se renovará el ${date}.`,
},
requestEarlyCancellation: {
title: 'Solicitar cancelación anticipada',
@@ -5245,7 +5328,7 @@ export default {
addCopilot: 'Agregar copiloto',
membersCanAccessYourAccount: 'Estos miembros pueden acceder a tu cuenta:',
youCanAccessTheseAccounts: 'Puedes acceder a estas cuentas a través del conmutador de cuentas:',
- role: (role?: string): string => {
+ role: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.DELEGATE_ROLE.ALL:
return 'Completo';
@@ -5256,10 +5339,11 @@ export default {
}
},
genericError: '¡Ups! Ha ocurrido un error. Por favor, inténtalo de nuevo.',
+ onBehalfOfMessage: ({delegator}: DelegatorParams) => `en nombre de ${delegator}`,
accessLevel: 'Nivel de acceso',
confirmCopilot: 'Confirma tu copiloto a continuación.',
accessLevelDescription: 'Elige un nivel de acceso a continuación. Tanto el acceso Completo como el Limitado permiten a los copilotos ver todas las conversaciones y gastos.',
- roleDescription: (role?: string): string => {
+ roleDescription: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.DELEGATE_ROLE.ALL:
return 'Permite a otro miembro realizar todas las acciones en tu cuenta, en tu nombre. Incluye chat, presentaciones, aprobaciones, pagos, actualizaciones de configuración y más.';
@@ -5285,9 +5369,9 @@ export default {
nothingToPreview: 'Nada que previsualizar',
editJson: 'Editar JSON:',
preview: 'Previa:',
- missingProperty: ({propertyName}) => `Falta ${propertyName}`,
- invalidProperty: ({propertyName, expectedType}) => `Propiedad inválida: ${propertyName} - Esperado: ${expectedType}`,
- invalidValue: ({expectedValues}) => `Valor inválido - Esperado: ${expectedValues}`,
+ missingProperty: ({propertyName}: MissingPropertyParams) => `Falta ${propertyName}`,
+ invalidProperty: ({propertyName, expectedType}: InvalidPropertyParams) => `Propiedad inválida: ${propertyName} - Esperado: ${expectedType}`,
+ invalidValue: ({expectedValues}: InvalidValueParams) => `Valor inválido - Esperado: ${expectedValues}`,
missingValue: 'Valor en falta',
createReportAction: 'Crear Report Action',
reportAction: 'Report Action',
@@ -5302,4 +5386,6 @@ export default {
time: 'Hora',
none: 'Ninguno',
},
-} satisfies EnglishTranslation;
+};
+
+export default translations satisfies TranslationDeepObject;
diff --git a/src/languages/params.ts b/src/languages/params.ts
new file mode 100644
index 000000000000..8ed122283064
--- /dev/null
+++ b/src/languages/params.ts
@@ -0,0 +1,754 @@
+import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx';
+import type {DelegateRole} from '@src/types/onyx/Account';
+import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName, Unit} from '@src/types/onyx/Policy';
+import type {ViolationDataType} from '@src/types/onyx/TransactionViolation';
+
+type AddressLineParams = {
+ lineNumber: number;
+};
+
+type CharacterLimitParams = {
+ limit: number | string;
+};
+
+type AssigneeParams = {
+ assignee: string;
+};
+
+type CharacterLengthLimitParams = {
+ limit: number;
+ length: number;
+};
+
+type ZipCodeExampleFormatParams = {
+ zipSampleFormat: string;
+};
+
+type LoggedInAsParams = {
+ email: string;
+};
+
+type SignUpNewFaceCodeParams = {
+ login: string;
+};
+
+type WelcomeEnterMagicCodeParams = {
+ login: string;
+};
+
+type AlreadySignedInParams = {
+ email: string;
+};
+
+type GoBackMessageParams = {
+ provider: string;
+};
+
+type LocalTimeParams = {
+ user: string;
+ time: string;
+};
+
+type EditActionParams = {
+ action: OnyxInputOrEntry;
+};
+
+type DeleteActionParams = {
+ action: OnyxInputOrEntry;
+};
+
+type DeleteConfirmationParams = {
+ action: OnyxInputOrEntry;
+};
+
+type BeginningOfChatHistoryDomainRoomPartOneParams = {
+ domainRoom: string;
+};
+
+type BeginningOfChatHistoryAdminRoomPartOneParams = {
+ workspaceName: string;
+};
+
+type BeginningOfChatHistoryAnnounceRoomPartOneParams = {
+ workspaceName: string;
+};
+
+type BeginningOfChatHistoryAnnounceRoomPartTwo = {
+ workspaceName: string;
+};
+
+type WelcomeToRoomParams = {
+ roomName: string;
+};
+
+type UsePlusButtonParams = {
+ additionalText: string;
+};
+
+type ReportArchiveReasonsClosedParams = {
+ displayName: string;
+};
+
+type ReportArchiveReasonsMergedParams = {
+ displayName: string;
+ oldDisplayName: string;
+};
+
+type ReportArchiveReasonsRemovedFromPolicyParams = {
+ displayName: string;
+ policyName: string;
+ shouldUseYou?: boolean;
+};
+
+type ReportPolicyNameParams = {
+ policyName: string;
+};
+
+type ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams = {
+ policyName: string;
+};
+
+type RequestCountParams = {
+ count: number;
+ scanningReceipts: number;
+ pendingReceipts: number;
+};
+
+type SettleExpensifyCardParams = {
+ formattedAmount: string;
+};
+
+type RequestAmountParams = {amount: string};
+
+type RequestedAmountMessageParams = {formattedAmount: string; comment?: string};
+
+type SplitAmountParams = {amount: string};
+
+type DidSplitAmountMessageParams = {formattedAmount: string; comment: string};
+
+type UserSplitParams = {amount: string};
+
+type PayerOwesAmountParams = {payer: string; amount: number | string; comment?: string};
+
+type PayerOwesParams = {payer: string};
+
+type CompanyCardFeedNameParams = {feedName: string};
+
+type PayerPaidAmountParams = {payer?: string; amount: number | string};
+
+type ApprovedAmountParams = {amount: number | string};
+
+type ForwardedAmountParams = {amount: number | string};
+
+type ManagerApprovedParams = {manager: string};
+
+type ManagerApprovedAmountParams = {manager: string; amount: number | string};
+
+type PayerPaidParams = {payer: string};
+
+type PayerSettledParams = {amount: number | string};
+
+type WaitingOnBankAccountParams = {submitterDisplayName: string};
+
+type CanceledRequestParams = {amount: string; submitterDisplayName: string};
+
+type AdminCanceledRequestParams = {manager: string; amount: string};
+
+type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string};
+
+type PaidElsewhereWithAmountParams = {payer?: string; amount: string};
+
+type PaidWithExpensifyWithAmountParams = {payer?: string; amount: string};
+
+type ThreadRequestReportNameParams = {formattedAmount: string; comment: string};
+
+type ThreadSentMoneyReportNameParams = {formattedAmount: string; comment: string};
+
+type SizeExceededParams = {maxUploadSizeInMB: number};
+
+type ResolutionConstraintsParams = {minHeightInPx: number; minWidthInPx: number; maxHeightInPx: number; maxWidthInPx: number};
+
+type NotAllowedExtensionParams = {allowedExtensions: string[]};
+
+type EnterMagicCodeParams = {contactMethod: string};
+
+type TransferParams = {amount: string};
+
+type InstantSummaryParams = {rate: string; minAmount: string};
+
+type NotYouParams = {user: string};
+
+type DateShouldBeBeforeParams = {dateString: string};
+
+type DateShouldBeAfterParams = {dateString: string};
+
+type WeSentYouMagicSignInLinkParams = {login: string; loginType: string};
+
+type ToValidateLoginParams = {primaryLogin: string; secondaryLogin: string};
+
+type NoLongerHaveAccessParams = {primaryLogin: string};
+
+type OurEmailProviderParams = {login: string};
+
+type ConfirmThatParams = {login: string};
+
+type UntilTimeParams = {time: string};
+
+type StepCounterParams = {step: number; total?: number; text?: string};
+
+type UserIsAlreadyMemberParams = {login: string; name: string};
+
+type GoToRoomParams = {roomName: string};
+
+type WelcomeNoteParams = {workspaceName: string};
+
+type RoomNameReservedErrorParams = {reservedName: string};
+
+type RenamedRoomActionParams = {oldName: string; newName: string};
+
+type RoomRenamedToParams = {newName: string};
+
+type OOOEventSummaryFullDayParams = {summary: string; dayCount: number; date: string};
+
+type OOOEventSummaryPartialDayParams = {summary: string; timePeriod: string; date: string};
+
+type ParentNavigationSummaryParams = {reportName?: string; workspaceName?: string};
+
+type SetTheRequestParams = {valueName: string; newValueToDisplay: string};
+
+type SetTheDistanceMerchantParams = {translatedChangedField: string; newMerchant: string; newAmountToDisplay: string};
+
+type RemovedTheRequestParams = {valueName: string; oldValueToDisplay: string};
+
+type UpdatedTheRequestParams = {valueName: string; newValueToDisplay: string; oldValueToDisplay: string};
+
+type UpdatedTheDistanceMerchantParams = {translatedChangedField: string; newMerchant: string; oldMerchant: string; newAmountToDisplay: string; oldAmountToDisplay: string};
+
+type FormattedMaxLengthParams = {formattedMaxLength: string};
+
+type WalletProgramParams = {walletProgram: string};
+
+type ViolationsAutoReportedRejectedExpenseParams = {rejectedBy: string; rejectReason: string};
+
+type ViolationsCashExpenseWithNoReceiptParams = {formattedLimit?: string} | undefined;
+
+type ViolationsConversionSurchargeParams = {surcharge: number};
+
+type ViolationsInvoiceMarkupParams = {invoiceMarkup: number};
+
+type ViolationsMaxAgeParams = {maxAge: number};
+
+type ViolationsMissingTagParams = {tagName?: string} | undefined;
+
+type ViolationsModifiedAmountParams = {type?: ViolationDataType; displayPercentVariance?: number};
+
+type ViolationsOverAutoApprovalLimitParams = {formattedLimit: string};
+
+type ViolationsOverCategoryLimitParams = {formattedLimit: string};
+
+type ViolationsOverLimitParams = {formattedLimit: string};
+
+type ViolationsPerDayLimitParams = {formattedLimit: string};
+
+type ViolationsReceiptRequiredParams = {formattedLimit?: string; category?: string};
+
+type ViolationsRterParams = {
+ brokenBankConnection: boolean;
+ isAdmin: boolean;
+ email?: string;
+ isTransactionOlderThan7Days: boolean;
+ member?: string;
+};
+
+type ViolationsTagOutOfPolicyParams = {tagName?: string} | undefined;
+
+type ViolationsTaxOutOfPolicyParams = {taxName?: string} | undefined;
+
+type PaySomeoneParams = {name?: string} | undefined;
+
+type TaskCreatedActionParams = {title: string};
+
+type OptionalParam = Partial;
+
+type TermsParams = {amount: string};
+
+type ElectronicFundsParams = {percentage: string; amount: string};
+
+type LogSizeParams = {size: number};
+
+type LogSizeAndDateParams = {size: number; date: string};
+
+type HeldRequestParams = {comment: string};
+
+type DistanceRateOperationsParams = {count: number};
+
+type ReimbursementRateParams = {unit: Unit};
+
+type ConfirmHoldExpenseParams = {transactionCount: number};
+
+type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string};
+
+type ChangePolicyParams = {fromPolicy: string; toPolicy: string};
+
+type ChangeTypeParams = {oldType: string; newType: string};
+
+type DelegateSubmitParams = {delegateUser: string; originalManager: string};
+
+type AccountOwnerParams = {accountOwnerEmail: string};
+
+type ExportedToIntegrationParams = {label: string; markedManually?: boolean; inProgress?: boolean; lastModified?: string};
+
+type IntegrationsMessageParams = {
+ label: string;
+ result: {
+ code?: number;
+ messages?: string[];
+ title?: string;
+ link?: {
+ url: string;
+ text: string;
+ };
+ };
+};
+
+type MarkedReimbursedParams = {amount: string; currency: string};
+
+type MarkReimbursedFromIntegrationParams = {amount: string; currency: string};
+
+type ShareParams = {to: string};
+
+type UnshareParams = {to: string};
+
+type StripePaidParams = {amount: string; currency: string};
+
+type UnapprovedParams = {amount: string; currency: string};
+
+type RemoveMembersWarningPrompt = {
+ memberName: string;
+ ownerName: string;
+};
+
+type RemoveMemberPromptParams = {
+ memberName: string;
+};
+
+type DeleteExpenseTranslationParams = {
+ count: number;
+};
+
+type IssueVirtualCardParams = {
+ assignee: string;
+ link: string;
+};
+
+type ApprovalWorkflowErrorParams = {
+ name1: string;
+ name2: string;
+};
+
+type ConnectionNameParams = {
+ connectionName: ConnectionName;
+};
+
+type LastSyncDateParams = {
+ connectionName: string;
+ formattedDate: string;
+};
+
+type CustomersOrJobsLabelParams = {
+ importFields: string[];
+ importType: string;
+};
+
+type ExportAgainModalDescriptionParams = {
+ reportName: string;
+ connectionName: ConnectionName;
+};
+
+type IntegrationSyncFailedParams = {label: string; errorMessage: string};
+
+type AddEmployeeParams = {email: string; role: string};
+
+type UpdateRoleParams = {email: string; currentRole: string; newRole: string};
+
+type RemoveMemberParams = {email: string; role: string};
+
+type DateParams = {date: string};
+
+type FiltersAmountBetweenParams = {greaterThan: string; lessThan: string};
+
+type StatementPageTitleParams = {year: string | number; monthName: string};
+
+type DisconnectPromptParams = {currentIntegration?: ConnectionName} | undefined;
+
+type DisconnectTitleParams = {integration?: ConnectionName} | undefined;
+
+type AmountWithCurrencyParams = {amountWithCurrency: string};
+
+type SelectedNumberParams = {selectedNumber: number};
+
+type LowerUpperParams = {lower: string; upper: string};
+
+type CategoryNameParams = {categoryName: string};
+
+type TaxAmountParams = {taxAmount: number};
+
+type SecondaryLoginParams = {secondaryLogin: string};
+
+type OwnerOwesAmountParams = {amount: string; email: string};
+
+type ChangeOwnerSubscriptionParams = {usersCount: number; finalCount: number};
+
+type ChangeOwnerDuplicateSubscriptionParams = {email: string; workspaceName: string};
+
+type ChangeOwnerHasFailedSettlementsParams = {email: string};
+
+type ActionsAreCurrentlyRestricted = {workspaceName: string};
+
+type WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams = {workspaceOwnerName: string};
+
+type RenamedWorkspaceNameActionParams = {oldName: string; newName: string};
+
+type StatementTitleParams = {year: number | string; monthName: string};
+
+type BadgeFreeTrialParams = {numOfDays: number};
+
+type BillingBannerSubtitleWithDateParams = {date: string};
+
+type BillingBannerDisputePendingParams = {amountOwed: number; cardEnding: string};
+
+type BillingBannerCardAuthenticationRequiredParams = {cardEnding: string};
+
+type BillingBannerInsufficientFundsParams = {amountOwed: number};
+
+type BillingBannerCardExpiredParams = {amountOwed: number};
+
+type BillingBannerCardOnDisputeParams = {amountOwed: string; cardEnding: string};
+
+type TrialStartedTitleParams = {numOfDays: number};
+
+type CardNextPaymentParams = {nextPaymentDate: string};
+
+type CardEndingParams = {cardNumber: string};
+
+type CardInfoParams = {name: string; expiration: string; currency: string};
+
+type YourPlanPriceParams = {lower: string; upper: string};
+
+type SubscriptionSizeParams = {size: number};
+
+type SubscriptionCommitmentParams = {size: number; date: string};
+
+type SubscriptionSettingsSaveUpToParams = {amountWithCurrency: string};
+
+type SubscriptionSettingsRenewsOnParams = {date: string};
+
+type UnapproveWithIntegrationWarningParams = {accountingIntegration: string};
+
+type IncorrectZipFormatParams = {zipFormat?: string} | undefined;
+
+type ExportIntegrationSelectedParams = {connectionName: ConnectionName};
+
+type DefaultVendorDescriptionParams = {isReimbursable: boolean};
+
+type RequiredFieldParams = {fieldName: string};
+
+type ImportFieldParams = {importField: string};
+
+type DimensionsCountParams = {dimensionsCount: number};
+
+type IntacctMappingTitleParams = {mappingName: SageIntacctMappingName};
+
+type AgeParams = {age: number};
+
+type LastSyncAccountingParams = {relativeDate: string};
+
+type SyncStageNameConnectionsParams = {stage: PolicyConnectionSyncStage};
+
+type ReconciliationWorksParams = {lastFourPAN: string};
+
+type DelegateRoleParams = {role: DelegateRole};
+
+type DelegatorParams = {delegator: string};
+
+type RoleNamesParams = {role: string};
+
+type AssignCardParams = {
+ assignee: string;
+ feed: string;
+};
+
+type SpreadSheetColumnParams = {
+ name: string;
+};
+
+type SpreadFieldNameParams = {
+ fieldName: string;
+};
+
+type SpreadCategoriesParams = {
+ categories: number;
+};
+
+type AssignedYouCardParams = {
+ assigner: string;
+};
+
+type FeatureNameParams = {
+ featureName: string;
+};
+
+type AutoPayApprovedReportsLimitErrorParams = {
+ currency?: string;
+};
+
+type DefaultAmountParams = {
+ defaultAmount: string;
+};
+
+type RemovedFromApprovalWorkflowParams = {
+ submittersNames: string[];
+};
+
+type IntegrationExportParams = {
+ integration: string;
+ type?: string;
+};
+
+type ConnectionParams = {
+ connection: string;
+};
+
+type MissingPropertyParams = {
+ propertyName: string;
+};
+
+type InvalidPropertyParams = {
+ propertyName: string;
+ expectedType: string;
+};
+
+type InvalidValueParams = {
+ expectedValues: string;
+};
+
+type ImportTagsSuccessfullDescriptionParams = {
+ tags: number;
+};
+
+type ImportedTagsMessageParams = {
+ columnCounts: number;
+};
+
+type ImportMembersSuccessfullDescriptionParams = {
+ members: number;
+};
+
+type AuthenticationErrorParams = {
+ connectionName: string;
+};
+
+export type {
+ AuthenticationErrorParams,
+ ImportMembersSuccessfullDescriptionParams,
+ ImportedTagsMessageParams,
+ ImportTagsSuccessfullDescriptionParams,
+ MissingPropertyParams,
+ InvalidPropertyParams,
+ InvalidValueParams,
+ ConnectionParams,
+ IntegrationExportParams,
+ RemovedFromApprovalWorkflowParams,
+ DefaultAmountParams,
+ AutoPayApprovedReportsLimitErrorParams,
+ FeatureNameParams,
+ SpreadSheetColumnParams,
+ SpreadFieldNameParams,
+ AssignedYouCardParams,
+ SpreadCategoriesParams,
+ DelegateRoleParams,
+ DelegatorParams,
+ ReconciliationWorksParams,
+ LastSyncAccountingParams,
+ SyncStageNameConnectionsParams,
+ AgeParams,
+ RequiredFieldParams,
+ DimensionsCountParams,
+ IntacctMappingTitleParams,
+ ImportFieldParams,
+ AssigneeParams,
+ DefaultVendorDescriptionParams,
+ ExportIntegrationSelectedParams,
+ UnapproveWithIntegrationWarningParams,
+ IncorrectZipFormatParams,
+ CardNextPaymentParams,
+ CardEndingParams,
+ CardInfoParams,
+ YourPlanPriceParams,
+ SubscriptionSizeParams,
+ SubscriptionCommitmentParams,
+ SubscriptionSettingsSaveUpToParams,
+ SubscriptionSettingsRenewsOnParams,
+ BadgeFreeTrialParams,
+ BillingBannerSubtitleWithDateParams,
+ BillingBannerDisputePendingParams,
+ BillingBannerCardAuthenticationRequiredParams,
+ BillingBannerInsufficientFundsParams,
+ BillingBannerCardExpiredParams,
+ BillingBannerCardOnDisputeParams,
+ TrialStartedTitleParams,
+ RemoveMemberPromptParams,
+ StatementTitleParams,
+ RenamedWorkspaceNameActionParams,
+ WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams,
+ ActionsAreCurrentlyRestricted,
+ ChangeOwnerHasFailedSettlementsParams,
+ OwnerOwesAmountParams,
+ ChangeOwnerDuplicateSubscriptionParams,
+ ChangeOwnerSubscriptionParams,
+ SecondaryLoginParams,
+ TaxAmountParams,
+ CategoryNameParams,
+ SelectedNumberParams,
+ AmountWithCurrencyParams,
+ LowerUpperParams,
+ LogSizeAndDateParams,
+ AddressLineParams,
+ AdminCanceledRequestParams,
+ AlreadySignedInParams,
+ ApprovedAmountParams,
+ BeginningOfChatHistoryAdminRoomPartOneParams,
+ BeginningOfChatHistoryAnnounceRoomPartOneParams,
+ BeginningOfChatHistoryAnnounceRoomPartTwo,
+ BeginningOfChatHistoryDomainRoomPartOneParams,
+ CanceledRequestParams,
+ CharacterLimitParams,
+ ConfirmHoldExpenseParams,
+ ConfirmThatParams,
+ CompanyCardFeedNameParams,
+ DateShouldBeAfterParams,
+ DateShouldBeBeforeParams,
+ DeleteActionParams,
+ DeleteConfirmationParams,
+ DidSplitAmountMessageParams,
+ DistanceRateOperationsParams,
+ EditActionParams,
+ ElectronicFundsParams,
+ EnterMagicCodeParams,
+ FormattedMaxLengthParams,
+ ForwardedAmountParams,
+ GoBackMessageParams,
+ GoToRoomParams,
+ HeldRequestParams,
+ InstantSummaryParams,
+ IssueVirtualCardParams,
+ LocalTimeParams,
+ LogSizeParams,
+ LoggedInAsParams,
+ ManagerApprovedAmountParams,
+ ManagerApprovedParams,
+ SignUpNewFaceCodeParams,
+ NoLongerHaveAccessParams,
+ NotAllowedExtensionParams,
+ NotYouParams,
+ OOOEventSummaryFullDayParams,
+ OOOEventSummaryPartialDayParams,
+ OurEmailProviderParams,
+ PaidElsewhereWithAmountParams,
+ PaidWithExpensifyWithAmountParams,
+ ParentNavigationSummaryParams,
+ PaySomeoneParams,
+ PayerOwesAmountParams,
+ PayerOwesParams,
+ RoleNamesParams,
+ PayerPaidAmountParams,
+ PayerPaidParams,
+ PayerSettledParams,
+ ReimbursementRateParams,
+ RemovedTheRequestParams,
+ RenamedRoomActionParams,
+ ReportArchiveReasonsClosedParams,
+ ReportArchiveReasonsMergedParams,
+ ReportPolicyNameParams,
+ ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
+ ReportArchiveReasonsRemovedFromPolicyParams,
+ RequestAmountParams,
+ RequestCountParams,
+ RequestedAmountMessageParams,
+ ResolutionConstraintsParams,
+ RoomNameReservedErrorParams,
+ RoomRenamedToParams,
+ SetTheDistanceMerchantParams,
+ SetTheRequestParams,
+ SettleExpensifyCardParams,
+ SettledAfterAddedBankAccountParams,
+ SizeExceededParams,
+ SplitAmountParams,
+ StepCounterParams,
+ TaskCreatedActionParams,
+ TermsParams,
+ ThreadRequestReportNameParams,
+ ThreadSentMoneyReportNameParams,
+ ToValidateLoginParams,
+ TransferParams,
+ UntilTimeParams,
+ UpdatedTheDistanceMerchantParams,
+ UpdatedTheRequestParams,
+ UsePlusButtonParams,
+ UserIsAlreadyMemberParams,
+ UserSplitParams,
+ ViolationsAutoReportedRejectedExpenseParams,
+ ViolationsCashExpenseWithNoReceiptParams,
+ ViolationsConversionSurchargeParams,
+ ViolationsInvoiceMarkupParams,
+ ViolationsMaxAgeParams,
+ ViolationsMissingTagParams,
+ ViolationsModifiedAmountParams,
+ ViolationsOverAutoApprovalLimitParams,
+ ViolationsOverCategoryLimitParams,
+ ViolationsOverLimitParams,
+ ViolationsPerDayLimitParams,
+ ViolationsReceiptRequiredParams,
+ ViolationsRterParams,
+ ViolationsTagOutOfPolicyParams,
+ ViolationsTaxOutOfPolicyParams,
+ WaitingOnBankAccountParams,
+ WalletProgramParams,
+ WeSentYouMagicSignInLinkParams,
+ WelcomeEnterMagicCodeParams,
+ WelcomeNoteParams,
+ WelcomeToRoomParams,
+ ZipCodeExampleFormatParams,
+ ChangeFieldParams,
+ ChangePolicyParams,
+ ChangeTypeParams,
+ ExportedToIntegrationParams,
+ DelegateSubmitParams,
+ AccountOwnerParams,
+ IntegrationsMessageParams,
+ MarkedReimbursedParams,
+ MarkReimbursedFromIntegrationParams,
+ ShareParams,
+ UnshareParams,
+ StripePaidParams,
+ UnapprovedParams,
+ RemoveMembersWarningPrompt,
+ DeleteExpenseTranslationParams,
+ ApprovalWorkflowErrorParams,
+ ConnectionNameParams,
+ LastSyncDateParams,
+ CustomersOrJobsLabelParams,
+ ExportAgainModalDescriptionParams,
+ IntegrationSyncFailedParams,
+ AddEmployeeParams,
+ UpdateRoleParams,
+ RemoveMemberParams,
+ DateParams,
+ FiltersAmountBetweenParams,
+ StatementPageTitleParams,
+ DisconnectPromptParams,
+ DisconnectTitleParams,
+ CharacterLengthLimitParams,
+ OptionalParam,
+ AssignCardParams,
+};
diff --git a/src/languages/translations.ts b/src/languages/translations.ts
index 4d89f1f529de..ec99d999f94e 100644
--- a/src/languages/translations.ts
+++ b/src/languages/translations.ts
@@ -1,7 +1,7 @@
import en from './en';
import es from './es';
import esES from './es-ES';
-import type {TranslationBase, TranslationFlatObject} from './types';
+import type {FlatTranslationsObject, TranslationDeepObject} from './types';
/**
* Converts an object to it's flattened version.
@@ -12,10 +12,10 @@ import type {TranslationBase, TranslationFlatObject} from './types';
*/
// Necessary to export so that it is accessible to the unit tests
// eslint-disable-next-line rulesdir/no-inline-named-export
-export function flattenObject(obj: TranslationBase): TranslationFlatObject {
+export function flattenObject(obj: TranslationDeepObject): FlatTranslationsObject {
const result: Record = {};
- const recursive = (data: TranslationBase, key: string): void => {
+ const recursive = (data: TranslationDeepObject, key: string): void => {
// If the data is a function or not a object (eg. a string or array),
// it's the final value for the key being built and there is no need
// for more recursion
@@ -27,7 +27,7 @@ export function flattenObject(obj: TranslationBase): TranslationFlatObject {
// Recursive call to the keys and connect to the respective data
Object.keys(data).forEach((k) => {
isEmpty = false;
- recursive(data[k] as TranslationBase, key ? `${key}.${k}` : k);
+ recursive(data[k] as TranslationDeepObject, key ? `${key}.${k}` : k);
});
// Check for when the object is empty but a key exists, so that
@@ -39,7 +39,7 @@ export function flattenObject(obj: TranslationBase): TranslationFlatObject {
};
recursive(obj, '');
- return result as TranslationFlatObject;
+ return result as FlatTranslationsObject;
}
export default {
diff --git a/src/languages/types.ts b/src/languages/types.ts
index a7a11fafb27b..0bdf740d982e 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -1,278 +1,53 @@
-import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx';
-import type {Unit} from '@src/types/onyx/Policy';
-import type {ViolationDataType} from '@src/types/onyx/TransactionViolation';
+/* eslint-disable @typescript-eslint/no-explicit-any */
import type en from './en';
-type AddressLineParams = {
- lineNumber: number;
-};
-
-type CharacterLimitParams = {
- limit: number;
-};
-
-type ZipCodeExampleFormatParams = {
- zipSampleFormat: string;
-};
-
-type LoggedInAsParams = {
- email: string;
-};
-
-type SignUpNewFaceCodeParams = {
- login: string;
-};
-
-type WelcomeEnterMagicCodeParams = {
- login: string;
-};
-
-type AlreadySignedInParams = {
- email: string;
-};
-
-type GoBackMessageParams = {
- provider: string;
-};
-
-type LocalTimeParams = {
- user: string;
- time: string;
-};
-
-type EditActionParams = {
- action: OnyxInputOrEntry;
-};
-
-type DeleteActionParams = {
- action: OnyxInputOrEntry;
-};
-
-type DeleteConfirmationParams = {
- action: OnyxInputOrEntry;
-};
-
-type BeginningOfChatHistoryDomainRoomPartOneParams = {
- domainRoom: string;
-};
-
-type BeginningOfChatHistoryAdminRoomPartOneParams = {
- workspaceName: string;
-};
-
-type BeginningOfChatHistoryAnnounceRoomPartOneParams = {
- workspaceName: string;
-};
-
-type BeginningOfChatHistoryAnnounceRoomPartTwo = {
- workspaceName: string;
-};
-
-type WelcomeToRoomParams = {
- roomName: string;
-};
-
-type UsePlusButtonParams = {
- additionalText: string;
-};
-
-type ReportArchiveReasonsClosedParams = {
- displayName: string;
-};
-
-type ReportArchiveReasonsMergedParams = {
- displayName: string;
- oldDisplayName: string;
-};
-
-type ReportArchiveReasonsRemovedFromPolicyParams = {
- displayName: string;
- policyName: string;
- shouldUseYou?: boolean;
-};
-
-type ReportArchiveReasonsPolicyDeletedParams = {
- policyName: string;
-};
-
-type ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams = {
- policyName: string;
-};
-
-type RequestCountParams = {
- count: number;
- scanningReceipts: number;
- pendingReceipts: number;
-};
-
-type SettleExpensifyCardParams = {
- formattedAmount: string;
-};
-
-type RequestAmountParams = {amount: string};
-
-type RequestedAmountMessageParams = {formattedAmount: string; comment?: string};
-
-type SplitAmountParams = {amount: string};
-
-type DidSplitAmountMessageParams = {formattedAmount: string; comment: string};
-
-type UserSplitParams = {amount: string};
-
-type PayerOwesAmountParams = {payer: string; amount: number | string; comment?: string};
-
-type PayerOwesParams = {payer: string};
-
-type CompanyCardFeedNameParams = {feedName: string};
-
-type PayerPaidAmountParams = {payer?: string; amount: number | string};
-
-type ApprovedAmountParams = {amount: number | string};
-
-type ForwardedAmountParams = {amount: number | string};
-
-type ManagerApprovedParams = {manager: string};
-
-type ManagerApprovedAmountParams = {manager: string; amount: number | string};
-
-type PayerPaidParams = {payer: string};
-
-type PayerSettledParams = {amount: number | string};
-
-type WaitingOnBankAccountParams = {submitterDisplayName: string};
-
-type CanceledRequestParams = {amount: string; submitterDisplayName: string};
-
-type AdminCanceledRequestParams = {manager: string; amount: string};
-
-type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string};
-
-type PaidElsewhereWithAmountParams = {payer?: string; amount: string};
-
-type PaidWithExpensifyWithAmountParams = {payer?: string; amount: string};
-
-type ThreadRequestReportNameParams = {formattedAmount: string; comment: string};
-
-type ThreadSentMoneyReportNameParams = {formattedAmount: string; comment: string};
-
-type SizeExceededParams = {maxUploadSizeInMB: number};
-
-type ResolutionConstraintsParams = {minHeightInPx: number; minWidthInPx: number; maxHeightInPx: number; maxWidthInPx: number};
-
-type NotAllowedExtensionParams = {allowedExtensions: string[]};
-
-type EnterMagicCodeParams = {contactMethod: string};
-
-type TransferParams = {amount: string};
-
-type InstantSummaryParams = {rate: string; minAmount: string};
-
-type NotYouParams = {user: string};
-
-type DateShouldBeBeforeParams = {dateString: string};
-
-type DateShouldBeAfterParams = {dateString: string};
-
-type WeSentYouMagicSignInLinkParams = {login: string; loginType: string};
-
-type ToValidateLoginParams = {primaryLogin: string; secondaryLogin: string};
-
-type NoLongerHaveAccessParams = {primaryLogin: string};
-
-type OurEmailProviderParams = {login: string};
-
-type ConfirmThatParams = {login: string};
-
-type UntilTimeParams = {time: string};
-
-type StepCounterParams = {step: number; total?: number; text?: string};
-
-type UserIsAlreadyMemberParams = {login: string; name: string};
-
-type GoToRoomParams = {roomName: string};
-
-type WelcomeNoteParams = {workspaceName: string};
-
-type RoomNameReservedErrorParams = {reservedName: string};
-
-type RenamedRoomActionParams = {oldName: string; newName: string};
-
-type RoomRenamedToParams = {newName: string};
-
-type OOOEventSummaryFullDayParams = {summary: string; dayCount: number; date: string};
-
-type OOOEventSummaryPartialDayParams = {summary: string; timePeriod: string; date: string};
-
-type ParentNavigationSummaryParams = {reportName?: string; workspaceName?: string};
-
-type SetTheRequestParams = {valueName: string; newValueToDisplay: string};
-
-type SetTheDistanceMerchantParams = {translatedChangedField: string; newMerchant: string; newAmountToDisplay: string};
-
-type RemovedTheRequestParams = {valueName: string; oldValueToDisplay: string};
-
-type UpdatedTheRequestParams = {valueName: string; newValueToDisplay: string; oldValueToDisplay: string};
-
-type UpdatedTheDistanceMerchantParams = {translatedChangedField: string; newMerchant: string; oldMerchant: string; newAmountToDisplay: string; oldAmountToDisplay: string};
-
-type FormattedMaxLengthParams = {formattedMaxLength: string};
-
-type WalletProgramParams = {walletProgram: string};
-
-type ViolationsAutoReportedRejectedExpenseParams = {rejectedBy: string; rejectReason: string};
-
-type ViolationsCashExpenseWithNoReceiptParams = {formattedLimit?: string};
-
-type ViolationsConversionSurchargeParams = {surcharge?: number};
-
-type ViolationsInvoiceMarkupParams = {invoiceMarkup?: number};
-
-type ViolationsMaxAgeParams = {maxAge: number};
-
-type ViolationsMissingTagParams = {tagName?: string};
-
-type ViolationsModifiedAmountParams = {type?: ViolationDataType; displayPercentVariance?: number};
-
-type ViolationsOverAutoApprovalLimitParams = {formattedLimit?: string};
-
-type ViolationsOverCategoryLimitParams = {formattedLimit?: string};
-
-type ViolationsOverLimitParams = {formattedLimit?: string};
-
-type ViolationsPerDayLimitParams = {formattedLimit?: string};
-
-type ViolationsReceiptRequiredParams = {formattedLimit?: string; category?: string};
-
-type ViolationsRterParams = {
- brokenBankConnection: boolean;
- isAdmin: boolean;
- email?: string;
- isTransactionOlderThan7Days: boolean;
- member?: string;
-};
-
-type ViolationsTagOutOfPolicyParams = {tagName?: string};
-
-type ViolationsTaxOutOfPolicyParams = {taxName?: string};
-
-type PaySomeoneParams = {name?: string};
-
-type TaskCreatedActionParams = {title: string};
-
-/* Translation Object types */
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type TranslationBaseValue = string | string[] | ((...args: any[]) => string);
-
-type TranslationBase = {[key: string]: TranslationBaseValue | TranslationBase};
-
-/* Flat Translation Object types */
-// Flattens an object and returns concatenations of all the keys of nested objects
-type FlattenObject = {
+type PluralParams = {count: number};
+type PluralHandler = ((count: number) => string) | string;
+type PluralForm = {
+ zero?: string;
+ one: string;
+ two?: string;
+ few?: PluralHandler;
+ many?: PluralHandler;
+ other: PluralHandler;
+};
+
+/**
+ * Retrieves the first argument of a function
+ */
+type FirstArgument = TFunction extends (arg: infer A, ...args: any[]) => any ? A : never;
+
+/**
+ * Translation value can be a string or a function that returns a string
+ */
+type TranslationLeafValue = TStringOrFunction extends string
+ ? string
+ : (
+ arg: FirstArgument extends Record | undefined ? FirstArgument : Record,
+ ...noOtherArguments: unknown[]
+ ) => string | PluralForm;
+
+/**
+ * Translation object is a recursive object that can contain other objects or string/function values
+ */
+type TranslationDeepObject = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- [TKey in keyof TObject]: TObject[TKey] extends (...args: any[]) => any
- ? `${TPrefix}${TKey & string}`
- : // eslint-disable-next-line @typescript-eslint/no-explicit-any
- TObject[TKey] extends any[]
+ [Path in keyof TTranslations]: TTranslations[Path] extends string | ((...args: any[]) => any)
+ ? TranslationLeafValue
+ : TTranslations[Path] extends number | boolean | null | undefined | unknown[]
+ ? string
+ : TranslationDeepObject;
+};
+
+/**
+ * Flattens an object and returns concatenations of all the keys of nested objects
+ *
+ * Ex:
+ * Input: { common: { yes: "Yes", no: "No" }}
+ * Output: "common.yes" | "common.no"
+ */
+type FlattenObject = {
+ [TKey in keyof TObject]: TObject[TKey] extends (arg: any) => any
? `${TPrefix}${TKey & string}`
: // eslint-disable-next-line @typescript-eslint/ban-types
TObject[TKey] extends object
@@ -280,222 +55,43 @@ type FlattenObject = {
: `${TPrefix}${TKey & string}`;
}[keyof TObject];
-// Retrieves a type for a given key path (calculated from the type above)
-type TranslateType = TPath extends keyof TObject
- ? TObject[TPath]
- : TPath extends `${infer TKey}.${infer TRest}`
- ? TKey extends keyof TObject
- ? TranslateType
+/**
+ * Retrieves a type for a given key path (calculated from the type above)
+ */
+type TranslationValue = TKey extends keyof TObject
+ ? TObject[TKey]
+ : TKey extends `${infer TPathKey}.${infer TRest}`
+ ? TPathKey extends keyof TObject
+ ? TranslationValue
: never
: never;
-type EnglishTranslation = typeof en;
-
-type TranslationPaths = FlattenObject;
-
-type TranslationFlatObject = {
- [TKey in TranslationPaths]: TranslateType;
-};
-
-type TermsParams = {amount: string};
-
-type ElectronicFundsParams = {percentage: string; amount: string};
-
-type LogSizeParams = {size: number};
-
-type HeldRequestParams = {comment: string};
-
-type DistanceRateOperationsParams = {count: number};
-
-type ReimbursementRateParams = {unit: Unit};
-
-type ConfirmHoldExpenseParams = {transactionCount: number};
-
-type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string};
-
-type ChangePolicyParams = {fromPolicy: string; toPolicy: string};
-
-type ChangeTypeParams = {oldType: string; newType: string};
-
-type DelegateSubmitParams = {delegateUser: string; originalManager: string};
-
-type AccountOwnerParams = {accountOwnerEmail: string};
-
-type ExportedToIntegrationParams = {label: string; markedManually?: boolean; inProgress?: boolean; lastModified?: string};
-
-type IntegrationsMessageParams = {
- label: string;
- result: {
- code?: number;
- messages?: string[];
- title?: string;
- link?: {
- url: string;
- text: string;
- };
- };
-};
-
-type MarkedReimbursedParams = {amount: string; currency: string};
-
-type MarkReimbursedFromIntegrationParams = {amount: string; currency: string};
-
-type ShareParams = {to: string};
-
-type UnshareParams = {to: string};
-
-type StripePaidParams = {amount: string; currency: string};
-
-type UnapprovedParams = {amount: string; currency: string};
-type RemoveMembersWarningPrompt = {
- memberName: string;
- ownerName: string;
-};
-
-type DeleteExpenseTranslationParams = {
- count: number;
-};
-
-type IssueVirtualCardParams = {
- assignee: string;
- link: string;
-};
-
-type ApprovalWorkflowErrorParams = {
- name1: string;
- name2: string;
-};
-
-type AssignCardParams = {
- assignee: string;
- feed: string;
-};
-
-export type {
- AddressLineParams,
- AdminCanceledRequestParams,
- AlreadySignedInParams,
- ApprovedAmountParams,
- BeginningOfChatHistoryAdminRoomPartOneParams,
- BeginningOfChatHistoryAnnounceRoomPartOneParams,
- BeginningOfChatHistoryAnnounceRoomPartTwo,
- BeginningOfChatHistoryDomainRoomPartOneParams,
- CanceledRequestParams,
- CharacterLimitParams,
- ConfirmHoldExpenseParams,
- ConfirmThatParams,
- CompanyCardFeedNameParams,
- DateShouldBeAfterParams,
- DateShouldBeBeforeParams,
- DeleteActionParams,
- DeleteConfirmationParams,
- DidSplitAmountMessageParams,
- DistanceRateOperationsParams,
- EditActionParams,
- ElectronicFundsParams,
- EnglishTranslation,
- EnterMagicCodeParams,
- FormattedMaxLengthParams,
- ForwardedAmountParams,
- GoBackMessageParams,
- GoToRoomParams,
- HeldRequestParams,
- InstantSummaryParams,
- IssueVirtualCardParams,
- LocalTimeParams,
- LogSizeParams,
- LoggedInAsParams,
- ManagerApprovedAmountParams,
- ManagerApprovedParams,
- SignUpNewFaceCodeParams,
- NoLongerHaveAccessParams,
- NotAllowedExtensionParams,
- NotYouParams,
- OOOEventSummaryFullDayParams,
- OOOEventSummaryPartialDayParams,
- OurEmailProviderParams,
- PaidElsewhereWithAmountParams,
- PaidWithExpensifyWithAmountParams,
- ParentNavigationSummaryParams,
- PaySomeoneParams,
- PayerOwesAmountParams,
- PayerOwesParams,
- PayerPaidAmountParams,
- PayerPaidParams,
- PayerSettledParams,
- ReimbursementRateParams,
- RemovedTheRequestParams,
- RenamedRoomActionParams,
- ReportArchiveReasonsClosedParams,
- ReportArchiveReasonsMergedParams,
- ReportArchiveReasonsPolicyDeletedParams,
- ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
- ReportArchiveReasonsRemovedFromPolicyParams,
- RequestAmountParams,
- RequestCountParams,
- RequestedAmountMessageParams,
- ResolutionConstraintsParams,
- RoomNameReservedErrorParams,
- RoomRenamedToParams,
- SetTheDistanceMerchantParams,
- SetTheRequestParams,
- SettleExpensifyCardParams,
- SettledAfterAddedBankAccountParams,
- SizeExceededParams,
- SplitAmountParams,
- StepCounterParams,
- TaskCreatedActionParams,
- TermsParams,
- ThreadRequestReportNameParams,
- ThreadSentMoneyReportNameParams,
- ToValidateLoginParams,
- TransferParams,
- TranslationBase,
- TranslationFlatObject,
- TranslationPaths,
- UntilTimeParams,
- UpdatedTheDistanceMerchantParams,
- UpdatedTheRequestParams,
- UsePlusButtonParams,
- UserIsAlreadyMemberParams,
- UserSplitParams,
- ViolationsAutoReportedRejectedExpenseParams,
- ViolationsCashExpenseWithNoReceiptParams,
- ViolationsConversionSurchargeParams,
- ViolationsInvoiceMarkupParams,
- ViolationsMaxAgeParams,
- ViolationsMissingTagParams,
- ViolationsModifiedAmountParams,
- ViolationsOverAutoApprovalLimitParams,
- ViolationsOverCategoryLimitParams,
- ViolationsOverLimitParams,
- ViolationsPerDayLimitParams,
- ViolationsReceiptRequiredParams,
- ViolationsRterParams,
- ViolationsTagOutOfPolicyParams,
- ViolationsTaxOutOfPolicyParams,
- WaitingOnBankAccountParams,
- WalletProgramParams,
- WeSentYouMagicSignInLinkParams,
- WelcomeEnterMagicCodeParams,
- WelcomeNoteParams,
- WelcomeToRoomParams,
- ZipCodeExampleFormatParams,
- ChangeFieldParams,
- ChangePolicyParams,
- ChangeTypeParams,
- ExportedToIntegrationParams,
- DelegateSubmitParams,
- AccountOwnerParams,
- IntegrationsMessageParams,
- MarkedReimbursedParams,
- MarkReimbursedFromIntegrationParams,
- ShareParams,
- UnshareParams,
- StripePaidParams,
- UnapprovedParams,
- RemoveMembersWarningPrompt,
- DeleteExpenseTranslationParams,
- ApprovalWorkflowErrorParams,
- AssignCardParams,
-};
+/**
+ * English is the default translation, other languages will be type-safe based on this
+ */
+type DefaultTranslation = typeof en;
+
+/**
+ * Flattened default translation object
+ */
+type TranslationPaths = FlattenObject;
+
+/**
+ * Flattened default translation object with its values
+ */
+type FlatTranslationsObject = {
+ [Path in TranslationPaths]: TranslationValue;
+};
+
+/**
+ * Determines the expected parameters for a specific translation function based on the provided translation path
+ */
+type TranslationParameters = FlatTranslationsObject[TKey] extends (...args: infer Args) => infer Return
+ ? Return extends PluralForm
+ ? Args[0] extends undefined
+ ? [PluralParams]
+ : [Args[0] & PluralParams]
+ : Args
+ : never[];
+
+export type {DefaultTranslation, TranslationDeepObject, TranslationPaths, PluralForm, TranslationValue, FlatTranslationsObject, TranslationParameters};
diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index 65fd2b6ad015..be1706886b1f 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -116,10 +116,10 @@ function processRequest(request: OnyxRequest, type: ApiRequestType): Promise(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void {
+function write(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): Promise {
Log.info('[API] Called API write', false, {command, ...apiCommandParameters});
const request = prepareRequest(command, CONST.API_REQUEST_TYPE.WRITE, apiCommandParameters, onyxData);
- processRequest(request, CONST.API_REQUEST_TYPE.WRITE);
+ return processRequest(request, CONST.API_REQUEST_TYPE.WRITE);
}
/**
diff --git a/src/libs/API/parameters/ResolveDuplicatesParams.ts b/src/libs/API/parameters/ResolveDuplicatesParams.ts
new file mode 100644
index 000000000000..d225f227c0d7
--- /dev/null
+++ b/src/libs/API/parameters/ResolveDuplicatesParams.ts
@@ -0,0 +1,24 @@
+type ResolveDuplicatesParams = {
+ /** The ID of the transaction that we want to keep */
+ transactionID: string;
+
+ /** The list of other duplicated transactions */
+ transactionIDList: string[];
+ created: string;
+ merchant: string;
+ amount: number;
+ currency: string;
+ category: string;
+ comment: string;
+ billable: boolean;
+ reimbursable: boolean;
+ tag: string;
+
+ /** The reportActionID of the dismissed violation action in the kept transaction thread report */
+ dismissedViolationReportActionID: string;
+
+ /** The ID list of the hold report actions corresponding to the transactionIDList */
+ reportActionIDList: string[];
+};
+
+export default ResolveDuplicatesParams;
diff --git a/src/libs/API/parameters/SaveSearch.ts b/src/libs/API/parameters/SaveSearch.ts
index e0ad38dd8363..9dd3416320c7 100644
--- a/src/libs/API/parameters/SaveSearch.ts
+++ b/src/libs/API/parameters/SaveSearch.ts
@@ -2,7 +2,7 @@ import type {SearchQueryString} from '@components/Search/types';
type SaveSearchParams = {
jsonQuery: SearchQueryString;
- name?: string;
+ newName?: string;
};
export default SaveSearchParams;
diff --git a/src/libs/API/parameters/SetPolicyAutoReimbursementLimit.ts b/src/libs/API/parameters/SetPolicyAutoReimbursementLimit.ts
index 7c6a721e03b0..b743369db926 100644
--- a/src/libs/API/parameters/SetPolicyAutoReimbursementLimit.ts
+++ b/src/libs/API/parameters/SetPolicyAutoReimbursementLimit.ts
@@ -1,6 +1,6 @@
type SetPolicyAutoReimbursementLimitParams = {
policyID: string;
- autoReimbursement: {limit: number};
+ limit: number;
};
export default SetPolicyAutoReimbursementLimitParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index e5cde1b77be7..9f51cab3f360 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -238,6 +238,7 @@ export type {default as SendInvoiceParams} from './SendInvoiceParams';
export type {default as PayInvoiceParams} from './PayInvoiceParams';
export type {default as MarkAsCashParams} from './MarkAsCashParams';
export type {default as TransactionMergeParams} from './TransactionMergeParams';
+export type {default as ResolveDuplicatesParams} from './ResolveDuplicatesParams';
export type {default as UpdateSubscriptionTypeParams} from './UpdateSubscriptionTypeParams';
export type {default as SignUpUserParams} from './SignUpUserParams';
export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscriptionAutoRenewParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 88372382d3c8..b72b77ae4739 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -90,6 +90,7 @@ const WRITE_COMMANDS = {
SIGN_IN_WITH_GOOGLE: 'SignInWithGoogle',
SIGN_IN_USER: 'SigninUser',
SIGN_IN_USER_WITH_LINK: 'SigninUserWithLink',
+ SEARCH: 'Search',
REQUEST_UNLINK_VALIDATION_LINK: 'RequestUnlinkValidationLink',
UNLINK_LOGIN: 'UnlinkLogin',
ENABLE_TWO_FACTOR_AUTH: 'EnableTwoFactorAuth',
@@ -284,6 +285,7 @@ const WRITE_COMMANDS = {
PAY_INVOICE: 'PayInvoice',
MARK_AS_CASH: 'MarkAsCash',
TRANSACTION_MERGE: 'Transaction_Merge',
+ RESOLVE_DUPLICATES: 'ResolveDuplicates',
UPDATE_SUBSCRIPTION_TYPE: 'UpdateSubscriptionType',
SIGN_UP_USER: 'SignUpUser',
UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew',
@@ -633,6 +635,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.RemovePolicyCategoryReceiptsRequiredParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_MAX_AMOUNT]: Parameters.SetPolicyCategoryMaxAmountParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_APPROVER]: Parameters.SetPolicyCategoryApproverParams;
+ [WRITE_COMMANDS.SEARCH]: Parameters.SearchParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX]: Parameters.SetPolicyCategoryTaxParams;
[WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
[WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams;
@@ -701,6 +704,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.PAY_INVOICE]: Parameters.PayInvoiceParams;
[WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams;
[WRITE_COMMANDS.TRANSACTION_MERGE]: Parameters.TransactionMergeParams;
+ [WRITE_COMMANDS.RESOLVE_DUPLICATES]: Parameters.ResolveDuplicatesParams;
[WRITE_COMMANDS.UPDATE_SUBSCRIPTION_TYPE]: Parameters.UpdateSubscriptionTypeParams;
[WRITE_COMMANDS.SIGN_UP_USER]: Parameters.SignUpUserParams;
[WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams;
@@ -872,7 +876,6 @@ const READ_COMMANDS = {
OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage',
OPEN_POLICY_PROFILE_PAGE: 'OpenPolicyProfilePage',
OPEN_POLICY_INITIAL_PAGE: 'OpenPolicyInitialPage',
- SEARCH: 'Search',
OPEN_SUBSCRIPTION_PAGE: 'OpenSubscriptionPage',
OPEN_DRAFT_DISTANCE_EXPENSE: 'OpenDraftDistanceExpense',
START_ISSUE_NEW_CARD_FLOW: 'StartIssueNewCardFlow',
@@ -930,7 +933,6 @@ type ReadCommandParameters = {
[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;
- [READ_COMMANDS.SEARCH]: Parameters.SearchParams;
[READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null;
[READ_COMMANDS.OPEN_DRAFT_DISTANCE_EXPENSE]: null;
[READ_COMMANDS.START_ISSUE_NEW_CARD_FLOW]: Parameters.StartIssueNewCardFlowParams;
diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts
index 7b2f71dbd101..f27b32360a84 100644
--- a/src/libs/CategoryUtils.ts
+++ b/src/libs/CategoryUtils.ts
@@ -38,10 +38,9 @@ function formatRequireReceiptsOverText(translate: LocaleContextProps['translate'
const maxExpenseAmountToDisplay = policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE ? 0 : policy?.maxExpenseAmount;
- return translate(
- `workspace.rules.categoryRules.requireReceiptsOverList.default`,
- CurrencyUtils.convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD),
- );
+ return translate(`workspace.rules.categoryRules.requireReceiptsOverList.default`, {
+ defaultAmount: CurrencyUtils.convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD),
+ });
}
function getCategoryApproverRule(approvalRules: ApprovalRule[], categoryName: string) {
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index 6b43a549256d..2de905ff6047 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -47,6 +47,8 @@ type CustomStatusTypes = ValueOf;
type Locale = ValueOf;
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
+const TIMEZONE_UPDATE_THROTTLE_MINUTES = 5;
+
let currentUserAccountID: number | undefined;
Onyx.connect({
key: ONYXKEYS.SESSION,
@@ -352,12 +354,12 @@ function getDaysOfWeek(preferredLocale: Locale): string[] {
return daysOfWeek.map((date) => format(date, 'eeee'));
}
-// Used to throttle updates to the timezone when necessary
-let lastUpdatedTimezoneTime = new Date();
+// Used to throttle updates to the timezone when necessary. Initialize outside the throttle window so it's updated the first time.
+let lastUpdatedTimezoneTime = subMinutes(new Date(), TIMEZONE_UPDATE_THROTTLE_MINUTES + 1);
function canUpdateTimezone(): boolean {
const currentTime = new Date();
- const fiveMinutesAgo = subMinutes(currentTime, 5);
+ const fiveMinutesAgo = subMinutes(currentTime, TIMEZONE_UPDATE_THROTTLE_MINUTES);
// Compare the last updated time with five minutes ago
return isBefore(lastUpdatedTimezoneTime, fiveMinutesAgo);
}
diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts
index cf852e533a20..78821cde5e13 100644
--- a/src/libs/ErrorUtils.ts
+++ b/src/libs/ErrorUtils.ts
@@ -1,14 +1,14 @@
import mapValues from 'lodash/mapValues';
import type {OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
-import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types';
+import type {TranslationPaths} from '@src/languages/types';
import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon';
import type Response from '@src/types/onyx/Response';
import type {ReceiptError} from '@src/types/onyx/Transaction';
import DateUtils from './DateUtils';
import * as Localize from './Localize';
-function getAuthenticateErrorMessage(response: Response): keyof TranslationFlatObject {
+function getAuthenticateErrorMessage(response: Response): TranslationPaths {
switch (response.jsonCode) {
case CONST.JSON_CODE.UNABLE_TO_RETRY:
return 'session.offlineMessageRetry';
diff --git a/src/libs/HeaderUtils.ts b/src/libs/HeaderUtils.ts
index 03c582d6b16b..b31d59804c51 100644
--- a/src/libs/HeaderUtils.ts
+++ b/src/libs/HeaderUtils.ts
@@ -17,11 +17,11 @@ function getPinMenuItem(report: OnyxReport): ThreeDotsMenuItem {
};
}
-function getShareMenuItem(report: OnyxReport): ThreeDotsMenuItem {
+function getShareMenuItem(report: OnyxReport, backTo?: string): ThreeDotsMenuItem {
return {
icon: Expensicons.QrCode,
text: Localize.translateLocal('common.share'),
- onSelected: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '')),
+ onSelected: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '', backTo)),
};
}
diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts
index c9eef3170245..bd8a34406846 100644
--- a/src/libs/Localize/index.ts
+++ b/src/libs/Localize/index.ts
@@ -6,7 +6,7 @@ import type {MessageElementBase, MessageTextElement} from '@libs/MessageElement'
import Config from '@src/CONFIG';
import CONST from '@src/CONST';
import translations from '@src/languages/translations';
-import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types';
+import type {PluralForm, TranslationParameters, TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Locale} from '@src/types/onyx';
import LocaleListener from './LocaleListener';
@@ -45,9 +45,6 @@ function init() {
}, {});
}
-type PhraseParameters = T extends (...args: infer A) => string ? A : never[];
-type Phrase = TranslationFlatObject[TKey] extends (...args: infer A) => unknown ? (...args: A) => string : string;
-
/**
* Map to store translated values for each locale.
* This is used to avoid translating the same phrase multiple times.
@@ -82,17 +79,12 @@ const translationCache = new Map, Map(
language: 'en' | 'es' | 'es-ES',
phraseKey: TKey,
- fallbackLanguage: 'en' | 'es' | null = null,
- ...phraseParameters: PhraseParameters>
+ fallbackLanguage: 'en' | 'es' | null,
+ ...parameters: TranslationParameters
): string | null {
// Get the cache for the above locale
const cacheForLocale = translationCache.get(language);
@@ -106,11 +98,44 @@ function getTranslatedPhrase(
return valueFromCache;
}
- const translatedPhrase = translations?.[language]?.[phraseKey] as Phrase;
+ const translatedPhrase = translations?.[language]?.[phraseKey];
if (translatedPhrase) {
if (typeof translatedPhrase === 'function') {
- return translatedPhrase(...phraseParameters);
+ /**
+ * If the result of `translatedPhrase` is an object, check if it contains the 'count' parameter
+ * to handle pluralization logic.
+ * Alternatively, before evaluating the translated result, we can check if the 'count' parameter
+ * exists in the passed parameters.
+ */
+ const translateFunction = translatedPhrase as unknown as (...parameters: TranslationParameters) => string | PluralForm;
+ const translateResult = translateFunction(...parameters);
+
+ if (typeof translateResult === 'string') {
+ return translateResult;
+ }
+
+ const phraseObject = parameters[0] as {count?: number};
+ if (typeof phraseObject?.count !== 'number') {
+ throw new Error(`Invalid plural form for '${phraseKey}'`);
+ }
+
+ const pluralRule = new Intl.PluralRules(language).select(phraseObject.count);
+
+ const pluralResult = translateResult[pluralRule];
+ if (pluralResult) {
+ if (typeof pluralResult === 'string') {
+ return pluralResult;
+ }
+
+ return pluralResult(phraseObject.count);
+ }
+
+ if (typeof translateResult.other === 'string') {
+ return translateResult.other;
+ }
+
+ return translateResult.other(phraseObject.count);
}
// We set the translated value in the cache only for the phrases without parameters.
@@ -123,10 +148,10 @@ function getTranslatedPhrase(
}
// Phrase is not found in full locale, search it in fallback language e.g. es
- const fallbacktranslatedPhrase = getTranslatedPhrase(fallbackLanguage, phraseKey, null, ...phraseParameters);
+ const fallbackTranslatedPhrase = getTranslatedPhrase(fallbackLanguage, phraseKey, null, ...parameters);
- if (fallbacktranslatedPhrase) {
- return fallbacktranslatedPhrase;
+ if (fallbackTranslatedPhrase) {
+ return fallbackTranslatedPhrase;
}
if (fallbackLanguage !== CONST.LOCALES.DEFAULT) {
@@ -134,22 +159,22 @@ function getTranslatedPhrase(
}
// Phrase is not translated, search it in default language (en)
- return getTranslatedPhrase(CONST.LOCALES.DEFAULT, phraseKey, null, ...phraseParameters);
+ return getTranslatedPhrase(CONST.LOCALES.DEFAULT, phraseKey, null, ...parameters);
}
/**
* Return translated string for given locale and phrase
*
* @param [desiredLanguage] eg 'en', 'es-ES'
- * @param [phraseParameters] Parameters to supply if the phrase is a template literal.
+ * @param [parameters] Parameters to supply if the phrase is a template literal.
*/
-function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', phraseKey: TKey, ...phraseParameters: PhraseParameters>): string {
+function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', path: TPath, ...parameters: TranslationParameters): string {
// Search phrase in full locale e.g. es-ES
const language = desiredLanguage === CONST.LOCALES.ES_ES_ONFIDO ? CONST.LOCALES.ES_ES : desiredLanguage;
// Phrase is not found in full locale, search it in fallback language e.g. es
const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es';
- const translatedPhrase = getTranslatedPhrase(language, phraseKey, languageAbbreviation, ...phraseParameters);
+ const translatedPhrase = getTranslatedPhrase(language, path, languageAbbreviation, ...parameters);
if (translatedPhrase !== null && translatedPhrase !== undefined) {
return translatedPhrase;
}
@@ -157,21 +182,21 @@ function translate(desiredLanguage: 'en' | 'es' |
// Phrase is not found in default language, on production and staging log an alert to server
// on development throw an error
if (Config.IS_IN_PRODUCTION || Config.IS_IN_STAGING) {
- const phraseString: string = Array.isArray(phraseKey) ? phraseKey.join('.') : phraseKey;
+ const phraseString = Array.isArray(path) ? path.join('.') : path;
Log.alert(`${phraseString} was not found in the en locale`);
if (userEmail.includes(CONST.EMAIL.EXPENSIFY_EMAIL_DOMAIN)) {
return CONST.MISSING_TRANSLATION;
}
return phraseString;
}
- throw new Error(`${phraseKey} was not found in the default language`);
+ throw new Error(`${path} was not found in the default language`);
}
/**
* Uses the locale in this file updated by the Onyx subscriber.
*/
-function translateLocal(phrase: TKey, ...variables: PhraseParameters>) {
- return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables);
+function translateLocal(phrase: TPath, ...parameters: TranslationParameters) {
+ return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...parameters);
}
function getPreferredListFormat(): Intl.ListFormat {
@@ -226,4 +251,3 @@ function getDevicePreferredLocale(): Locale {
}
export {translate, translateLocal, formatList, formatMessageElementList, getDevicePreferredLocale};
-export type {PhraseParameters, Phrase};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index dee84a4f201f..98e490524bef 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -1,7 +1,7 @@
import React, {memo, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import Onyx, {withOnyx} from 'react-native-onyx';
+import Onyx, {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener';
import ComposeProviders from '@components/ComposeProviders';
@@ -50,6 +50,7 @@ import SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
import beforeRemoveReportOpenedFromSearchRHP from './beforeRemoveReportOpenedFromSearchRHP';
import CENTRAL_PANE_SCREENS from './CENTRAL_PANE_SCREENS';
@@ -65,17 +66,6 @@ import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator';
import RightModalNavigator from './Navigators/RightModalNavigator';
import WelcomeVideoModalNavigator from './Navigators/WelcomeVideoModalNavigator';
-type AuthScreensProps = {
- /** Session of currently logged in user */
- session: OnyxEntry;
-
- /** The report ID of the last opened public room as anonymous user */
- lastOpenedPublicRoomID: OnyxEntry;
-
- /** The last Onyx update ID was applied to the client */
- initialLastUpdateIDAppliedToClient: OnyxEntry;
-};
-
const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default;
const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default;
const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default;
@@ -223,7 +213,10 @@ const modalScreenListenersWithCancelSearch = {
},
};
-function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDAppliedToClient}: AuthScreensProps) {
+function AuthScreens() {
+ const [session, sessionStatus] = useOnyx(ONYXKEYS.SESSION);
+ const [lastOpenedPublicRoomID, lastOpenedPublicRoomIDStatus] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID);
+ const [initialLastUpdateIDAppliedToClient, initialLastUpdateIDAppliedToClientStatus] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT);
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {shouldUseNarrowLayout, onboardingIsMediumOrLargerScreenWidth, isSmallScreenWidth} = useResponsiveLayout();
@@ -405,6 +398,9 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
// Prevent unnecessary scrolling
cardStyle: styles.cardStyleNavigator,
};
+ if (isLoadingOnyxValue(sessionStatus, lastOpenedPublicRoomIDStatus, initialLastUpdateIDAppliedToClientStatus)) {
+ return;
+ }
return (
@@ -583,16 +579,4 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
AuthScreens.displayName = 'AuthScreens';
-const AuthScreensMemoized = memo(AuthScreens, () => true);
-
-export default withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
- lastOpenedPublicRoomID: {
- key: ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID,
- },
- initialLastUpdateIDAppliedToClient: {
- key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
- },
-})(AuthScreensMemoized);
+export default memo(AuthScreens, () => true);
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 4108addac0f3..e1c645118003 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -87,6 +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.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,
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index 4c61b953f572..d5e9c5229a89 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -20,6 +20,7 @@ import getTopmostBottomTabRoute from './getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import originalGetTopmostReportId from './getTopmostReportId';
+import isReportOpenInRHP from './isReportOpenInRHP';
import linkingConfig from './linkingConfig';
import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState';
import linkTo from './linkTo';
@@ -157,6 +158,13 @@ function getActiveRoute(): string {
return '';
}
+function getReportRHPActiveRoute(): string {
+ if (isReportOpenInRHP(navigationRef.getRootState())) {
+ return getActiveRoute();
+ }
+ return '';
+}
+
/**
* Check whether the passed route is currently Active or not.
*
@@ -419,6 +427,7 @@ export default {
isActiveRoute,
getActiveRoute,
getActiveRouteWithoutParams,
+ getReportRHPActiveRoute,
closeAndNavigate,
goBack,
isNavigationReady,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index f90ddbe2f818..319ec60d143e 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -955,12 +955,12 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.RIGHT_MODAL.NEW_TASK]: {
screens: {
- [SCREENS.NEW_TASK.ROOT]: ROUTES.NEW_TASK,
- [SCREENS.NEW_TASK.TASK_ASSIGNEE_SELECTOR]: ROUTES.NEW_TASK_ASSIGNEE,
+ [SCREENS.NEW_TASK.ROOT]: ROUTES.NEW_TASK.route,
+ [SCREENS.NEW_TASK.TASK_ASSIGNEE_SELECTOR]: ROUTES.NEW_TASK_ASSIGNEE.route,
[SCREENS.NEW_TASK.TASK_SHARE_DESTINATION_SELECTOR]: ROUTES.NEW_TASK_SHARE_DESTINATION,
- [SCREENS.NEW_TASK.DETAILS]: ROUTES.NEW_TASK_DETAILS,
- [SCREENS.NEW_TASK.TITLE]: ROUTES.NEW_TASK_TITLE,
- [SCREENS.NEW_TASK.DESCRIPTION]: ROUTES.NEW_TASK_DESCRIPTION,
+ [SCREENS.NEW_TASK.DETAILS]: ROUTES.NEW_TASK_DETAILS.route,
+ [SCREENS.NEW_TASK.TITLE]: ROUTES.NEW_TASK_TITLE.route,
+ [SCREENS.NEW_TASK.DESCRIPTION]: ROUTES.NEW_TASK_DESCRIPTION.route,
},
},
[SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: {
@@ -1012,6 +1012,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.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,
@@ -1110,7 +1111,12 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.RIGHT_MODAL.EDIT_REQUEST]: {
screens: {
- [SCREENS.EDIT_REQUEST.REPORT_FIELD]: ROUTES.EDIT_REPORT_FIELD_REQUEST.route,
+ [SCREENS.EDIT_REQUEST.REPORT_FIELD]: {
+ path: ROUTES.EDIT_REPORT_FIELD_REQUEST.route,
+ parse: {
+ fieldID: (fieldID: string) => decodeURIComponent(fieldID),
+ },
+ },
},
},
[SCREENS.RIGHT_MODAL.SIGN_IN]: {
@@ -1125,7 +1131,7 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.RIGHT_MODAL.PROCESS_MONEY_REQUEST_HOLD]: {
screens: {
- [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: ROUTES.PROCESS_MONEY_REQUEST_HOLD,
+ [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: ROUTES.PROCESS_MONEY_REQUEST_HOLD.route,
},
},
[SCREENS.RIGHT_MODAL.TRAVEL]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 1326a0c86709..39053de521db 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -68,6 +68,7 @@ type CentralPaneScreensParamList = {
[SCREENS.SEARCH.CENTRAL_PANE]: {
q: SearchQueryString;
+ name?: string;
};
[SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined;
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined;
@@ -829,53 +830,87 @@ type ProfileNavigatorParamList = {
};
type ReportDetailsNavigatorParamList = {
- [SCREENS.REPORT_DETAILS.ROOT]: undefined;
+ [SCREENS.REPORT_DETAILS.ROOT]: {
+ reportID: string;
+ backTo?: Routes;
+ };
[SCREENS.REPORT_DETAILS.SHARE_CODE]: {
reportID: string;
+ backTo?: Routes;
};
[SCREENS.REPORT_DETAILS.EXPORT]: {
reportID: string;
policyID: string;
connectionName: ConnectionName;
+ backTo?: Routes;
};
};
type ReportSettingsNavigatorParamList = {
- [SCREENS.REPORT_SETTINGS.ROOT]: {reportID: string};
- [SCREENS.REPORT_SETTINGS.NAME]: {reportID: string};
- [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: {reportID: string};
- [SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: {reportID: string};
+ [SCREENS.REPORT_SETTINGS.ROOT]: {
+ reportID: string;
+ backTo?: Routes;
+ };
+ [SCREENS.REPORT_SETTINGS.NAME]: {
+ reportID: string;
+ backTo?: Routes;
+ };
+ [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: {
+ reportID: string;
+ backTo?: Routes;
+ };
+ [SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: {
+ reportID: string;
+ backTo?: Routes;
+ };
[SCREENS.REPORT_SETTINGS.VISIBILITY]: {
reportID: string;
+ backTo?: Routes;
};
};
type ReportDescriptionNavigatorParamList = {
- [SCREENS.REPORT_DESCRIPTION_ROOT]: {reportID: string};
+ [SCREENS.REPORT_DESCRIPTION_ROOT]: {
+ reportID: string;
+ backTo?: Routes;
+ };
};
type ParticipantsNavigatorParamList = {
- [SCREENS.REPORT_PARTICIPANTS.ROOT]: {reportID: string};
- [SCREENS.REPORT_PARTICIPANTS.INVITE]: {reportID: string};
+ [SCREENS.REPORT_PARTICIPANTS.ROOT]: {
+ reportID: string;
+ backTo?: Routes;
+ };
+ [SCREENS.REPORT_PARTICIPANTS.INVITE]: {
+ reportID: string;
+ backTo?: Routes;
+ };
[SCREENS.REPORT_PARTICIPANTS.DETAILS]: {
reportID: string;
accountID: string;
+ backTo?: Routes;
};
[SCREENS.REPORT_PARTICIPANTS.ROLE]: {
reportID: string;
accountID: string;
+ backTo?: Routes;
};
};
type RoomMembersNavigatorParamList = {
- [SCREENS.ROOM_MEMBERS.ROOT]: {reportID: string};
+ [SCREENS.ROOM_MEMBERS.ROOT]: {
+ reportID: string;
+ backTo?: Routes;
+ };
[SCREENS.ROOM_MEMBERS.INVITE]: {
reportID: string;
role?: 'accountant';
+ backTo?: Routes;
};
[SCREENS.ROOM_MEMBERS.DETAILS]: {
reportID: string;
accountID: string;
+ backTo?: Routes;
};
};
@@ -989,6 +1024,7 @@ type MoneyRequestNavigatorParamList = {
backTo: never;
action: never;
currency: never;
+ pageIndex?: string;
};
[SCREENS.MONEY_REQUEST.START]: {
iouType: IOUType;
@@ -1002,6 +1038,7 @@ type MoneyRequestNavigatorParamList = {
transactionID: string;
backTo: Routes;
action: IOUAction;
+ pageIndex?: string;
currency?: string;
};
[SCREENS.MONEY_REQUEST.STEP_DISTANCE_RATE]: {
@@ -1040,12 +1077,22 @@ type MoneyRequestNavigatorParamList = {
};
type NewTaskNavigatorParamList = {
- [SCREENS.NEW_TASK.ROOT]: undefined;
- [SCREENS.NEW_TASK.TASK_ASSIGNEE_SELECTOR]: undefined;
+ [SCREENS.NEW_TASK.ROOT]: {
+ backTo?: Routes;
+ };
+ [SCREENS.NEW_TASK.TASK_ASSIGNEE_SELECTOR]: {
+ backTo?: Routes;
+ };
[SCREENS.NEW_TASK.TASK_SHARE_DESTINATION_SELECTOR]: undefined;
- [SCREENS.NEW_TASK.DETAILS]: undefined;
- [SCREENS.NEW_TASK.TITLE]: undefined;
- [SCREENS.NEW_TASK.DESCRIPTION]: undefined;
+ [SCREENS.NEW_TASK.DETAILS]: {
+ backTo?: Routes;
+ };
+ [SCREENS.NEW_TASK.TITLE]: {
+ backTo?: Routes;
+ };
+ [SCREENS.NEW_TASK.DESCRIPTION]: {
+ backTo?: Routes;
+ };
};
type TeachersUniteNavigatorParamList = {
@@ -1056,9 +1103,12 @@ type TeachersUniteNavigatorParamList = {
};
type TaskDetailsNavigatorParamList = {
- [SCREENS.TASK.TITLE]: undefined;
+ [SCREENS.TASK.TITLE]: {
+ backTo?: Routes;
+ };
[SCREENS.TASK.ASSIGNEE]: {
reportID: string;
+ backTo?: Routes;
};
};
@@ -1070,6 +1120,7 @@ type SplitDetailsNavigatorParamList = {
[SCREENS.SPLIT_DETAILS.ROOT]: {
reportID: string;
reportActionID: string;
+ backTo?: Routes;
};
[SCREENS.SPLIT_DETAILS.EDIT_REQUEST]: {
field: string;
@@ -1103,11 +1154,17 @@ type FlagCommentNavigatorParamList = {
[SCREENS.FLAG_COMMENT_ROOT]: {
reportID: string;
reportActionID: string;
+ backTo?: Routes;
};
};
type EditRequestNavigatorParamList = {
- [SCREENS.EDIT_REQUEST.REPORT_FIELD]: undefined;
+ [SCREENS.EDIT_REQUEST.REPORT_FIELD]: {
+ fieldID: string;
+ reportID: string;
+ policyID: string;
+ backTo?: Routes;
+ };
};
type SignInNavigatorParamList = {
@@ -1130,37 +1187,48 @@ type ProcessMoneyRequestHoldNavigatorParamList = {
};
type PrivateNotesNavigatorParamList = {
- [SCREENS.PRIVATE_NOTES.LIST]: undefined;
+ [SCREENS.PRIVATE_NOTES.LIST]: {
+ backTo?: Routes;
+ };
[SCREENS.PRIVATE_NOTES.EDIT]: {
reportID: string;
accountID: string;
+ backTo?: Routes;
};
};
type TransactionDuplicateNavigatorParamList = {
[SCREENS.TRANSACTION_DUPLICATE.REVIEW]: {
threadReportID: string;
+ backTo?: Routes;
};
[SCREENS.TRANSACTION_DUPLICATE.MERCHANT]: {
threadReportID: string;
+ backTo?: Routes;
};
[SCREENS.TRANSACTION_DUPLICATE.CATEGORY]: {
threadReportID: string;
+ backTo?: Routes;
};
[SCREENS.TRANSACTION_DUPLICATE.TAG]: {
threadReportID: string;
+ backTo?: Routes;
};
[SCREENS.TRANSACTION_DUPLICATE.DESCRIPTION]: {
threadReportID: string;
+ backTo?: Routes;
};
[SCREENS.TRANSACTION_DUPLICATE.TAX_CODE]: {
threadReportID: string;
+ backTo?: Routes;
};
[SCREENS.TRANSACTION_DUPLICATE.BILLABLE]: {
threadReportID: string;
+ backTo?: Routes;
};
[SCREENS.TRANSACTION_DUPLICATE.REIMBURSABLE]: {
threadReportID: string;
+ backTo?: Routes;
};
};
diff --git a/src/libs/Network/NetworkStore.ts b/src/libs/Network/NetworkStore.ts
index 89b40e63834c..fe90aa87495e 100644
--- a/src/libs/Network/NetworkStore.ts
+++ b/src/libs/Network/NetworkStore.ts
@@ -98,7 +98,23 @@ function getAuthToken(): string | null | undefined {
}
function isSupportRequest(command: string): boolean {
- return [WRITE_COMMANDS.OPEN_APP, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, READ_COMMANDS.SEARCH].some((cmd) => cmd === command);
+ return [
+ WRITE_COMMANDS.OPEN_APP,
+ WRITE_COMMANDS.SEARCH,
+ SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP,
+ SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT,
+ READ_COMMANDS.OPEN_CARD_DETAILS_PAGE,
+ READ_COMMANDS.OPEN_POLICY_CATEGORIES_PAGE,
+ READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_PAGE,
+ READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_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_TAGS_PAGE,
+ READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE,
+ READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE,
+ ].some((cmd) => cmd === command);
}
function isSupportAuthToken(): boolean {
diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts
index 7f13e1297b6f..ba435cc57b8f 100644
--- a/src/libs/NextStepUtils.ts
+++ b/src/libs/NextStepUtils.ts
@@ -8,9 +8,9 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportNextStep} from '@src/types/onyx';
import type {Message} from '@src/types/onyx/ReportNextStep';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
+import {getNextApproverAccountID} from './actions/IOU';
import DateUtils from './DateUtils';
import EmailUtils from './EmailUtils';
-import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PolicyUtils from './PolicyUtils';
import * as ReportUtils from './ReportUtils';
@@ -67,18 +67,10 @@ function parseMessage(messages: Message[] | undefined) {
return `${formattedHtml} `;
}
-function getNextApproverDisplayName(policy: Policy, ownerAccountID: number, submitToAccountID: number, report: OnyxEntry) {
- const approvalChain = ReportUtils.getApprovalChain(policy, ownerAccountID, report?.total ?? 0);
- if (approvalChain.length === 0) {
- return ReportUtils.getDisplayNameForParticipant(submitToAccountID);
- }
-
- const nextApproverEmail = approvalChain.length === 1 ? approvalChain[0] : approvalChain[approvalChain.indexOf(currentUserEmail) + 1];
- if (!nextApproverEmail) {
- return ReportUtils.getDisplayNameForParticipant(submitToAccountID);
- }
+function getNextApproverDisplayName(report: OnyxEntry) {
+ const approverAccountID = getNextApproverAccountID(report);
- return PersonalDetailsUtils.getPersonalDetailByEmail(nextApproverEmail)?.displayName ?? nextApproverEmail;
+ return ReportUtils.getDisplayNameForParticipant(approverAccountID) ?? ReportUtils.getPersonalDetailsForAccountID(approverAccountID).login;
}
/**
@@ -98,9 +90,8 @@ function buildNextStep(report: OnyxEntry, predictedNextStatus: ValueOf & {
preferChatroomsOverThreads?: boolean;
preferPolicyExpenseChat?: boolean;
+ preferRecentExpenseReports?: boolean;
};
/**
@@ -496,9 +497,11 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry<
reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage');
}
// All error objects related to the report. Each object in the sources contains error messages keyed by microtime
- // Use Object.assign to merge all error objects into one since it is faster and uses less memory than spread operator
- // eslint-disable-next-line prefer-object-spread
- const errorSources = Object.assign({}, reportErrors, reportErrorFields, reportActionErrors);
+ const errorSources = {
+ reportErrors,
+ ...reportErrorFields,
+ ...reportActionErrors,
+ };
// Combine all error messages keyed by microtime into one object
const errorSourcesArray = Object.values(errorSources ?? {});
@@ -657,7 +660,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastReportAction).text);
} else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) {
lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction);
- } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
+ } else if (ReportActionUtils.isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) {
lastMessageTextFromReport = ReportUtils.getIOUSubmittedMessage(lastReportAction);
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(lastReportAction);
@@ -1571,7 +1574,11 @@ function createOptionFromReport(report: Report, personalDetails: OnyxEntry option?.lastIOUCreationDate ?? '' : '',
+ preferRecentExpenseReports ? (option) => option?.isPolicyExpenseChat : 0,
],
- ['asc'],
+ ['asc', 'desc', 'desc'],
);
}
@@ -1921,6 +1937,8 @@ function getOptions(
let recentReportOptions = [];
let personalDetailsOptions: ReportUtils.OptionData[] = [];
+ const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE;
+
if (includeRecentReports) {
for (const reportOption of allReportOptions) {
/**
@@ -1943,9 +1961,11 @@ function getOptions(
reportOption.isPolicyExpenseChat && reportOption.ownerAccountID === currentUserAccountID && includeOwnedWorkspaceChats && !reportOption.private_isArchived;
const shouldShowInvoiceRoom =
- includeInvoiceRooms && ReportUtils.isInvoiceRoom(reportOption.item) && ReportUtils.isPolicyAdmin(reportOption.policyID ?? '', policies) && !reportOption.private_isArchived;
- // TODO: Uncomment the following line when the invoices screen is ready - https://github.com/Expensify/App/issues/45175.
- // && PolicyUtils.canSendInvoiceFromWorkspace(reportOption.policyID);
+ includeInvoiceRooms &&
+ ReportUtils.isInvoiceRoom(reportOption.item) &&
+ ReportUtils.isPolicyAdmin(reportOption.policyID ?? '', policies) &&
+ !reportOption.private_isArchived &&
+ PolicyUtils.canSendInvoiceFromWorkspace(reportOption.policyID);
/**
Exclude the report option if it doesn't meet any of the following conditions:
@@ -1979,6 +1999,22 @@ function getOptions(
recentReportOptions.push(reportOption);
}
+ // Add a field to sort the recent reports by the time of last IOU request for create actions
+ if (preferRecentExpenseReports) {
+ const reportPreviewAction = allSortedReportActions[reportOption.reportID]?.find((reportAction) =>
+ ReportActionUtils.isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW),
+ );
+
+ if (reportPreviewAction) {
+ const iouReportID = ReportActionUtils.getIOUReportIDFromReportActionPreview(reportPreviewAction);
+ const iouReportActions = allSortedReportActions[iouReportID] ?? [];
+ const lastIOUAction = iouReportActions.find((iouAction) => iouAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ if (lastIOUAction) {
+ reportOption.lastIOUCreationDate = lastIOUAction.lastModified;
+ }
+ }
+ }
+
// 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});
@@ -2027,7 +2063,11 @@ function getOptions(
recentReportOptions.push(...personalDetailsOptions);
personalDetailsOptions = [];
}
- recentReportOptions = orderOptions(recentReportOptions, searchValue, {preferChatroomsOverThreads: true, preferPolicyExpenseChat: !!action});
+ recentReportOptions = orderOptions(recentReportOptions, searchValue, {
+ preferChatroomsOverThreads: true,
+ preferPolicyExpenseChat: !!action,
+ preferRecentExpenseReports,
+ });
}
return {
@@ -2391,6 +2431,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
excludeLogins = [],
preferChatroomsOverThreads = false,
preferPolicyExpenseChat = false,
+ preferRecentExpenseReports = false,
} = config ?? {};
if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) {
return {...options, recentReports: options.recentReports.slice(0, maxRecentReportsToShow)};
@@ -2472,7 +2513,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
return {
personalDetails,
- recentReports: orderOptions(recentReports, searchValue, {preferChatroomsOverThreads, preferPolicyExpenseChat}),
+ recentReports: orderOptions(recentReports, searchValue, {preferChatroomsOverThreads, preferPolicyExpenseChat, preferRecentExpenseReports}),
userToInvite,
currentUserOption: matchResults.currentUserOption,
categoryOptions: [],
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index d276909103cb..c95f6b5a371a 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -588,9 +588,7 @@ function canSendInvoiceFromWorkspace(policyID: string | undefined): boolean {
/** Whether the user can send invoice */
function canSendInvoice(policies: OnyxCollection | null, currentUserLogin: string | undefined): boolean {
- return getActiveAdminWorkspaces(policies, currentUserLogin).length > 0;
- // TODO: Uncomment the following line when the invoices screen is ready - https://github.com/Expensify/App/issues/45175.
- // return getActiveAdminWorkspaces(policies).some((policy) => canSendInvoiceFromWorkspace(policy.id));
+ return getActiveAdminWorkspaces(policies, currentUserLogin).some((policy) => canSendInvoiceFromWorkspace(policy.id));
}
function hasDependentTags(policy: OnyxEntry, policyTagList: OnyxEntry) {
@@ -823,7 +821,10 @@ function getCustomersOrJobsLabelNetSuite(policy: Policy | undefined, translate:
importFields.push(translate('workspace.netsuite.import.customersOrJobs.jobs'));
}
- const importedValueLabel = translate(`workspace.netsuite.import.customersOrJobs.label`, importFields, translate(`workspace.accounting.importTypes.${importedValue}`).toLowerCase());
+ const importedValueLabel = translate(`workspace.netsuite.import.customersOrJobs.label`, {
+ importFields,
+ importType: translate(`workspace.accounting.importTypes.${importedValue}`).toLowerCase(),
+ });
return importedValueLabel.charAt(0).toUpperCase() + importedValueLabel.slice(1);
}
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index f584f694edd0..8d567509c90e 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -215,8 +215,10 @@ function isActionOfType(
function getOriginalMessage(reportAction: OnyxInputOrEntry>): OriginalMessage | undefined {
if (!Array.isArray(reportAction?.message)) {
+ // eslint-disable-next-line deprecation/deprecation
return reportAction?.message ?? reportAction?.originalMessage;
}
+ // eslint-disable-next-line deprecation/deprecation
return reportAction.originalMessage;
}
@@ -593,6 +595,7 @@ function isReportActionDeprecated(reportAction: OnyxEntry, key: st
// HACK ALERT: We're temporarily filtering out any reportActions keyed by sequenceNumber
// to prevent bugs during the migration from sequenceNumber -> reportActionID
+ // eslint-disable-next-line deprecation/deprecation
if (String(reportAction.sequenceNumber) === key) {
Log.info('Front-end filtered out reportAction keyed by sequenceNumber!', false, reportAction);
return true;
@@ -753,6 +756,18 @@ function getLastVisibleAction(reportID: string, actionsToMerge: Record 0) || (trimmedMessage === '?\u2026' && lastMessageText.length > CONST.REPORT.MIN_LENGTH_LAST_MESSAGE_WITH_ELLIPSIS)) {
+ return ' ';
+ }
+
+ return StringUtils.lineBreaksToSpaces(trimmedMessage).substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim();
+}
+
function getLastVisibleMessage(
reportID: string,
actionsToMerge: Record | null> = {},
@@ -777,7 +792,7 @@ function getLastVisibleMessage(
let messageText = getReportActionMessageText(lastVisibleAction) ?? '';
if (messageText) {
- messageText = StringUtils.lineBreaksToSpaces(String(messageText)).substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim();
+ messageText = formatLastMessageText(messageText);
}
return {
lastMessageText: messageText,
@@ -994,20 +1009,20 @@ const iouRequestTypes = new Set>([
CONST.IOU.REPORT_ACTION_TYPE.TRACK,
]);
-/**
- * Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions.
- * Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
- */
-function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined {
- // If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report.
+function getMoneyRequestActions(
+ reportID: string,
+ reportActions: OnyxEntry | ReportAction[],
+ isOffline: boolean | undefined = undefined,
+): Array> {
+ // If the report is not an IOU, Expense report, or Invoice, it shouldn't have money request actions.
const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) {
- return;
+ return [];
}
const reportActionsArray = Array.isArray(reportActions) ? reportActions : Object.values(reportActions ?? {});
if (!reportActionsArray.length) {
- return;
+ return [];
}
const iouRequestActions = [];
@@ -1035,6 +1050,15 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn
iouRequestActions.push(action);
}
}
+ return iouRequestActions;
+}
+
+/**
+ * Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions.
+ * Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
+ */
+function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined {
+ const iouRequestActions = getMoneyRequestActions(reportID, reportActions, isOffline);
// If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report
if (!iouRequestActions.length || iouRequestActions.length > 1) {
@@ -1054,6 +1078,27 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn
return singleAction.childReportID;
}
+/**
+ * Returns true if all transactions on the report have the same ownerID
+ */
+function hasSameActorForAllTransactions(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): boolean {
+ const iouRequestActions = getMoneyRequestActions(reportID, reportActions, isOffline);
+ if (!iouRequestActions.length) {
+ return true;
+ }
+
+ let actorID: number | undefined;
+
+ for (const action of iouRequestActions) {
+ if (actorID !== undefined && actorID !== action?.actorAccountID) {
+ return false;
+ }
+ actorID = action?.actorAccountID;
+ }
+
+ return true;
+}
+
/**
* When we delete certain reports, we want to check whether there are any visible actions left to display.
* If there are no visible actions left (including system messages), we can hide the report from view entirely
@@ -1266,7 +1311,7 @@ function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldD
case CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE: {
const {result, label} = originalMessage;
const errorMessage = result?.messages?.join(', ') ?? '';
- return Localize.translateLocal('report.actions.type.integrationsMessage', errorMessage, label);
+ return Localize.translateLocal('report.actions.type.integrationsMessage', {errorMessage, label});
}
case CONST.REPORT.ACTIONS.TYPE.MANAGER_ATTACH_RECEIPT:
return Localize.translateLocal('report.actions.type.managerAttachReceipt');
@@ -1643,7 +1688,7 @@ function getPolicyChangeLogAddEmployeeMessage(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
@@ -1658,7 +1703,7 @@ function getPolicyChangeLogChangeRoleMessage(reportAction: OnyxInputOrEntry>) {
@@ -1698,7 +1743,7 @@ function isCardIssuedAction(reportAction: OnyxEntry) {
}
function getCardIssuedMessage(reportAction: OnyxEntry, shouldRenderHTML = false) {
- const assigneeAccountID = (reportAction?.originalMessage as IssueNewCardOriginalMessage)?.assigneeAccountID;
+ const assigneeAccountID = (getOriginalMessage(reportAction) as IssueNewCardOriginalMessage)?.assigneeAccountID;
const assigneeDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([assigneeAccountID], currentUserAccountID ?? -1)[0];
const assignee = shouldRenderHTML ? ` ` : assigneeDetails?.firstName ?? assigneeDetails.login ?? '';
@@ -1719,11 +1764,11 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende
const shouldShowAddMissingDetailsButton = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && missingDetails && isAssigneeCurrentUser;
switch (reportAction?.actionName) {
case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED:
- return Localize.translateLocal('workspace.expensifyCard.issuedCard', assignee);
+ return Localize.translateLocal('workspace.expensifyCard.issuedCard', {assignee});
case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL:
return Localize.translateLocal('workspace.expensifyCard.issuedCardVirtual', {assignee, link});
case CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS:
- return Localize.translateLocal(`workspace.expensifyCard.${shouldShowAddMissingDetailsButton ? 'issuedCardNoShippingDetails' : 'addedShippingDetails'}`, assignee);
+ return Localize.translateLocal(`workspace.expensifyCard.${shouldShowAddMissingDetailsButton ? 'issuedCardNoShippingDetails' : 'addedShippingDetails'}`, {assignee});
default:
return '';
}
@@ -1732,6 +1777,7 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende
export {
doesReportHaveVisibleActions,
extractLinksFromMessageHtml,
+ formatLastMessageText,
getActionableMentionWhisperMessage,
getAllReportActions,
getCombinedReportActions,
@@ -1754,6 +1800,7 @@ export {
getNumberOfMoneyRequests,
getOneTransactionThreadReportID,
getOriginalMessage,
+ // eslint-disable-next-line deprecation/deprecation
getParentReportAction,
getRemovedFromApprovalChainMessage,
getReportAction,
@@ -1834,6 +1881,7 @@ export {
getRenamedAction,
isCardIssuedAction,
getCardIssuedMessage,
+ hasSameActorForAllTransactions,
};
export type {LastVisibleMessage};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index d38d3f8b950e..684274bc0079 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -19,7 +19,8 @@ import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvata
import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput';
import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
-import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types';
+import type {ParentNavigationSummaryParams} from '@src/languages/params';
+import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
@@ -83,7 +84,6 @@ import * as PolicyUtils from './PolicyUtils';
import type {LastVisibleMessage} from './ReportActionsUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
import * as ReportConnection from './ReportConnection';
-import StringUtils from './StringUtils';
import * as TransactionUtils from './TransactionUtils';
import * as Url from './Url';
import type {AvatarSource} from './UserUtils';
@@ -127,6 +127,7 @@ type OptimisticAddCommentReportAction = Pick<
| 'childCommenterCount'
| 'childLastVisibleActionCreated'
| 'childOldestFourAccountIDs'
+ | 'delegateAccountID'
> & {isOptimisticAction: boolean};
type OptimisticReportAction = {
@@ -180,6 +181,7 @@ type OptimisticIOUReportAction = Pick<
| 'childReportID'
| 'childVisibleActionCount'
| 'childCommenterCount'
+ | 'delegateAccountID'
>;
type PartialReportAction = OnyxInputOrEntry | Partial | OptimisticIOUReportAction | OptimisticApprovedReportAction | OptimisticSubmittedReportAction | undefined;
@@ -196,12 +198,36 @@ type ReportOfflinePendingActionAndErrors = {
type OptimisticApprovedReportAction = Pick<
ReportAction,
- 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachmentOnly' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
+ | 'actionName'
+ | 'actorAccountID'
+ | 'automatic'
+ | 'avatar'
+ | 'isAttachmentOnly'
+ | 'originalMessage'
+ | 'message'
+ | 'person'
+ | 'reportActionID'
+ | 'shouldShow'
+ | 'created'
+ | 'pendingAction'
+ | 'delegateAccountID'
>;
type OptimisticUnapprovedReportAction = Pick<
ReportAction,
- 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachmentOnly' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
+ | 'actionName'
+ | 'actorAccountID'
+ | 'automatic'
+ | 'avatar'
+ | 'isAttachmentOnly'
+ | 'originalMessage'
+ | 'message'
+ | 'person'
+ | 'reportActionID'
+ | 'shouldShow'
+ | 'created'
+ | 'pendingAction'
+ | 'delegateAccountID'
>;
type OptimisticSubmittedReportAction = Pick<
@@ -219,6 +245,7 @@ type OptimisticSubmittedReportAction = Pick<
| 'shouldShow'
| 'created'
| 'pendingAction'
+ | 'delegateAccountID'
>;
type OptimisticHoldReportAction = Pick<
@@ -233,7 +260,7 @@ type OptimisticCancelPaymentReportAction = Pick<
type OptimisticEditedTaskReportAction = Pick<
ReportAction,
- 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person'
+ 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person' | 'delegateAccountID'
>;
type OptimisticClosedReportAction = Pick<
@@ -248,7 +275,7 @@ type OptimisticDismissedViolationReportAction = Pick<
type OptimisticCreatedReportAction = Pick<
ReportAction,
- 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' | 'actionName'
+ 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' | 'actionName' | 'delegateAccountID'
>;
type OptimisticRenamedReportAction = Pick<
@@ -321,6 +348,7 @@ type OptimisticTaskReportAction = Pick<
| 'previousMessage'
| 'errors'
| 'linkMetadata'
+ | 'delegateAccountID'
>;
type OptimisticWorkspaceChats = {
@@ -340,7 +368,19 @@ type OptimisticWorkspaceChats = {
type OptimisticModifiedExpenseReportAction = Pick<
ReportAction,
- 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'isAttachmentOnly' | 'message' | 'originalMessage' | 'person' | 'pendingAction' | 'reportActionID' | 'shouldShow'
+ | 'actionName'
+ | 'actorAccountID'
+ | 'automatic'
+ | 'avatar'
+ | 'created'
+ | 'isAttachmentOnly'
+ | 'message'
+ | 'originalMessage'
+ | 'person'
+ | 'pendingAction'
+ | 'reportActionID'
+ | 'shouldShow'
+ | 'delegateAccountID'
> & {reportID?: string};
type OptimisticTaskReport = Pick<
@@ -459,6 +499,7 @@ type OptionData = {
tabIndex?: 0 | -1;
isConciergeChat?: boolean;
isBold?: boolean;
+ lastIOUCreationDate?: string;
} & Report;
type OnyxDataTaskAssigneeChat = {
@@ -636,6 +677,14 @@ Onyx.connect({
callback: (value) => (onboarding = value),
});
+let delegateEmail = '';
+Onyx.connect({
+ key: ONYXKEYS.ACCOUNT,
+ callback: (value) => {
+ delegateEmail = value?.delegatedAccess?.delegate ?? '';
+ },
+});
+
function getCurrentUserAvatar(): AvatarSource | undefined {
return currentUserPersonalDetails?.avatar;
}
@@ -1580,12 +1629,20 @@ function hasOnlyNonReimbursableTransactions(iouReportID: string | undefined): bo
return transactions.every((transaction) => !TransactionUtils.getReimbursable(transaction));
}
+/**
+ * Checks if a report has only transactions with same ownerID
+ */
+function isSingleActorMoneyReport(reportID: string): boolean {
+ const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]);
+ return !!ReportActionsUtils.hasSameActorForAllTransactions(reportID, reportActions);
+}
+
/**
* Checks if a report has only one transaction associated with it
*/
function isOneTransactionReport(reportID: string): boolean {
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]);
- return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null;
+ return !!ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions);
}
/*
@@ -1830,7 +1887,8 @@ function formatReportLastMessageText(lastMessageText: string, isModifiedExpenseM
if (isModifiedExpenseMessage) {
return String(lastMessageText).trim().replace(CONST.REGEX.LINE_BREAK, '').trim();
}
- return StringUtils.lineBreaksToSpaces(String(lastMessageText).trim()).substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim();
+
+ return ReportActionsUtils.formatLastMessageText(lastMessageText);
}
/**
@@ -2143,7 +2201,14 @@ function getGroupChatName(participants?: SelectedParticipant[], shouldApplyLimit
return report.reportName;
}
- let participantAccountIDs = participants?.map((participant) => participant.accountID) ?? Object.keys(report?.participants ?? {}).map(Number);
+ const pendingMemberAccountIDs = new Set(
+ report?.pendingChatMembers?.filter((member) => member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).map((member) => member.accountID),
+ );
+ let participantAccountIDs =
+ participants?.map((participant) => participant.accountID) ??
+ Object.keys(report?.participants ?? {})
+ .map(Number)
+ .filter((accountID) => !pendingMemberAccountIDs.has(accountID.toString()));
if (shouldApplyLimit) {
participantAccountIDs = participantAccountIDs.slice(0, 5);
}
@@ -2210,7 +2275,7 @@ function getIcons(
if (isChatThread(report)) {
const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
- const actorAccountID = getReportActionActorAccountID(parentReportAction, report);
+ const actorAccountID = getReportActionActorAccountID(parentReportAction);
const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1], '', false);
const actorIcon = {
id: actorAccountID,
@@ -2305,7 +2370,7 @@ function getIcons(
const isManager = currentUserAccountID === report?.managerID;
// For one transaction IOUs, display a simplified report icon
- if (isOneTransactionReport(report?.reportID ?? '-1')) {
+ if (isOneTransactionReport(report?.reportID ?? '-1') || isSingleActorMoneyReport(report?.reportID ?? '-1')) {
return [ownerIcon];
}
@@ -3120,7 +3185,7 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry)
return {canHoldRequest, canUnholdRequest};
}
-const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, backTo?: string): void => {
+const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, backTo?: string, searchHash?: number): void => {
if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) {
return;
}
@@ -3137,11 +3202,13 @@ const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, bac
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport.policyID}`] ?? null;
if (isOnHold) {
- IOU.unholdRequest(transactionID, reportAction.childReportID ?? '');
+ IOU.unholdRequest(transactionID, reportAction.childReportID ?? '', searchHash);
} else {
const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams());
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, reportAction.childReportID ?? '', backTo || activeRoute));
+ Navigation.navigate(
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, reportAction.childReportID ?? '', backTo || activeRoute, searchHash),
+ );
}
};
@@ -3706,7 +3773,7 @@ function getReportName(
}
const parentReportActionMessage = ReportActionsUtils.getReportActionMessage(parentReportAction);
- if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
+ if (ReportActionsUtils.isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) {
return getIOUSubmittedMessage(parentReportAction);
}
if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) {
@@ -3755,9 +3822,9 @@ function getReportName(
return `${reportActionMessage} (${Localize.translateLocal('common.archived')})`;
}
if (!isEmptyObject(parentReportAction) && ReportActionsUtils.isModifiedExpenseAction(parentReportAction)) {
- return ModifiedExpenseMessage.getForReportAction(report?.reportID, parentReportAction);
+ const modifiedMessage = ModifiedExpenseMessage.getForReportAction(report?.reportID, parentReportAction);
+ return formatReportLastMessageText(modifiedMessage);
}
-
if (isTripRoom(report)) {
return report?.reportName ?? '';
}
@@ -3920,34 +3987,34 @@ function getParentNavigationSubtitle(report: OnyxEntry, invoiceReceiverP
/**
* Navigate to the details page of a given report
*/
-function navigateToDetailsPage(report: OnyxEntry) {
+function navigateToDetailsPage(report: OnyxEntry, backTo?: string) {
const isSelfDMReport = isSelfDM(report);
const isOneOnOneChatReport = isOneOnOneChat(report);
const participantAccountID = getParticipantsAccountIDsForDisplay(report);
if (isSelfDMReport || isOneOnOneChatReport) {
- Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountID[0]));
+ Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountID[0], backTo));
return;
}
if (report?.reportID) {
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID, backTo));
}
}
/**
* Go back to the details page of a given report
*/
-function goBackToDetailsPage(report: OnyxEntry) {
+function goBackToDetailsPage(report: OnyxEntry, backTo?: string) {
const isOneOnOneChatReport = isOneOnOneChat(report);
const participantAccountID = getParticipantsAccountIDsForDisplay(report);
if (isOneOnOneChatReport) {
- Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountID[0]));
+ Navigation.goBack(ROUTES.PROFILE.getRoute(participantAccountID[0], backTo));
return;
}
- Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '-1'));
+ Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '-1', backTo));
}
function navigateBackAfterDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean) {
@@ -3970,7 +4037,7 @@ function navigateBackAfterDeleteTransaction(backRoute: Route | undefined, isFrom
/**
* Go back to the previous page from the edit private page of a given report
*/
-function goBackFromPrivateNotes(report: OnyxEntry, session: OnyxEntry) {
+function goBackFromPrivateNotes(report: OnyxEntry, session: OnyxEntry, backTo?: string) {
if (isEmpty(report) || isEmpty(session) || !session.accountID) {
return;
}
@@ -3979,16 +4046,16 @@ function goBackFromPrivateNotes(report: OnyxEntry, session: OnyxEntry${file.name}`;
}
-function getReportDescriptionText(report: Report): string {
- if (!report.description) {
+function getReportDescriptionText(report: OnyxEntry): string {
+ if (!report?.description) {
return '';
}
- return Parser.htmlToText(report.description);
+ return Parser.htmlToText(report?.description);
}
function getPolicyDescriptionText(policy: OnyxEntry): string {
@@ -4130,6 +4197,7 @@ function buildOptimisticAddCommentReportAction(
const isAttachmentOnly = file && !text;
const isAttachmentWithText = !!text && file !== undefined;
const accountID = actorAccountID ?? currentUserAccountID ?? -1;
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
// Remove HTML from text when applying optimistic offline comment
return {
@@ -4166,6 +4234,7 @@ function buildOptimisticAddCommentReportAction(
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
shouldShow: true,
isOptimisticAction: true,
+ delegateAccountID: delegateAccountDetails?.accountID,
},
};
}
@@ -4441,7 +4510,11 @@ function getFormattedAmount(reportAction: ReportAction) {
return formattedAmount;
}
-function getIOUSubmittedMessage(reportAction: ReportAction) {
+function getReportAutomaticallySubmittedMessage(reportAction: ReportAction) {
+ return Localize.translateLocal('iou.automaticallySubmittedAmount', {formattedAmount: getFormattedAmount(reportAction)});
+}
+
+function getIOUSubmittedMessage(reportAction: ReportAction) {
return Localize.translateLocal('iou.submittedAmount', {formattedAmount: getFormattedAmount(reportAction)});
}
@@ -4590,6 +4663,8 @@ function buildOptimisticIOUReportAction(
type,
};
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+
if (type === CONST.IOU.REPORT_ACTION_TYPE.PAY) {
// In pay someone flow, we store amount, comment, currency in IOUDetails when type = pay
if (isSendMoneyFlow) {
@@ -4641,6 +4716,7 @@ function buildOptimisticIOUReportAction(
shouldShow: true,
created,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4653,6 +4729,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e
currency,
expenseReportID,
};
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
return {
actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED,
@@ -4673,6 +4750,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e
shouldShow: true,
created: DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4680,6 +4758,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e
* Builds an optimistic APPROVED report action with a randomly generated reportActionID.
*/
function buildOptimisticUnapprovedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticUnapprovedReportAction {
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
return {
actionName: CONST.REPORT.ACTIONS.TYPE.UNAPPROVED,
actorAccountID: currentUserAccountID,
@@ -4703,6 +4782,7 @@ function buildOptimisticUnapprovedReportAction(amount: number, currency: string,
shouldShow: true,
created: DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4759,6 +4839,8 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
expenseReportID,
};
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+
return {
actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
actorAccountID: currentUserAccountID,
@@ -4779,6 +4861,7 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
shouldShow: true,
created: DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4819,9 +4902,9 @@ function buildOptimisticReportPreview(
},
],
created,
- accountID: iouReport?.managerID ?? -1,
+ accountID: iouReport?.ownerAccountID ?? -1,
// The preview is initially whispered if created with a receipt, so the actor is the current user as well
- actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? -1,
+ actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.ownerAccountID ?? -1,
childReportID: childReportID ?? iouReport?.reportID,
childMoneyRequestCount: 1,
childLastMoneyRequestComment: comment,
@@ -4879,6 +4962,8 @@ function buildOptimisticModifiedExpenseReportAction(
updatedTransaction?: OnyxInputOrEntry,
): OptimisticModifiedExpenseReportAction {
const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport, policy, updatedTransaction);
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+
return {
actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
actorAccountID: currentUserAccountID,
@@ -4906,6 +4991,7 @@ function buildOptimisticModifiedExpenseReportAction(
reportActionID: NumberUtils.rand64(),
reportID: transactionThread?.reportID,
shouldShow: true,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4915,6 +5001,8 @@ function buildOptimisticModifiedExpenseReportAction(
* @param movedToReportID - The reportID of the report the transaction is moved to
*/
function buildOptimisticMovedTrackedExpenseModifiedReportAction(transactionThreadID: string, movedToReportID: string): OptimisticModifiedExpenseReportAction {
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+
return {
actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
actorAccountID: currentUserAccountID,
@@ -4944,6 +5032,7 @@ function buildOptimisticMovedTrackedExpenseModifiedReportAction(transactionThrea
reportActionID: NumberUtils.rand64(),
reportID: transactionThreadID,
shouldShow: true,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -5019,6 +5108,8 @@ function buildOptimisticTaskReportAction(
html: message,
whisperedTo: [],
};
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+
return {
actionName,
actorAccountID,
@@ -5045,6 +5136,7 @@ function buildOptimisticTaskReportAction(
created: DateUtils.getDBTimeWithSkew(Date.now() + createdOffset),
isFirstItem: false,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -5374,6 +5466,7 @@ function buildOptimisticEditedTaskFieldReportAction({title, description}: Task):
} else if (field) {
changelog = `removed the ${field}`;
}
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
return {
reportActionID: NumberUtils.rand64(),
@@ -5398,10 +5491,13 @@ function buildOptimisticEditedTaskFieldReportAction({title, description}: Task):
avatar: getCurrentUserAvatar(),
created: DateUtils.getDBTime(),
shouldShow: false,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: number): OptimisticEditedTaskReportAction {
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+
return {
reportActionID: NumberUtils.rand64(),
actionName: CONST.REPORT.ACTIONS.TYPE.TASK_EDITED,
@@ -5425,6 +5521,7 @@ function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: numbe
avatar: getCurrentUserAvatar(),
created: DateUtils.getDBTime(),
shouldShow: false,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -6439,9 +6536,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry): boolean {
return true;
}
- if (isExpenseReport(report) && isOneTransactionReport(report?.reportID ?? '-1')) {
+ if (isExpenseReport(report)) {
return true;
}
@@ -6656,7 +6751,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean {
return true;
}
- if (isInvoiceRoom(report)) {
+ if (isInvoiceRoom(report) || isInvoiceReport(report)) {
return true;
}
@@ -7138,16 +7233,16 @@ function shouldAutoFocusOnKeyPress(event: KeyboardEvent): boolean {
/**
* Navigates to the appropriate screen based on the presence of a private note for the current user.
*/
-function navigateToPrivateNotes(report: OnyxEntry, session: OnyxEntry) {
+function navigateToPrivateNotes(report: OnyxEntry, session: OnyxEntry, backTo?: string) {
if (isEmpty(report) || isEmpty(session) || !session.accountID) {
return;
}
const currentUserPrivateNote = report.privateNotes?.[session.accountID]?.note ?? '';
if (isEmpty(currentUserPrivateNote)) {
- Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID));
+ Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID, backTo));
return;
}
- Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID, backTo));
}
/**
@@ -7538,10 +7633,10 @@ function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boo
return (isChatThread(report) && !!getReportNotificationPreference(report)) || isUserCreatedPolicyRoom(report) || isNonAdminOrOwnerOfPolicyExpenseChat(report, policy);
}
-function getReportActionActorAccountID(reportAction: OnyxInputOrEntry, iouReport: OnyxInputOrEntry | undefined): number | undefined {
+function getReportActionActorAccountID(reportAction: OnyxInputOrEntry): number | undefined {
switch (reportAction?.actionName) {
case CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW:
- return !isEmptyObject(iouReport) ? iouReport.managerID : reportAction?.childManagerAccountID;
+ return reportAction?.childOwnerAccountID ?? reportAction?.actorAccountID;
case CONST.REPORT.ACTIONS.TYPE.SUBMITTED:
return reportAction?.adminAccountID ?? reportAction?.actorAccountID;
@@ -7696,7 +7791,7 @@ function getFieldViolationTranslation(reportField: PolicyReportField, violation?
switch (violation) {
case 'fieldRequired':
- return Localize.translateLocal('reportViolations.fieldRequired', reportField.name);
+ return Localize.translateLocal('reportViolations.fieldRequired', {fieldName: reportField.name});
default:
return '';
}
@@ -7810,8 +7905,8 @@ function hasMissingInvoiceBankAccount(iouReportID: string): boolean {
return invoiceReport?.ownerAccountID === currentUserAccountID && isEmptyObject(getPolicy(invoiceReport?.policyID)?.invoice?.bankAccount ?? {}) && isSettled(iouReportID);
}
-function isExpenseReportManagerWithoutParentAccess(report: OnyxEntry) {
- return isExpenseReport(report) && report?.hasParentAccess === false && report?.managerID === currentUserAccountID;
+function isExpenseReportWithoutParentAccess(report: OnyxEntry) {
+ return isExpenseReport(report) && report?.hasParentAccess === false;
}
export {
@@ -7912,6 +8007,7 @@ export {
getIOUForwardedMessage,
getRejectedReportMessage,
getWorkspaceNameUpdatedMessage,
+ getReportAutomaticallySubmittedMessage,
getIOUSubmittedMessage,
getIcons,
getIconsForParticipants,
@@ -8009,7 +8105,7 @@ export {
isEmptyReport,
isRootGroupChat,
isExpenseReport,
- isExpenseReportManagerWithoutParentAccess,
+ isExpenseReportWithoutParentAccess,
isExpenseRequest,
isExpensifyOnlyParticipantInReport,
isGroupChat,
@@ -8122,6 +8218,7 @@ export {
isIndividualInvoiceRoom,
isAuditor,
hasMissingInvoiceBankAccount,
+ isSingleActorMoneyReport,
};
export type {
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index 703c4df5a29c..bd402b65d86d 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -793,7 +793,7 @@ function getOverflowMenu(itemName: string, hash: number, inputQuery: string, sho
if (isMobileMenu && closeMenu) {
closeMenu();
}
- Navigation.navigate(ROUTES.SEARCH_SAVED_SEARCH_RENAME.getRoute({name: itemName, jsonQuery: inputQuery}));
+ Navigation.navigate(ROUTES.SEARCH_SAVED_SEARCH_RENAME.getRoute({name: encodeURIComponent(itemName), jsonQuery: inputQuery}));
},
icon: Expensicons.Pencil,
shouldShowRightIcon: false,
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index d056f111695e..e120f7026fce 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -129,10 +129,10 @@ function getOrderedReportIDs(
return;
}
const isSystemChat = ReportUtils.isSystemChat(report);
- const isExpenseReportManagerWithoutParentAccess = ReportUtils.isExpenseReportManagerWithoutParentAccess(report);
+ const isExpenseReportWithoutParentAccess = ReportUtils.isExpenseReportWithoutParentAccess(report);
const shouldOverrideHidden =
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- hasValidDraftComment(report.reportID) || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || report.isPinned || isExpenseReportManagerWithoutParentAccess;
+ hasValidDraftComment(report.reportID) || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || report.isPinned || isExpenseReportWithoutParentAccess;
if (isHidden && !shouldOverrideHidden) {
return;
}
diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts
index 13bc3293cd9d..b57774799315 100644
--- a/src/libs/TripReservationUtils.ts
+++ b/src/libs/TripReservationUtils.ts
@@ -1,8 +1,43 @@
+import {Str} from 'expensify-common';
+import type {Dispatch, SetStateAction} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import type {LocaleContextProps} from '@components/LocaleContextProvider';
import * as Expensicons from '@src/components/Icon/Expensicons';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {TravelSettings} from '@src/types/onyx';
import type {Reservation, ReservationType} from '@src/types/onyx/Transaction';
import type Transaction from '@src/types/onyx/Transaction';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
+import * as Link from './actions/Link';
+import Navigation from './Navigation/Navigation';
+
+let travelSettings: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.NVP_TRAVEL_SETTINGS,
+ callback: (val) => {
+ travelSettings = val;
+ },
+});
+
+let activePolicyID: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
+ callback: (val) => {
+ activePolicyID = val;
+ },
+});
+
+let primaryLogin: string;
+Onyx.connect({
+ key: ONYXKEYS.ACCOUNT,
+ callback: (val) => {
+ primaryLogin = val?.primaryLogin ?? '';
+ },
+});
function getTripReservationIcon(reservationType: ReservationType): IconAsset {
switch (reservationType) {
@@ -38,4 +73,24 @@ function getTripEReceiptIcon(transaction?: Transaction): IconAsset | undefined {
}
}
-export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptIcon};
+function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessage: Dispatch>, ctaErrorMessage = ''): void {
+ if (Str.isSMSLogin(primaryLogin)) {
+ setCtaErrorMessage(translate('travel.phoneError'));
+ return;
+ }
+ if (isEmptyObject(travelSettings)) {
+ Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(activePolicyID ?? '-1', Navigation.getActiveRoute()));
+ return;
+ }
+ if (!travelSettings?.hasAcceptedTerms) {
+ Navigation.navigate(ROUTES.TRAVEL_TCS);
+ return;
+ }
+ if (ctaErrorMessage) {
+ setCtaErrorMessage('');
+ }
+ Link.openTravelDotLink(activePolicyID)?.catch(() => {
+ setCtaErrorMessage(translate('travel.errorMessage'));
+ });
+}
+export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptIcon, bookATrip};
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index adbc05460220..c7ee0a0b0867 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -1,11 +1,10 @@
import reject from 'lodash/reject';
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
-import type {Phrase, PhraseParameters} from '@libs/Localize';
+import type {LocaleContextProps} from '@components/LocaleContextProvider';
import {getCustomUnitRate, getSortedTagKeys} from '@libs/PolicyUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
-import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, PolicyCategories, PolicyTagLists, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx';
@@ -237,10 +236,7 @@ const ViolationsUtils = {
* possible values could be either translation keys that resolve to strings or translation keys that resolve to
* functions.
*/
- getViolationTranslation(
- violation: TransactionViolation,
- translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string,
- ): string {
+ getViolationTranslation(violation: TransactionViolation, translate: LocaleContextProps['translate']): string {
const {
brokenBankConnection = false,
isAdmin = false,
@@ -250,7 +246,7 @@ const ViolationsUtils = {
category,
rejectedBy = '',
rejectReason = '',
- formattedLimit,
+ formattedLimit = '',
surcharge = 0,
invoiceMarkup = 0,
maxAge = 0,
diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts
index ed46b0b5f5ec..d8cd2ff00828 100644
--- a/src/libs/WorkspacesSettingsUtils.ts
+++ b/src/libs/WorkspacesSettingsUtils.ts
@@ -1,6 +1,7 @@
import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
+import type {LocaleContextProps} from '@components/LocaleContextProvider';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -8,7 +9,6 @@ import type {Policy, ReimbursementAccount, Report, ReportAction, ReportActions,
import type {PolicyConnectionSyncProgress, Unit} from '@src/types/onyx/Policy';
import {isConnectionInProgress} from './actions/connections';
import * as CurrencyUtils from './CurrencyUtils';
-import type {Phrase, PhraseParameters} from './Localize';
import * as OptionsListUtils from './OptionsListUtils';
import {hasCustomUnitsError, hasEmployeeListError, hasPolicyError, hasSyncError, hasTaxRateError} from './PolicyUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
@@ -234,7 +234,7 @@ function getUnitTranslationKey(unit: Unit): TranslationPaths {
*/
function getOwnershipChecksDisplayText(
error: ValueOf,
- translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string,
+ translate: LocaleContextProps['translate'],
policy: OnyxEntry,
accountLogin: string | undefined,
) {
@@ -271,14 +271,14 @@ function getOwnershipChecksDisplayText(
case CONST.POLICY.OWNERSHIP_ERRORS.DUPLICATE_SUBSCRIPTION:
title = translate('workspace.changeOwner.duplicateSubscriptionTitle');
text = translate('workspace.changeOwner.duplicateSubscriptionText', {
- email: changeOwner?.duplicateSubscription,
- workspaceName: policy?.name,
+ email: changeOwner?.duplicateSubscription ?? '',
+ workspaceName: policy?.name ?? '',
});
buttonText = translate('workspace.changeOwner.duplicateSubscriptionButtonText');
break;
case CONST.POLICY.OWNERSHIP_ERRORS.HAS_FAILED_SETTLEMENTS:
title = translate('workspace.changeOwner.hasFailedSettlementsTitle');
- text = translate('workspace.changeOwner.hasFailedSettlementsText', {email: accountLogin});
+ text = translate('workspace.changeOwner.hasFailedSettlementsText', {email: accountLogin ?? ''});
buttonText = translate('workspace.changeOwner.hasFailedSettlementsButtonText');
break;
case CONST.POLICY.OWNERSHIP_ERRORS.FAILED_TO_CLEAR_BALANCE:
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index a0f60752913e..6b6f1a5f6dc6 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -243,10 +243,9 @@ function getOnyxDataForOpenOrReconnect(isOpenApp = false): OnyxData {
* Fetches data needed for app initialization
*/
function openApp() {
- getPolicyParamsForOpenOrReconnect().then((policyParams: PolicyParamsForOpenOrReconnect) => {
+ return getPolicyParamsForOpenOrReconnect().then((policyParams: PolicyParamsForOpenOrReconnect) => {
const params: OpenAppParams = {enablePriorityModeFilter: true, ...policyParams};
-
- API.write(WRITE_COMMANDS.OPEN_APP, params, getOnyxDataForOpenOrReconnect(true));
+ return API.write(WRITE_COMMANDS.OPEN_APP, params, getOnyxDataForOpenOrReconnect(true));
});
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index daaa766145ed..bc553ea86d70 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -17,6 +17,7 @@ import type {
PayMoneyRequestParams,
ReplaceReceiptParams,
RequestMoneyParams,
+ ResolveDuplicatesParams,
SendInvoiceParams,
SendMoneyParams,
SetNameValuePairParams,
@@ -42,6 +43,7 @@ import Navigation from '@libs/Navigation/Navigation';
import * as NextStepUtils from '@libs/NextStepUtils';
import {rand64} from '@libs/NumberUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PhoneNumber from '@libs/PhoneNumber';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -63,6 +65,7 @@ import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type ReportAction from '@src/types/onyx/ReportAction';
import type {OnyxData} from '@src/types/onyx/Request';
+import type {SearchTransaction} from '@src/types/onyx/SearchResults';
import type {Comment, Receipt, ReceiptSource, Routes, SplitShares, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import * as CachedPDFPaths from './CachedPDFPaths';
@@ -1247,8 +1250,8 @@ function buildOnyxDataForInvoice(
key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`,
value: {
invoice: {
- companyName: undefined,
- companyWebsite: undefined,
+ companyName: null,
+ companyWebsite: null,
pendingFields: {
companyName: null,
companyWebsite: null,
@@ -6433,7 +6436,7 @@ function getHoldReportActionsAndTransactions(reportID: string) {
function getReportFromHoldRequestsOnyxData(
chatReport: OnyxTypes.Report,
- iouReport: OnyxTypes.Report,
+ iouReport: OnyxEntry,
recipient: Participant,
): {
optimisticHoldReportID: string;
@@ -6442,13 +6445,13 @@ function getReportFromHoldRequestsOnyxData(
optimisticData: OnyxUpdate[];
failureData: OnyxUpdate[];
} {
- const {holdReportActions, holdTransactions} = getHoldReportActionsAndTransactions(iouReport.reportID);
+ const {holdReportActions, holdTransactions} = getHoldReportActionsAndTransactions(iouReport?.reportID ?? '');
const firstHoldTransaction = holdTransactions[0];
const newParentReportActionID = rand64();
const optimisticExpenseReport = ReportUtils.buildOptimisticExpenseReport(
chatReport.reportID,
- chatReport.policyID ?? iouReport.policyID ?? '',
+ chatReport.policyID ?? iouReport?.policyID ?? '',
recipient.accountID ?? 1,
holdTransactions.reduce((acc, transaction) => acc + transaction.amount, 0) * (ReportUtils.isIOUReport(iouReport) ? 1 : -1),
getCurrency(firstHoldTransaction),
@@ -6537,7 +6540,7 @@ function getReportFromHoldRequestsOnyxData(
// remove hold report actions from old iou report
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID ?? ''}`,
value: deleteHoldReportActions,
},
// add hold report actions to new iou report
@@ -6588,7 +6591,7 @@ function getReportFromHoldRequestsOnyxData(
// add hold report actions back to old iou report
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID ?? ''}`,
value: bringReportActionsBack,
},
// remove hold report actions from the new iou report
@@ -6616,7 +6619,7 @@ function getReportFromHoldRequestsOnyxData(
function getPayMoneyRequestParams(
initialChatReport: OnyxTypes.Report,
- iouReport: OnyxTypes.Report,
+ iouReport: OnyxEntry,
recipient: Participant,
paymentMethodType: PaymentMethodType,
full: boolean,
@@ -6679,34 +6682,34 @@ function getPayMoneyRequestParams(
}
}
- let total = (iouReport.total ?? 0) - (iouReport.nonReimbursableTotal ?? 0);
- if (ReportUtils.hasHeldExpenses(iouReport.reportID) && !full && !!iouReport.unheldTotal) {
- total = iouReport.unheldTotal;
+ let total = (iouReport?.total ?? 0) - (iouReport?.nonReimbursableTotal ?? 0);
+ if (ReportUtils.hasHeldExpenses(iouReport?.reportID ?? '') && !full && !!iouReport?.unheldTotal) {
+ total = iouReport?.unheldTotal;
}
const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction(
CONST.IOU.REPORT_ACTION_TYPE.PAY,
ReportUtils.isExpenseReport(iouReport) ? -total : total,
- iouReport.currency ?? '',
+ iouReport?.currency ?? '',
'',
[recipient],
'',
paymentMethodType,
- iouReport.reportID,
+ iouReport?.reportID,
true,
);
// In some instances, the report preview action might not be available to the payer (only whispered to the requestor)
// hence we need to make the updates to the action safely.
let optimisticReportPreviewAction = null;
- const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport.reportID);
+ const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport?.reportID ?? '');
if (reportPreviewAction) {
optimisticReportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction, true);
}
let currentNextStep = null;
let optimisticNextStep = null;
if (!isInvoiceReport) {
- currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`] ?? null;
+ currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport?.reportID ?? ''}`] ?? null;
optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED);
}
@@ -6734,7 +6737,7 @@ function getPayMoneyRequestParams(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID ?? ''}`,
value: {
[optimisticIOUReportAction.reportActionID]: {
...(optimisticIOUReportAction as OnyxTypes.ReportAction),
@@ -6744,7 +6747,7 @@ function getPayMoneyRequestParams(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID ?? ''}`,
value: {
...iouReport,
lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction),
@@ -6761,18 +6764,18 @@ function getPayMoneyRequestParams(
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD,
- value: {[iouReport.policyID ?? '-1']: paymentMethodType},
+ value: {[iouReport?.policyID ?? '-1']: paymentMethodType},
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport?.reportID ?? ''}`,
value: optimisticNextStep,
},
);
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID ?? ''}`,
value: {
pendingFields: {
preview: null,
@@ -6785,7 +6788,7 @@ function getPayMoneyRequestParams(
failureData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID ?? ''}`,
value: {
[optimisticIOUReportAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'),
@@ -6794,7 +6797,7 @@ function getPayMoneyRequestParams(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID ?? ''}`,
value: {
...iouReport,
},
@@ -6806,7 +6809,7 @@ function getPayMoneyRequestParams(
},
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport?.reportID ?? ''}`,
value: currentNextStep,
},
);
@@ -6833,7 +6836,7 @@ function getPayMoneyRequestParams(
// Optimistically unhold all transactions if we pay all requests
if (full) {
- const reportTransactions = TransactionUtils.getAllReportTransactions(iouReport.reportID);
+ const reportTransactions = TransactionUtils.getAllReportTransactions(iouReport?.reportID);
for (const transaction of reportTransactions) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
@@ -6871,7 +6874,7 @@ function getPayMoneyRequestParams(
return {
params: {
- iouReportID: iouReport.reportID,
+ iouReportID: iouReport?.reportID ?? '',
chatReportID: chatReport.reportID,
reportActionID: optimisticIOUReportAction.reportActionID,
paymentMethodType,
@@ -7013,6 +7016,24 @@ function isLastApprover(approvalChain: string[]): boolean {
return approvalChain[approvalChain.length - 1] === currentUserEmail;
}
+function getNextApproverAccountID(report: OnyxEntry) {
+ const ownerAccountID = report?.ownerAccountID ?? -1;
+ const policy = PolicyUtils.getPolicy(report?.policyID);
+ const approvalChain = ReportUtils.getApprovalChain(policy, ownerAccountID, report?.total ?? 0);
+ const submitToAccountID = PolicyUtils.getSubmitToAccountID(policy, ownerAccountID);
+
+ if (approvalChain.length === 0) {
+ return submitToAccountID;
+ }
+
+ const nextApproverEmail = approvalChain.length === 1 ? approvalChain[0] : approvalChain[approvalChain.indexOf(currentUserEmail) + 1];
+ if (!nextApproverEmail) {
+ return submitToAccountID;
+ }
+
+ return PersonalDetailsUtils.getAccountIDsByLogins([nextApproverEmail])[0];
+}
+
function approveMoneyRequest(expenseReport: OnyxEntry, full?: boolean) {
if (expenseReport?.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(expenseReport.policyID)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(expenseReport.policyID));
@@ -7031,6 +7052,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?:
const predictedNextStatus = isLastApprover(approvalChain) ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED;
const predictedNextState = isLastApprover(approvalChain) ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED;
+ const managerID = isLastApprover(approvalChain) ? expenseReport?.managerID : getNextApproverAccountID(expenseReport);
const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, predictedNextStatus);
const chatReport = ReportUtils.getReportOrDraftReport(expenseReport?.chatReportID);
@@ -7054,6 +7076,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?:
lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticApprovedReportAction),
stateNum: predictedNextState,
statusNum: predictedNextStatus,
+ managerID,
pendingFields: {
partial: full ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
@@ -7530,7 +7553,7 @@ function completePaymentOnboarding(paymentSelected: ValueOf, full = true) {
if (chatReport.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(chatReport.policyID)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(chatReport.policyID));
return;
@@ -7539,7 +7562,7 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R
const paymentSelected = paymentType === CONST.IOU.PAYMENT_TYPE.VBBA ? CONST.IOU.PAYMENT_SELECTED.BBA : CONST.IOU.PAYMENT_SELECTED.PBA;
completePaymentOnboarding(paymentSelected);
- const recipient = {accountID: iouReport.ownerAccountID};
+ const recipient = {accountID: iouReport?.ownerAccountID ?? -1};
const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType, full);
// For now, we need to call the PayMoneyRequestWithWallet API since PayMoneyRequest was not updated to work with
@@ -7549,8 +7572,8 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R
API.write(apiCommand, params, {optimisticData, successData, failureData});
}
-function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report, payAsBusiness = false) {
- const recipient = {accountID: invoiceReport.ownerAccountID};
+function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxEntry, payAsBusiness = false) {
+ const recipient = {accountID: invoiceReport?.ownerAccountID ?? -1};
const {
optimisticData,
successData,
@@ -7575,7 +7598,7 @@ function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.
completePaymentOnboarding(paymentSelected);
let params: PayInvoiceParams = {
- reportID: invoiceReport.reportID,
+ reportID: invoiceReport?.reportID ?? '',
reportActionID,
paymentMethodType,
payAsBusiness,
@@ -7834,7 +7857,7 @@ function adjustRemainingSplitShares(transaction: NonNullable>>,
+ });
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`,
+ value: {
+ data: {
+ [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {
+ canHold: true,
+ canUnhold: false,
+ },
+ },
+ } as Record>>,
+ });
+ }
+
API.write(
'HoldRequest',
{
@@ -7906,7 +7957,7 @@ function putOnHold(transactionID: string, comment: string, reportID: string) {
/**
* Remove expense from HOLD
*/
-function unholdRequest(transactionID: string, reportID: string) {
+function unholdRequest(transactionID: string, reportID: string, searchHash?: number) {
const createdReportAction = ReportUtils.buildOptimisticUnHoldReportAction();
const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`];
@@ -7964,6 +8015,34 @@ function unholdRequest(transactionID: string, reportID: string) {
},
];
+ // If we are unholding from the search page, we optimistically update the snapshot data that search uses so that it is kept in sync
+ if (searchHash) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`,
+ value: {
+ data: {
+ [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {
+ canHold: true,
+ canUnhold: false,
+ },
+ },
+ } as Record>>,
+ });
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`,
+ value: {
+ data: {
+ [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {
+ canHold: false,
+ canUnhold: true,
+ },
+ },
+ } as Record>>,
+ });
+ }
+
API.write(
'UnHoldRequest',
{
@@ -8011,6 +8090,21 @@ function getIOURequestPolicyID(transaction: OnyxEntry, re
return workspaceSender?.policyID ?? report?.policyID ?? '-1';
}
+function getIOUActionForTransactions(transactionIDList: string[], iouReportID: string): Array> {
+ return Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`] ?? {})?.filter(
+ (reportAction): reportAction is ReportAction => {
+ if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) {
+ return false;
+ }
+ const message = ReportActionsUtils.getOriginalMessage(reportAction);
+ if (!message?.IOUTransactionID) {
+ return false;
+ }
+ return transactionIDList.includes(message.IOUTransactionID);
+ },
+ );
+}
+
/** Merge several transactions into one by updating the fields of the one we want to keep and deleting the rest */
function mergeDuplicates(params: TransactionMergeParams) {
const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];
@@ -8095,18 +8189,7 @@ function mergeDuplicates(params: TransactionMergeParams) {
},
};
- const iouActionsToDelete = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${params.reportID}`] ?? {})?.filter(
- (reportAction): reportAction is ReportAction => {
- if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) {
- return false;
- }
- const message = ReportActionsUtils.getOriginalMessage(reportAction);
- if (!message?.IOUTransactionID) {
- return false;
- }
- return params.transactionIDList.includes(message.IOUTransactionID);
- },
- );
+ const iouActionsToDelete = getIOUActionForTransactions(params.transactionIDList, params.reportID);
const deletedTime = DateUtils.getDBTime();
const expenseReportActionsOptimisticData: OnyxUpdate = {
@@ -8167,8 +8250,128 @@ function mergeDuplicates(params: TransactionMergeParams) {
API.write(WRITE_COMMANDS.TRANSACTION_MERGE, params, {optimisticData, failureData});
}
+/** Instead of merging the duplicates, it updates the transaction we want to keep and puts the others on hold without deleting them */
+function resolveDuplicates(params: TransactionMergeParams) {
+ const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];
+
+ const optimisticTransactionData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`,
+ value: {
+ ...originalSelectedTransaction,
+ billable: params.billable,
+ comment: {
+ comment: params.comment,
+ },
+ category: params.category,
+ created: params.created,
+ currency: params.currency,
+ modifiedMerchant: params.merchant,
+ reimbursable: params.reimbursable,
+ tag: params.tag,
+ },
+ };
+
+ const failureTransactionData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`,
+ // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
+ value: originalSelectedTransaction as OnyxTypes.Transaction,
+ };
+
+ const optimisticTransactionViolations: OnyxUpdate[] = [...params.transactionIDList, params.transactionID].map((id) => {
+ const violations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? [];
+ const newViolation = {name: CONST.VIOLATIONS.HOLD, type: CONST.VIOLATION_TYPES.VIOLATION};
+ const updatedViolations = id === params.transactionID ? violations : [...violations, newViolation];
+ return {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`,
+ value: updatedViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION),
+ };
+ });
+
+ const failureTransactionViolations: OnyxUpdate[] = [...params.transactionIDList, params.transactionID].map((id) => {
+ const violations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? [];
+ return {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`,
+ value: violations,
+ };
+ });
+
+ const iouActionList = getIOUActionForTransactions(params.transactionIDList, params.reportID);
+ const transactionThreadReportIDList = iouActionList.map((action) => action?.childReportID);
+ const orderedTransactionIDList = iouActionList.map((action) => {
+ const message = ReportActionsUtils.getOriginalMessage(action);
+ return message?.IOUTransactionID ?? '';
+ });
+
+ const optimisticHoldActions: OnyxUpdate[] = [];
+ const failureHoldActions: OnyxUpdate[] = [];
+ const reportActionIDList: string[] = [];
+ transactionThreadReportIDList.forEach((transactionThreadReportID) => {
+ const createdReportAction = ReportUtils.buildOptimisticHoldReportAction();
+ reportActionIDList.push(createdReportAction.reportActionID);
+ optimisticHoldActions.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
+ value: {
+ [createdReportAction.reportActionID]: createdReportAction,
+ },
+ });
+ failureHoldActions.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
+ value: {
+ [createdReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericHoldExpenseFailureMessage'),
+ },
+ },
+ });
+ });
+
+ const transactionThreadReportID = getIOUActionForTransactions([params.transactionID], params.reportID)?.[0]?.childReportID;
+ const optimisticReportAction = ReportUtils.buildOptimisticDismissedViolationReportAction({
+ reason: 'manual',
+ violationName: CONST.VIOLATIONS.DUPLICATED_TRANSACTION,
+ });
+
+ const optimisticReportActionData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
+ value: {
+ [optimisticReportAction.reportActionID]: optimisticReportAction,
+ },
+ };
+
+ const failureReportActionData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
+ value: {
+ [optimisticReportAction.reportActionID]: null,
+ },
+ };
+
+ const optimisticData: OnyxUpdate[] = [];
+ const failureData: OnyxUpdate[] = [];
+
+ optimisticData.push(optimisticTransactionData, ...optimisticTransactionViolations, ...optimisticHoldActions, optimisticReportActionData);
+ failureData.push(failureTransactionData, ...failureTransactionViolations, ...failureHoldActions, failureReportActionData);
+ const {reportID, transactionIDList, receiptID, ...otherParams} = params;
+
+ const parameters: ResolveDuplicatesParams = {
+ ...otherParams,
+ reportActionIDList,
+ transactionIDList: orderedTransactionIDList,
+ dismissedViolationReportActionID: optimisticReportAction.reportActionID,
+ };
+
+ API.write(WRITE_COMMANDS.RESOLVE_DUPLICATES, parameters, {optimisticData, failureData});
+}
+
export {
adjustRemainingSplitShares,
+ getNextApproverAccountID,
approveMoneyRequest,
canApproveIOU,
cancelPayment,
@@ -8237,5 +8440,7 @@ export {
updateMoneyRequestTaxAmount,
updateMoneyRequestTaxRate,
mergeDuplicates,
+ resolveDuplicates,
+ prepareToCleanUpMoneyRequest,
};
export type {GPSPoint as GpsPoint, IOURequestType};
diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts
index c342fe6eedb6..a9f6e376b80a 100644
--- a/src/libs/actions/Policy/Category.ts
+++ b/src/libs/actions/Policy/Category.ts
@@ -153,7 +153,7 @@ function updateImportSpreadsheetData(categoriesLength: number) {
shouldFinalModalBeOpened: true,
importFinalModal: {
title: translateLocal('spreadsheet.importSuccessfullTitle'),
- prompt: translateLocal('spreadsheet.importCategoriesSuccessfullDescription', categoriesLength),
+ prompt: translateLocal('spreadsheet.importCategoriesSuccessfullDescription', {categories: categoriesLength}),
},
},
},
diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts
index eae625388f33..8c2a66a8ccf6 100644
--- a/src/libs/actions/Policy/Member.ts
+++ b/src/libs/actions/Policy/Member.ts
@@ -183,7 +183,10 @@ function updateImportSpreadsheetData(membersLength: number): OnyxData {
key: ONYXKEYS.IMPORTED_SPREADSHEET,
value: {
shouldFinalModalBeOpened: true,
- importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importMembersSuccessfullDescription', membersLength)},
+ importFinalModal: {
+ title: translateLocal('spreadsheet.importSuccessfullTitle'),
+ prompt: translateLocal('spreadsheet.importMembersSuccessfullDescription', {members: membersLength}),
+ },
},
},
],
@@ -887,7 +890,7 @@ function declineJoinRequest(reportID: string, reportAction: OnyxEntry void) {
const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_MEMBERS_CSV, {
policyID,
});
@@ -899,7 +902,7 @@ function downloadMembersCSV(policyID: string) {
formData.append(key, String(value));
});
- fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_MEMBERS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST);
+ fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_MEMBERS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed);
}
export {
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 2123f2a47764..8f8bba8e916f 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -224,7 +224,6 @@ function getPolicy(policyID: string | undefined): OnyxEntry {
/**
* Returns a primary policy for the user
*/
-// TODO: Use getInvoicePrimaryWorkspace when the invoices screen is ready - https://github.com/Expensify/App/issues/45175.
function getPrimaryPolicy(activePolicyID: OnyxEntry, currentUserLogin: string | undefined): Policy | undefined {
const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies, currentUserLogin);
const primaryPolicy: Policy | null | undefined = activeAdminWorkspaces.find((policy) => policy.id === activePolicyID);
@@ -4161,9 +4160,10 @@ function setPolicyAutomaticApprovalLimit(policyID: string, limit: string) {
function setPolicyAutomaticApprovalRate(policyID: string, auditRate: string) {
const policy = getPolicy(policyID);
const fallbackAuditRate = auditRate === '' ? '0' : auditRate;
- const parsedAuditRate = parseInt(fallbackAuditRate, 10);
+ const parsedAuditRate = parseInt(fallbackAuditRate, 10) / 100;
- if (parsedAuditRate === policy?.autoApproval?.auditRate ?? CONST.POLICY.RANDOM_AUDIT_DEFAULT_PERCENTAGE) {
+ // The auditRate arrives as an int to this method so we will convert it to a float before sending it to the API.
+ if (parsedAuditRate === (policy?.autoApproval?.auditRate ?? CONST.POLICY.RANDOM_AUDIT_DEFAULT_PERCENTAGE)) {
return;
}
@@ -4239,17 +4239,8 @@ function enableAutoApprovalOptions(policyID: string, enabled: boolean) {
return;
}
- const autoApprovalCleanupValues = !enabled
- ? {
- pendingFields: {
- limit: null,
- auditRate: null,
- },
- }
- : {};
- const autoApprovalValues = !enabled ? {auditRate: CONST.POLICY.RANDOM_AUDIT_DEFAULT_PERCENTAGE, limit: CONST.POLICY.AUTO_APPROVE_REPORTS_UNDER_DEFAULT_CENTS} : {};
- const autoApprovalFailureValues = !enabled ? {autoApproval: {limit: policy?.autoApproval?.limit, auditRate: policy?.autoApproval?.auditRate, ...autoApprovalCleanupValues}} : {};
-
+ const autoApprovalValues = {auditRate: CONST.POLICY.RANDOM_AUDIT_DEFAULT_PERCENTAGE, limit: CONST.POLICY.AUTO_APPROVE_REPORTS_UNDER_DEFAULT_CENTS};
+ const autoApprovalFailureValues = {autoApproval: {limit: policy?.autoApproval?.limit, auditRate: policy?.autoApproval?.auditRate, pendingFields: null}};
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -4275,7 +4266,7 @@ function enableAutoApprovalOptions(policyID: string, enabled: boolean) {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
- autoApproval: {...autoApprovalCleanupValues},
+ autoApproval: {pendingFields: null},
pendingFields: {
shouldShowAutoApprovalOptions: null,
},
@@ -4368,7 +4359,7 @@ function setPolicyAutoReimbursementLimit(policyID: string, limit: string) {
];
const parameters: SetPolicyAutoReimbursementLimitParams = {
- autoReimbursement: {limit: parsedLimit},
+ limit: parsedLimit,
policyID,
};
@@ -4381,6 +4372,7 @@ function setPolicyAutoReimbursementLimit(policyID: string, limit: string) {
/**
* Call the API to enable auto-payment for the reports in the given policy
+ *
* @param policyID - id of the policy to apply the limit to
* @param enabled - whether auto-payment for the reports is enabled in the given policy
*/
@@ -4391,16 +4383,8 @@ function enablePolicyAutoReimbursementLimit(policyID: string, enabled: boolean)
return;
}
- const autoReimbursementCleanupValues = !enabled
- ? {
- pendingFields: {
- limit: null,
- },
- }
- : {};
- const autoReimbursementFailureValues = !enabled ? {autoReimbursement: {limit: policy?.autoReimbursement?.limit, ...autoReimbursementCleanupValues}} : {};
- const autoReimbursementValues = !enabled ? {limit: CONST.POLICY.AUTO_REIMBURSEMENT_DEFAULT_LIMIT_CENTS} : {};
-
+ const autoReimbursementFailureValues = {autoReimbursement: {limit: policy?.autoReimbursement?.limit, pendingFields: null}};
+ const autoReimbursementValues = {limit: CONST.POLICY.AUTO_REIMBURSEMENT_DEFAULT_LIMIT_CENTS};
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -4425,7 +4409,7 @@ function enablePolicyAutoReimbursementLimit(policyID: string, enabled: boolean)
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
- autoReimbursement: {...autoReimbursementCleanupValues},
+ autoReimbursement: {pendingFields: null},
pendingFields: {
shouldShowAutoReimbursementLimitOption: null,
},
diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts
index d6f67e496b92..9628b6ceda77 100644
--- a/src/libs/actions/Policy/Tag.ts
+++ b/src/libs/actions/Policy/Tag.ts
@@ -136,7 +136,10 @@ function updateImportSpreadsheetData(tagsLength: number): OnyxData {
key: ONYXKEYS.IMPORTED_SPREADSHEET,
value: {
shouldFinalModalBeOpened: true,
- importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importTagsSuccessfullDescription', tagsLength)},
+ importFinalModal: {
+ title: translateLocal('spreadsheet.importSuccessfullTitle'),
+ prompt: translateLocal('spreadsheet.importTagsSuccessfullDescription', {tags: tagsLength}),
+ },
},
},
],
@@ -990,7 +993,7 @@ function setPolicyTagApprover(policyID: string, tag: string, approver: string) {
API.write(WRITE_COMMANDS.SET_POLICY_TAG_APPROVER, parameters, onyxData);
}
-function downloadTagsCSV(policyID: string) {
+function downloadTagsCSV(policyID: string, onDownloadFailed: () => void) {
const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_TAGS_CSV, {
policyID,
});
@@ -1001,7 +1004,7 @@ function downloadTagsCSV(policyID: string) {
formData.append(key, String(value));
});
- fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_TAGS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST);
+ fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_TAGS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed);
}
export {
diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts
index beec327a2e40..2aca5d9f9de8 100644
--- a/src/libs/actions/PriorityMode.ts
+++ b/src/libs/actions/PriorityMode.ts
@@ -57,11 +57,11 @@ Onyx.connect({
},
});
-let hasTriedFocusMode: boolean | null | undefined;
+let hasTriedFocusMode: boolean | undefined;
Onyx.connect({
key: ONYXKEYS.NVP_TRY_FOCUS_MODE,
callback: (val) => {
- hasTriedFocusMode = val ?? null;
+ hasTriedFocusMode = val;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
checkRequiredData();
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index b038f16d003d..e53cac804b90 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -187,12 +187,12 @@ const allReportActions: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
- callback: (action, key) => {
- if (!key || !action) {
+ callback: (actions, key) => {
+ if (!key || !actions) {
return;
}
const reportID = CollectionUtils.extractCollectionItemID(key);
- allReportActions[reportID] = action;
+ allReportActions[reportID] = actions;
},
});
@@ -1357,6 +1357,15 @@ function handleReportChanged(report: OnyxEntry) {
return;
}
+ // Handle cleanup of stale optimistic IOU report and its report preview separately
+ if (report?.reportID && report.preexistingReportID && ReportUtils.isMoneyRequestReport(report) && report?.parentReportActionID) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {
+ [report.parentReportActionID]: null,
+ });
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null);
+ return;
+ }
+
// It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists.
// In this case, the API will let us know by returning a preexistingReportID.
// We should clear out the optimistically created report and re-route the user to the preexisting report.
@@ -1705,15 +1714,11 @@ function updateNotificationPreference(
reportID: string,
previousValue: NotificationPreference | undefined,
newValue: NotificationPreference,
- navigate: boolean,
parentReportID?: string,
parentReportActionID?: string,
- report?: OnyxEntry,
) {
+ // No change needed
if (previousValue === newValue) {
- if (navigate && !isEmptyObject(report) && report.reportID) {
- ReportUtils.goBackToDetailsPage(report);
- }
return;
}
@@ -1761,9 +1766,6 @@ function updateNotificationPreference(
const parameters: UpdateReportNotificationPreferenceParams = {reportID, notificationPreference: newValue};
API.write(WRITE_COMMANDS.UPDATE_REPORT_NOTIFICATION_PREFERENCE, parameters, {optimisticData, failureData});
- if (navigate && !isEmptyObject(report)) {
- ReportUtils.goBackToDetailsPage(report);
- }
}
function updateRoomVisibility(reportID: string, previousValue: RoomVisibility | undefined, newValue: RoomVisibility) {
@@ -1805,9 +1807,9 @@ function toggleSubscribeToChildReport(childReportID = '-1', parentReportAction:
openReport(childReportID);
const parentReportActionID = parentReportAction?.reportActionID ?? '-1';
if (!prevNotificationPreference || prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) {
- updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, false, parentReportID, parentReportActionID);
+ updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, parentReportID, parentReportActionID);
} else {
- updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, false, parentReportID, parentReportActionID);
+ updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, parentReportID, parentReportActionID);
}
} else {
const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction?.actorAccountID)])];
@@ -1831,7 +1833,7 @@ function toggleSubscribeToChildReport(childReportID = '-1', parentReportAction:
openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID);
const notificationPreference =
prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
- updateNotificationPreference(newChat.reportID, prevNotificationPreference, notificationPreference, false, parentReportID, parentReportAction?.reportActionID);
+ updateNotificationPreference(newChat.reportID, prevNotificationPreference, notificationPreference, parentReportID, parentReportAction?.reportActionID);
}
}
@@ -2052,9 +2054,8 @@ function deleteReportField(reportID: string, reportField: PolicyReportField) {
}
function updateDescription(reportID: string, previousValue: string, newValue: string) {
- // No change needed, navigate back
+ // No change needed
if (previousValue === newValue) {
- Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID));
return;
}
@@ -2120,12 +2121,11 @@ function updateDescription(reportID: string, previousValue: string, newValue: st
const parameters: UpdateRoomDescriptionParams = {reportID, description: parsedDescription, reportActionID: optimisticDescriptionUpdatedReportAction.reportActionID};
API.write(WRITE_COMMANDS.UPDATE_ROOM_DESCRIPTION, parameters, {optimisticData, failureData, successData});
- Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID));
}
-function updateWriteCapabilityAndNavigate(report: Report, newValue: WriteCapability) {
+function updateWriteCapability(report: Report, newValue: WriteCapability) {
+ // No change needed
if (report.writeCapability === newValue) {
- Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID));
return;
}
@@ -2147,8 +2147,6 @@ function updateWriteCapabilityAndNavigate(report: Report, newValue: WriteCapabil
const parameters: UpdateReportWriteCapabilityParams = {reportID: report.reportID, writeCapability: newValue};
API.write(WRITE_COMMANDS.UPDATE_REPORT_WRITE_CAPABILITY, parameters, {optimisticData, failureData});
- // Return to the report settings page since this field utilizes push-to-page
- Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID));
}
/**
@@ -2314,13 +2312,12 @@ function navigateToConciergeChatAndDeleteReport(reportID: string, shouldPopToTop
* @param policyRoomReport The policy room report
* @param policyRoomName The updated name for the policy room
*/
-function updatePolicyRoomNameAndNavigate(policyRoomReport: Report, policyRoomName: string) {
+function updatePolicyRoomName(policyRoomReport: Report, policyRoomName: string) {
const reportID = policyRoomReport.reportID;
const previousName = policyRoomReport.reportName;
- // No change needed, navigate back
+ // No change needed
if (previousName === policyRoomName) {
- Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID));
return;
}
@@ -2386,7 +2383,6 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport: Report, policyRoomNam
};
API.write(WRITE_COMMANDS.UPDATE_POLICY_ROOM_NAME, parameters, {optimisticData, successData, failureData});
- Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID));
}
/**
@@ -2758,10 +2754,8 @@ function joinRoom(report: OnyxEntry) {
report.reportID,
ReportUtils.getReportNotificationPreference(report),
CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
- false,
report.parentReportID,
report.parentReportActionID,
- report,
);
}
@@ -4100,7 +4094,7 @@ export {
addComment,
addAttachment,
updateDescription,
- updateWriteCapabilityAndNavigate,
+ updateWriteCapability,
updateNotificationPreference,
subscribeToReportTypingEvents,
subscribeToReportLeavingEvents,
@@ -4129,7 +4123,7 @@ export {
navigateToAndOpenReportWithAccountIDs,
navigateToAndOpenChildReport,
toggleSubscribeToChildReport,
- updatePolicyRoomNameAndNavigate,
+ updatePolicyRoomName,
clearPolicyRoomNameErrors,
clearIOUError,
subscribeToNewActionEvent,
diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts
index 873603b68739..0f89232dc3cf 100644
--- a/src/libs/actions/Search.ts
+++ b/src/libs/actions/Search.ts
@@ -5,7 +5,7 @@ import type {FormOnyxValues} from '@components/Form/types';
import type {SearchQueryJSON} from '@components/Search/types';
import * as API from '@libs/API';
import type {ExportSearchItemsToCSVParams} from '@libs/API/parameters';
-import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
+import {WRITE_COMMANDS} from '@libs/API/types';
import * as ApiUtils from '@libs/ApiUtils';
import fileDownload from '@libs/fileDownload';
import enhanceParameters from '@libs/Network/enhanceParameters';
@@ -51,15 +51,82 @@ function getOnyxLoadingData(hash: number): {optimisticData: OnyxUpdate[]; finall
return {optimisticData, finallyData};
}
-function saveSearch({queryJSON, name}: {queryJSON: SearchQueryJSON; name?: string}) {
- const saveSearchName = name ?? queryJSON?.inputQuery ?? '';
+function saveSearch({queryJSON, newName}: {queryJSON: SearchQueryJSON; newName?: string}) {
+ const saveSearchName = newName ?? queryJSON?.inputQuery ?? '';
const jsonQuery = JSON.stringify(queryJSON);
- API.write(WRITE_COMMANDS.SAVE_SEARCH, {jsonQuery, name: saveSearchName});
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.SAVED_SEARCHES}`,
+ value: {
+ [queryJSON.hash]: {
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ name: saveSearchName,
+ query: queryJSON.inputQuery,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.SAVED_SEARCHES}`,
+ value: {
+ [queryJSON.hash]: null,
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.SAVED_SEARCHES}`,
+ value: {
+ [queryJSON.hash]: {
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+ API.write(WRITE_COMMANDS.SAVE_SEARCH, {jsonQuery, newName: saveSearchName}, {optimisticData, failureData, successData});
}
function deleteSavedSearch(hash: number) {
- API.write(WRITE_COMMANDS.DELETE_SAVED_SEARCH, {hash});
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.SAVED_SEARCHES}`,
+ value: {
+ [hash]: {
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ },
+ },
+ },
+ ];
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.SAVED_SEARCHES}`,
+ value: {
+ [hash]: null,
+ },
+ },
+ ];
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.SAVED_SEARCHES}`,
+ value: {
+ [hash]: {
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ API.write(WRITE_COMMANDS.DELETE_SAVED_SEARCH, {hash}, {optimisticData, failureData, successData});
}
function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: number}) {
@@ -71,7 +138,7 @@ function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: numbe
};
const jsonQuery = JSON.stringify(queryWithOffset);
- API.read(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery}, {optimisticData, finallyData});
+ API.write(WRITE_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery}, {optimisticData, finallyData});
}
/**
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index 41ccfee1786b..08568b6d5a02 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -18,6 +18,7 @@ import playSound, {SOUNDS} from '@libs/Sound';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import type {ReportActions} from '@src/types/onyx/ReportAction';
@@ -846,7 +847,7 @@ function clearOutTaskInfoAndNavigate(reportID?: string, chatReport?: OnyxEntry) {
/**
* Closes the current open task modal and clears out the task info from the store.
*/
-function dismissModalAndClearOutTaskInfo() {
- Navigation.closeRHPFlow();
+function dismissModalAndClearOutTaskInfo(backTo?: Route) {
+ if (backTo) {
+ Navigation.goBack(backTo);
+ } else {
+ Navigation.closeRHPFlow();
+ }
clearOutTaskInfo();
}
diff --git a/src/libs/actions/connections/QuickbooksOnline.ts b/src/libs/actions/connections/QuickbooksOnline.ts
index c62c97aa88ca..bb85c8f5223f 100644
--- a/src/libs/actions/connections/QuickbooksOnline.ts
+++ b/src/libs/actions/connections/QuickbooksOnline.ts
@@ -384,7 +384,7 @@ function updateQuickbooksOnlinePreferredExporter) {
// If the report is optimistic, there's no need to fetch it. The original action should create it.
// If there is an error for creating the chat, there's no need to fetch it since it doesn't exist
return !report?.isOptimisticReport && !report?.errorFields?.createChat;
diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx
index 5690d3202136..ec4ce6568f70 100644
--- a/src/pages/EditReportFieldPage.tsx
+++ b/src/pages/EditReportFieldPage.tsx
@@ -1,3 +1,4 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import {Str} from 'expensify-common';
import React, {useState} from 'react';
import {withOnyx} from 'react-native-onyx';
@@ -14,9 +15,12 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane';
import Navigation from '@libs/Navigation/Navigation';
+import type {EditRequestNavigatorParamList} from '@libs/Navigation/types';
import * as ReportUtils from '@libs/ReportUtils';
import * as ReportActions from '@src/libs/actions/Report';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import type {Policy, Report} from '@src/types/onyx';
import EditReportFieldDate from './EditReportFieldDate';
import EditReportFieldDropdown from './EditReportFieldDropdown';
@@ -30,26 +34,12 @@ type EditReportFieldPageOnyxProps = {
policy: OnyxEntry;
};
-type EditReportFieldPageProps = EditReportFieldPageOnyxProps & {
- /** Route from navigation */
- route: {
- /** Params from the route */
- params: {
- /** Which field we are editing */
- fieldID: string;
-
- /** reportID for the expense report */
- reportID: string;
-
- /** policyID for the expense report */
- policyID: string;
- };
- };
-};
+type EditReportFieldPageProps = EditReportFieldPageOnyxProps & StackScreenProps;
function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) {
const {windowWidth} = useWindowDimensions();
const styles = useThemeStyles();
+ const backTo = route.params.backTo;
const fieldKey = ReportUtils.getReportFieldKey(route.params.fieldID);
const reportField = report?.fieldList?.[fieldKey] ?? policy?.fieldList?.[fieldKey];
const policyField = policy?.fieldList?.[fieldKey] ?? reportField;
@@ -71,11 +61,19 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps)
);
}
+ const goBack = () => {
+ if (isReportFieldTitle) {
+ Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo));
+ return;
+ }
+ Navigation.goBack(backTo);
+ };
+
const handleReportFieldChange = (form: FormOnyxValues) => {
const value = form[fieldKey];
if (isReportFieldTitle) {
ReportActions.updateReportName(report.reportID, value, report.reportName ?? '');
- Navigation.goBack();
+ goBack();
} else {
ReportActions.updateReportField(report.reportID, {...reportField, value: value === '' ? null : value}, reportField);
Navigation.dismissModal(isSearchTopmostCentralPane() ? undefined : report?.reportID);
@@ -111,6 +109,7 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps)
threeDotsMenuItems={menuItems}
shouldShowThreeDotsButton={!!menuItems?.length}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
+ onBackButtonPress={goBack}
/>
{({safeAreaPaddingBottomStyle}) => (
-
+ Navigation.goBack(route.params.backTo)}
+ />
>>;
function InviteReportParticipantsPage({betas, personalDetails, report, didScreenTransitionEnd}: InviteReportParticipantsPageProps) {
+ const route = useRoute>();
const {options, areOptionsInitialized} = useOptionsList({
shouldInitialize: didScreenTransitionEnd,
});
@@ -163,7 +168,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen
const validate = useCallback(() => selectedOptions.length > 0, [selectedOptions]);
const reportID = report.reportID;
- const backRoute = useMemo(() => ROUTES.REPORT_PARTICIPANTS.getRoute(reportID), [reportID]);
+ const backRoute = useMemo(() => ROUTES.REPORT_PARTICIPANTS.getRoute(reportID, route.params.backTo), [reportID, route.params.backTo]);
const reportName = useMemo(() => ReportUtils.getGroupChatName(undefined, true, report), [report]);
const inviteUsers = useCallback(() => {
if (!validate()) {
diff --git a/src/pages/MissingPersonalDetails/index.tsx b/src/pages/MissingPersonalDetails/index.tsx
index 9a2266f6384b..5220f25be981 100644
--- a/src/pages/MissingPersonalDetails/index.tsx
+++ b/src/pages/MissingPersonalDetails/index.tsx
@@ -135,7 +135,7 @@ function MissingPersonalDetails() {
if (countrySpecificZipRegex) {
if (!countrySpecificZipRegex.test(values[INPUT_IDS.ZIP_POST_CODE]?.trim().toUpperCase())) {
if (ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.ZIP_POST_CODE]?.trim())) {
- errors[INPUT_IDS.ZIP_POST_CODE] = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat);
+ errors[INPUT_IDS.ZIP_POST_CODE] = translate('privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat});
} else {
errors[INPUT_IDS.ZIP_POST_CODE] = translate('common.error.fieldRequired');
}
diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
index 1779fe8e085e..ac8eb7f862b6 100644
--- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
+++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
@@ -47,6 +47,7 @@ type PrivateNotesEditPageProps = WithReportAndPrivateNotesOrNotFoundProps &
};
function PrivateNotesEditPage({route, personalDetailsList, report, session}: PrivateNotesEditPageProps) {
+ const backTo = route.params.backTo;
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -102,9 +103,9 @@ function PrivateNotesEditPage({route, personalDetailsList, report, session}: Pri
Keyboard.dismiss();
if (!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) {
- ReportUtils.navigateToDetailsPage(report);
+ ReportUtils.navigateToDetailsPage(report, backTo);
} else {
- Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID));
+ Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID, backTo));
}
};
@@ -116,7 +117,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report, session}: Pri
>
ReportUtils.goBackFromPrivateNotes(report, session)}
+ onBackButtonPress={() => ReportUtils.goBackFromPrivateNotes(report, session, backTo)}
shouldShowBackButton
onCloseButtonPress={() => Navigation.dismissModal()}
/>
diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx
index 055bba42c552..cc7ee9f54daa 100644
--- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx
+++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx
@@ -1,3 +1,5 @@
+import type {RouteProp} from '@react-navigation/native';
+import {useRoute} from '@react-navigation/native';
import React, {useCallback, useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
@@ -11,11 +13,13 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
+import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types';
import type {WithReportAndPrivateNotesOrNotFoundProps} from '@pages/home/report/withReportAndPrivateNotesOrNotFound';
import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import type {PersonalDetailsList, Report} from '@src/types/onyx';
type PrivateNotesListPageOnyxProps = {
@@ -40,6 +44,8 @@ type NoteListItem = {
};
function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) {
+ const route = useRoute>();
+ const backTo = route.params.backTo;
const styles = useThemeStyles();
const {translate} = useLocalize();
const getAttachmentValue = useCallback((item: NoteListItem) => ({reportID: item.reportID, accountID: Number(item.accountID), type: CONST.ATTACHMENT_TYPE.NOTE}), []);
@@ -77,13 +83,13 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot
reportID: report.reportID,
accountID,
title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '',
- action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)),
+ action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID, backTo)),
brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)),
note: privateNote?.note ?? '',
disabled: Number(session?.accountID) !== Number(accountID),
};
});
- }, [report, personalDetailsList, session, translate]);
+ }, [report, personalDetailsList, session, translate, backTo]);
return (
Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo))}
onCloseButtonPress={() => Navigation.dismissModal()}
/>
{translate('privateNotes.personalNoteMessage')}
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 2b732bb5506a..0d47e3fd8f35 100755
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -277,7 +277,7 @@ function ProfilePage({route}: ProfilePageProps) {
shouldShowRightIcon
title={notificationPreference}
description={translate('notificationPreferencesPage.label')}
- onPress={() => Navigation.navigate(ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES.getRoute(report.reportID))}
+ onPress={() => Navigation.navigate(ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES.getRoute(report.reportID, navigateBackTo))}
/>
)}
{!isEmptyObject(report) && report.reportID && !isCurrentUser && (
@@ -285,7 +285,7 @@ function ProfilePage({route}: ProfilePageProps) {
title={`${translate('privateNotes.title')}`}
titleStyle={styles.flex1}
icon={Expensicons.Pencil}
- onPress={() => ReportUtils.navigateToPrivateNotes(report, session)}
+ onPress={() => ReportUtils.navigateToPrivateNotes(report, session, navigateBackTo)}
wrapperStyle={styles.breakAll}
shouldShowRightIcon
brickRoadIndicator={ReportActions.hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index d0049dfe5865..e382596bebac 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -22,6 +22,7 @@ import PromotedActionsBar, {PromotedActions} from '@components/PromotedActionsBa
import RoomHeaderAvatars from '@components/RoomHeaderAvatars';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
+import {useSearchContext} from '@components/Search/SearchContext';
import Text from '@components/Text';
import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
import useLocalize from '@hooks/useLocalize';
@@ -74,10 +75,11 @@ const CASES = {
type CaseID = ValueOf;
-function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
+function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const styles = useThemeStyles();
+ const backTo = route.params.backTo;
// The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here.
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
@@ -86,6 +88,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
const [parentReportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.parentReportID || '-1'}`);
const {reportActions} = usePaginatedReportActions(report.reportID || '-1');
/* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
+ const {currentSearchHash} = useSearchContext();
const transactionThreadReportID = useMemo(
() => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline),
@@ -315,9 +318,9 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
shouldShowRightIcon: true,
action: () => {
if (shouldOpenRoomMembersPage) {
- Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '-1'));
+ Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '-1', backTo));
} else {
- Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '-1'));
+ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '-1', backTo));
}
},
});
@@ -342,7 +345,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
isAnonymousAction: false,
shouldShowRightIcon: true,
action: () => {
- Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '-1'));
+ Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '-1', backTo));
},
});
}
@@ -355,7 +358,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
icon: Expensicons.Pencil,
isAnonymousAction: false,
shouldShowRightIcon: true,
- action: () => ReportUtils.navigateToPrivateNotes(report, session),
+ action: () => ReportUtils.navigateToPrivateNotes(report, session, backTo),
brickRoadIndicator: Report.hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
});
}
@@ -410,7 +413,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
icon: Expensicons.Upload,
isAnonymousAction: false,
action: () => {
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.getRoute(report?.reportID ?? '', connectedIntegration));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.getRoute(report?.reportID ?? '', connectedIntegration, backTo));
},
});
}
@@ -470,6 +473,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
isDebugModeEnabled,
unapproveExpenseReportOrShowModal,
isExpenseReport,
+ backTo,
]);
const displayNamesWithTooltips = useMemo(() => {
@@ -489,10 +493,11 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
/>
) : null;
- const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', connectedIntegration) : '';
+ const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', {connectionName: connectedIntegration}) : '';
const unapproveWarningText = (
- {translate('iou.headsUp')} {translate('iou.unapproveWithIntegrationWarning', connectedIntegrationName)}
+ {translate('iou.headsUp')} {' '}
+ {translate('iou.unapproveWithIntegrationWarning', {accountingIntegration: connectedIntegrationName})}
);
@@ -569,6 +574,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
reportID: transactionThreadReportID ? report.reportID : moneyRequestAction?.childReportID ?? '-1',
isDelegateAccessRestricted,
setIsNoDelegateAccessMenuVisible,
+ currentSearchHash,
}),
);
}
@@ -577,10 +583,21 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
result.push(PromotedActions.pin(report));
}
- result.push(PromotedActions.share(report));
+ result.push(PromotedActions.share(report, backTo));
return result;
- }, [report, moneyRequestAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest, transactionThreadReportID, isDelegateAccessRestricted]);
+ }, [
+ report,
+ moneyRequestAction,
+ currentSearchHash,
+ canJoin,
+ isExpenseReport,
+ shouldShowHoldAction,
+ canHoldUnholdReportAction.canHoldRequest,
+ transactionThreadReportID,
+ isDelegateAccessRestricted,
+ backTo,
+ ]);
const nameSectionExpenseIOU = (
@@ -642,7 +659,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
shouldCheckActionAllowedOnPress={false}
description={!shouldDisableRename ? roomDescription : ''}
furtherDetails={chatRoomSubtitle && !isGroupChat ? additionalRoomDetails : ''}
- onPress={() => Navigation.navigate(ROUTES.REPORT_SETTINGS_NAME.getRoute(report.reportID))}
+ onPress={() => Navigation.navigate(ROUTES.REPORT_SETTINGS_NAME.getRoute(report.reportID, backTo))}
/>
@@ -687,7 +704,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
titleStyle={styles.newKansasLarge}
shouldCheckActionAllowedOnPress={false}
description={Str.UCFirst(titleField.name)}
- onPress={() => Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '-1', titleField.fieldID ?? '-1'))}
+ onPress={() => Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '-1', titleField.fieldID ?? '-1', backTo))}
furtherDetailsComponent={nameSectionFurtherDetailsContent}
/>
@@ -722,7 +739,10 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
return (
-
+ Navigation.goBack(backTo)}
+ />
{renderedAvatar}
@@ -744,7 +764,7 @@ function ReportDetailsPage({policies, report}: ReportDetailsPageProps) {
characterLimit={100}
shouldCheckActionAllowedOnPress={false}
description={translate('reportDescriptionPage.roomDescription')}
- onPress={() => Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID))}
+ onPress={() => Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID, Navigation.getActiveRoute()))}
/>
)}
diff --git a/src/pages/ReportParticipantDetailsPage.tsx b/src/pages/ReportParticipantDetailsPage.tsx
index 6ff710cf23b0..db978b70cad8 100644
--- a/src/pages/ReportParticipantDetailsPage.tsx
+++ b/src/pages/ReportParticipantDetailsPage.tsx
@@ -49,7 +49,7 @@ function ReportParticipantDetails({personalDetails, report, route}: ReportPartic
const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = React.useState(false);
const accountID = Number(route.params.accountID);
- const backTo = ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '-1');
+ const backTo = ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '-1', route.params.backTo);
const member = report?.participants?.[accountID];
const details = personalDetails?.[accountID] ?? ({} as PersonalDetails);
@@ -68,8 +68,8 @@ function ReportParticipantDetails({personalDetails, report, route}: ReportPartic
}, [accountID]);
const openRoleSelectionModal = useCallback(() => {
- Navigation.navigate(ROUTES.REPORT_PARTICIPANTS_ROLE_SELECTION.getRoute(report.reportID, accountID));
- }, [accountID, report.reportID]);
+ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS_ROLE_SELECTION.getRoute(report.reportID, accountID, route.params.backTo));
+ }, [accountID, report.reportID, route.params.backTo]);
if (!member) {
return ;
diff --git a/src/pages/ReportParticipantRoleSelectionPage.tsx b/src/pages/ReportParticipantRoleSelectionPage.tsx
index d2fb5ca365a0..17b84e8903ea 100644
--- a/src/pages/ReportParticipantRoleSelectionPage.tsx
+++ b/src/pages/ReportParticipantRoleSelectionPage.tsx
@@ -30,7 +30,7 @@ function ReportParticipantRoleSelectionPage({report, route}: ReportParticipantRo
const styles = useThemeStyles();
const accountID = Number(route?.params?.accountID) ?? -1;
- const backTo = ROUTES.REPORT_PARTICIPANTS_DETAILS.getRoute(report?.reportID ?? '-1', accountID);
+ const backTo = ROUTES.REPORT_PARTICIPANTS_DETAILS.getRoute(report?.reportID ?? '-1', accountID, route.params.backTo);
const member = report.participants?.[accountID];
if (!member) {
diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx
index 8ce9ec4d09a4..579e6f7ec104 100755
--- a/src/pages/ReportParticipantsPage.tsx
+++ b/src/pages/ReportParticipantsPage.tsx
@@ -1,4 +1,5 @@
import {useIsFocused} from '@react-navigation/native';
+import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
import type {TextInput} from 'react-native';
@@ -28,18 +29,22 @@ import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import * as Report from '@libs/actions/Report';
import * as UserSearchPhraseActions from '@libs/actions/RoomMembersUserSearchPhrase';
import Navigation from '@libs/Navigation/Navigation';
+import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound';
import withReportOrNotFound from './home/report/withReportOrNotFound';
type MemberOption = Omit & {accountID: number};
-function ReportParticipantsPage({report}: WithReportOrNotFoundProps) {
+type ReportParticipantsPageProps = WithReportOrNotFoundProps & StackScreenProps;
+function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) {
+ const backTo = route.params.backTo;
const [selectedMembers, setSelectedMembers] = useState([]);
const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
const {translate, formatPhoneNumber} = useLocalize();
@@ -192,8 +197,8 @@ function ReportParticipantsPage({report}: WithReportOrNotFoundProps) {
* Open the modal to invite a user
*/
const inviteUser = useCallback(() => {
- Navigation.navigate(ROUTES.REPORT_PARTICIPANTS_INVITE.getRoute(report.reportID));
- }, [report]);
+ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS_INVITE.getRoute(report.reportID, backTo));
+ }, [report, backTo]);
/**
* Remove selected users from the workspace
@@ -330,12 +335,12 @@ function ReportParticipantsPage({report}: WithReportOrNotFoundProps) {
const openMemberDetails = useCallback(
(item: MemberOption) => {
if (isGroupChat && isCurrentUserAdmin) {
- Navigation.navigate(ROUTES.REPORT_PARTICIPANTS_DETAILS.getRoute(report.reportID, item.accountID));
+ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS_DETAILS.getRoute(report.reportID, item.accountID, backTo));
return;
}
- Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID));
+ Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID, Navigation.getActiveRoute()));
},
- [report, isCurrentUserAdmin, isGroupChat],
+ [report, isCurrentUserAdmin, isGroupChat, backTo],
);
const headerTitle = useMemo(() => {
if (
@@ -377,7 +382,7 @@ function ReportParticipantsPage({report}: WithReportOrNotFoundProps) {
if (report) {
setSearchValue('');
- Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
+ Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo));
}
}}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx
index 89b5dcdd8a2b..342cd4ce5e6e 100644
--- a/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx
+++ b/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx
@@ -50,10 +50,10 @@ function WorkspaceAdminRestrictedAction({policyID}: WorkspaceAdminRestrictedActi
height={variables.restrictedActionIllustrationHeight}
/>
- {translate('workspace.restrictedAction.actionsAreCurrentlyRestricted', {workspaceName: policy?.name})}
+ {translate('workspace.restrictedAction.actionsAreCurrentlyRestricted', {workspaceName: policy?.name ?? ''})}
- {translate('workspace.restrictedAction.workspaceOwnerWillNeedToAddOrUpdatePaymentCard', {workspaceOwnerName: policy?.owner})}
+ {translate('workspace.restrictedAction.workspaceOwnerWillNeedToAddOrUpdatePaymentCard', {workspaceOwnerName: policy?.owner ?? ''})}
- {translate('workspace.restrictedAction.actionsAreCurrentlyRestricted', {workspaceName: policy?.name})}
+ {translate('workspace.restrictedAction.actionsAreCurrentlyRestricted', {workspaceName: policy?.name ?? ''})}
{translate('workspace.restrictedAction.pleaseReachOutToYourWorkspaceAdmin')}
diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx
index 6589a73c88e8..a70f17d1468c 100644
--- a/src/pages/RoomDescriptionPage.tsx
+++ b/src/pages/RoomDescriptionPage.tsx
@@ -1,4 +1,5 @@
-import {useFocusEffect} from '@react-navigation/native';
+import type {RouteProp} from '@react-navigation/native';
+import {useFocusEffect, useRoute} from '@react-navigation/native';
import React, {useCallback, useRef, useState} from 'react';
import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
@@ -13,6 +14,8 @@ import TextInput from '@components/TextInput';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {ReportDescriptionNavigatorParamList} from '@libs/Navigation/types';
import Parser from '@libs/Parser';
import * as ReportUtils from '@libs/ReportUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
@@ -20,6 +23,8 @@ import variables from '@styles/variables';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/ReportDescriptionForm';
import type * as OnyxTypes from '@src/types/onyx';
@@ -32,6 +37,8 @@ type RoomDescriptionPageProps = {
};
function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) {
+ const route = useRoute>();
+ const backTo = route.params.backTo;
const styles = useThemeStyles();
const [description, setDescription] = useState(() => Parser.htmlToMarkdown(report?.description ?? ''));
const reportDescriptionInputRef = useRef(null);
@@ -43,9 +50,17 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) {
setDescription(value);
}, []);
+ const goBack = useCallback(() => {
+ Navigation.goBack(backTo ?? ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
+ }, [report.reportID, backTo]);
+
const submitForm = useCallback(() => {
- Report.updateDescription(report.reportID, report?.description ?? '', description.trim());
- }, [report.reportID, report.description, description]);
+ const previousValue = report?.description ?? '';
+ const newValue = description.trim();
+
+ Report.updateDescription(report.reportID, previousValue, newValue);
+ goBack();
+ }, [report.reportID, report.description, description, goBack]);
useFocusEffect(
useCallback(() => {
@@ -68,7 +83,10 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) {
includeSafeAreaPaddingBottom={false}
testID={RoomDescriptionPage.displayName}
>
-
+
{canEdit && (
ReportUtils.getReportName(report), [report]);
const inviteUsers = useCallback(() => {
HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS);
diff --git a/src/pages/RoomMemberDetailsPage.tsx b/src/pages/RoomMemberDetailsPage.tsx
index 8b2e89024d8c..475cf37a8847 100644
--- a/src/pages/RoomMemberDetailsPage.tsx
+++ b/src/pages/RoomMemberDetailsPage.tsx
@@ -38,7 +38,7 @@ function RoomMemberDetailsPage({report, route}: RoomMemberDetailsPagePageProps)
const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = React.useState(false);
const accountID = Number(route.params.accountID);
- const backTo = ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '-1');
+ const backTo = ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '-1', route.params.backTo);
const member = report?.participants?.[accountID];
const details = personalDetails?.[accountID] ?? ({} as PersonalDetails);
diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx
index 19e631098e40..765c738bc574 100644
--- a/src/pages/RoomMembersPage.tsx
+++ b/src/pages/RoomMembersPage.tsx
@@ -1,4 +1,5 @@
-import {useIsFocused} from '@react-navigation/native';
+import type {RouteProp} from '@react-navigation/native';
+import {useIsFocused, useRoute} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
@@ -55,6 +56,7 @@ type RoomMembersPageProps = WithReportOrNotFoundProps &
StackScreenProps;
function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
+ const route = useRoute>();
const styles = useThemeStyles();
const {formatPhoneNumber, translate} = useLocalize();
const [selectedMembers, setSelectedMembers] = useState([]);
@@ -65,6 +67,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]);
const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(report), [report]);
+ const backTo = route.params.backTo;
const isFocusedScreen = useIsFocused();
const {isOffline} = useNetwork();
@@ -113,8 +116,8 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
return;
}
setSearchValue('');
- Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(report.reportID));
- }, [report, setSearchValue]);
+ Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(report.reportID, undefined, backTo));
+ }, [report, setSearchValue, backTo]);
/**
* Remove selected users from the room
@@ -311,9 +314,9 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
/** Opens the room member details page */
const openRoomMemberDetails = useCallback(
(item: ListItem) => {
- Navigation.navigate(ROUTES.ROOM_MEMBER_DETAILS.getRoute(report.reportID, item?.accountID ?? -1));
+ Navigation.navigate(ROUTES.ROOM_MEMBER_DETAILS.getRoute(report.reportID, item?.accountID ?? -1, backTo));
},
- [report],
+ [report, backTo],
);
const selectionModeHeader = selectionMode?.isEnabled && isSmallScreenWidth;
@@ -345,7 +348,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
}
subtitleKey={isEmptyObject(report) ? undefined : 'roomMembersPage.notAuthorized'}
onBackButtonPress={() => {
- Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
+ Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo));
}}
>
{headerButtons}
diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx
index 87ee65a7e2df..c116a9dc5fbc 100644
--- a/src/pages/Search/AdvancedSearchFilters.tsx
+++ b/src/pages/Search/AdvancedSearchFilters.tsx
@@ -144,13 +144,13 @@ function getFilterDisplayTitle(filters: Partial, fiel
const {dateAfter, dateBefore} = filters;
let dateValue = '';
if (dateBefore) {
- dateValue = translate('search.filters.date.before', dateBefore);
+ dateValue = translate('search.filters.date.before', {date: dateBefore});
}
if (dateBefore && dateAfter) {
dateValue += ', ';
}
if (dateAfter) {
- dateValue += translate('search.filters.date.after', dateAfter);
+ dateValue += translate('search.filters.date.after', {date: dateAfter});
}
return dateValue;
@@ -159,13 +159,16 @@ function getFilterDisplayTitle(filters: Partial, fiel
if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) {
const {lessThan, greaterThan} = filters;
if (lessThan && greaterThan) {
- return translate('search.filters.amount.between', convertToDisplayStringWithoutCurrency(Number(greaterThan)), convertToDisplayStringWithoutCurrency(Number(lessThan)));
+ return translate('search.filters.amount.between', {
+ lessThan: convertToDisplayStringWithoutCurrency(Number(lessThan)),
+ greaterThan: convertToDisplayStringWithoutCurrency(Number(greaterThan)),
+ });
}
if (lessThan) {
- return translate('search.filters.amount.lessThan', convertToDisplayStringWithoutCurrency(Number(lessThan)));
+ return translate('search.filters.amount.lessThan', {amount: convertToDisplayStringWithoutCurrency(Number(lessThan))});
}
if (greaterThan) {
- return translate('search.filters.amount.greaterThan', convertToDisplayStringWithoutCurrency(Number(greaterThan)));
+ return translate('search.filters.amount.greaterThan', {amount: convertToDisplayStringWithoutCurrency(Number(greaterThan))});
}
// Will never happen
return;
diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx
index edcf6ab23219..c1ea305c5b68 100644
--- a/src/pages/Search/EmptySearchView.tsx
+++ b/src/pages/Search/EmptySearchView.tsx
@@ -1,36 +1,103 @@
-import React, {useMemo} from 'react';
+import React, {useMemo, useState} from 'react';
+import {Linking, View} from 'react-native';
+import DotIndicatorMessage from '@components/DotIndicatorMessage';
import EmptyStateComponent from '@components/EmptyStateComponent';
+import type {FeatureListItem} from '@components/FeatureList';
+import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
+import MenuItem from '@components/MenuItem';
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
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 * as TripsResevationUtils from '@libs/TripReservationUtils';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
-import ROUTES from '@src/ROUTES';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
type EmptySearchViewProps = {
type: SearchDataTypes;
};
+const tripsFeatures: FeatureListItem[] = [
+ {
+ icon: Illustrations.PiggyBank,
+ translationKey: 'travel.features.saveMoney',
+ },
+ {
+ icon: Illustrations.Alert,
+ translationKey: 'travel.features.alerts',
+ },
+];
+
function EmptySearchView({type}: EmptySearchViewProps) {
const theme = useTheme();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const [ctaErrorMessage, setCtaErrorMessage] = useState('');
+
+ const subtitleComponent = useMemo(() => {
+ return (
+ <>
+
+ {translate('travel.subtitle')}{' '}
+ {
+ Linking.openURL(CONST.BOOK_TRAVEL_DEMO_URL);
+ }}
+ >
+ {translate('travel.bookADemo')}
+
+ {translate('travel.toLearnMore')}
+
+
+ {tripsFeatures.map((tripsFeature) => (
+
+
+
+ ))}
+
+ {ctaErrorMessage && (
+
+ )}
+ >
+ );
+ }, [styles, translate, ctaErrorMessage]);
+
const content = useMemo(() => {
switch (type) {
case CONST.SEARCH.DATA_TYPES.TRIP:
return {
headerMedia: LottieAnimations.TripsEmptyState,
- headerStyles: [StyleUtils.getBackgroundColorStyle(theme.travelBG), styles.w100],
- title: translate('search.searchResults.emptyTripResults.title'),
- subtitle: translate('search.searchResults.emptyTripResults.subtitle'),
+ headerStyles: StyleUtils.getBackgroundColorStyle(theme.travelBG),
+ headerContentStyles: StyleUtils.getWidthAndHeightStyle(375, 240),
+ title: translate('travel.title'),
+ titleStyles: {...styles.textAlignLeft},
+ subtitle: subtitleComponent,
buttonText: translate('search.searchResults.emptyTripResults.buttonText'),
- buttonAction: () => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS),
+ buttonAction: () => TripsResevationUtils.bookATrip(translate, setCtaErrorMessage, ctaErrorMessage),
};
case CONST.SEARCH.DATA_TYPES.CHAT:
case CONST.SEARCH.DATA_TYPES.EXPENSE:
@@ -46,7 +113,7 @@ function EmptySearchView({type}: EmptySearchViewProps) {
headerContentStyles: styles.emptyStateFolderWebStyles,
};
}
- }, [type, StyleUtils, translate, theme, styles.w100, styles.emptyStateFolderWebStyles]);
+ }, [type, StyleUtils, translate, theme, styles, subtitleComponent, ctaErrorMessage]);
return (
(null);
const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0});
-
+ const styles = useThemeStyles();
return (
-
+