diff --git a/.eslintrc.js b/.eslintrc.js
index 77c7fafb7a02..5f450f3ae6c2 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 6b78a204b57a..62d326c9af3a 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 986c6c12afb9..9c2defebd01d 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/scripts/verifyDeploy.sh b/.github/scripts/verifyDeploy.sh
new file mode 100755
index 000000000000..0a8fd3c97bcf
--- /dev/null
+++ b/.github/scripts/verifyDeploy.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+ENV="$1"
+EXPECTED_VERSION="$2"
+
+BASE_URL=""
+if [[ "$ENV" == 'staging' ]]; then
+ BASE_URL='https://staging.new.expensify.com'
+else
+ BASE_URL='https://new.expensify.com'
+fi
+
+sleep 5
+ATTEMPT=0
+MAX_ATTEMPTS=10
+while [[ $ATTEMPT -lt $MAX_ATTEMPTS ]]; do
+ ((ATTEMPT++))
+
+ echo "Attempt $ATTEMPT: Checking deployed version..."
+ DOWNLOADED_VERSION="$(wget -q -O /dev/stdout "$BASE_URL"/version.json | jq -r '.version')"
+
+ if [[ "$EXPECTED_VERSION" == "$DOWNLOADED_VERSION" ]]; then
+ echo "Success: Deployed version matches local version: $DOWNLOADED_VERSION"
+ exit 0
+ fi
+
+ if [[ $ATTEMPT -lt $MAX_ATTEMPTS ]]; then
+ echo "Version mismatch, found $DOWNLOADED_VERSION. Retrying in 5 seconds..."
+ sleep 5
+ fi
+done
+
+echo "Error: Deployed version did not match local version after $MAX_ATTEMPTS attempts. Something went wrong..."
+exit 1
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 2ab19d13183a..99cd0c1dabc5 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -386,23 +386,11 @@ jobs:
- name: Verify staging deploy
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- run: |
- sleep 5
- DOWNLOADED_VERSION="$(wget -q -O /dev/stdout https://staging.new.expensify.com/version.json | jq -r '.version')"
- if [[ '${{ needs.prep.outputs.APP_VERSION }}' != "$DOWNLOADED_VERSION" ]]; then
- echo "Error: deployed version $DOWNLOADED_VERSION does not match local version ${{ needs.prep.outputs.APP_VERSION }}. Something went wrong..."
- exit 1
- fi
+ run: ./.github/scripts/verifyDeploy.sh staging ${{ needs.prep.outputs.APP_VERSION }}
- name: Verify production deploy
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- run: |
- sleep 5
- DOWNLOADED_VERSION="$(wget -q -O /dev/stdout https://new.expensify.com/version.json | jq -r '.version')"
- if [[ '${{ needs.prep.outputs.APP_VERSION }}' != "$DOWNLOADED_VERSION" ]]; then
- echo "Error: deployed version $DOWNLOADED_VERSION does not match local version ${{ needs.prep.outputs.APP_VERSION }}. Something went wrong..."
- exit 1
- fi
+ run: ./.github/scripts/verifyDeploy.sh production ${{ needs.prep.outputs.APP_VERSION }}
- name: Upload web sourcemaps artifact
uses: actions/upload-artifact@v4
@@ -507,11 +495,13 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
- name: Rename web and desktop sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name
+ continue-on-error: true
run: |
mv ./desktop-staging-sourcemaps-artifact/merged-source-map.js.map ./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map
mv ./web-staging-sourcemaps-artifact/merged-source-map.js.map ./web-staging-sourcemaps-artifact/web-staging-merged-source-map.js.map
- name: Upload artifacts to GitHub Release
+ continue-on-error: true
run: |
gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber \
./android-sourcemaps-artifact/index.android.bundle.map#android-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \
@@ -552,11 +542,6 @@ jobs:
- name: Download all workflow run artifacts
uses: actions/download-artifact@v4
- - name: Rename web and desktop sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name
- run: |
- mv ./desktop-sourcemaps-artifact/merged-source-map.js.map ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map
- mv ./web-sourcemaps-artifact/merged-source-map.js.map ./web-sourcemaps-artifact/web-merged-source-map.js.map
-
- name: 🚀 Edit the release to be no longer a prerelease 🚀
run: |
LATEST_RELEASE="$(gh release list --repo ${{ github.repository }} --exclude-pre-releases --json tagName,isLatest --jq '.[] | select(.isLatest) | .tagName')"
@@ -565,7 +550,14 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
+ - name: Rename web and desktop sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name
+ continue-on-error: true
+ run: |
+ mv ./desktop-sourcemaps-artifact/merged-source-map.js.map ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map
+ mv ./web-sourcemaps-artifact/merged-source-map.js.map ./web-sourcemaps-artifact/web-merged-source-map.js.map
+
- name: Upload artifacts to GitHub Release
+ continue-on-error: true
run: |
gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber \
./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map#desktop-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \
@@ -649,34 +641,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/deployNewHelp.yml b/.github/workflows/deployNewHelp.yml
new file mode 100644
index 000000000000..8c4d0fb0ae3b
--- /dev/null
+++ b/.github/workflows/deployNewHelp.yml
@@ -0,0 +1,74 @@
+name: Deploy New Help Site
+
+on:
+ # Run on any push to main that has changes to the help directory
+# TEST: Verify Cloudflare picks this up even if not run when merged to main
+# push:
+# branches:
+# - main
+# paths:
+# - 'help/**'
+
+ # Run on any pull request (except PRs against staging or production) that has changes to the help directory
+ pull_request:
+ types: [opened, synchronize]
+ branches-ignore: [staging, production]
+ paths:
+ - 'help/**'
+
+ # Run on any manual trigger
+ workflow_dispatch:
+
+# Allow only one concurrent deployment
+concurrency:
+ group: "newhelp"
+ cancel-in-progress: false
+
+jobs:
+ build:
+ env:
+ IS_PR_FROM_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }}
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ # Set up Ruby and run bundle install inside the /help directory
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+ working-directory: ./help
+
+ - name: Build Jekyll site
+ run: bundle exec jekyll build --source ./ --destination ./_site
+ working-directory: ./help # Ensure Jekyll is building the site in /help
+
+ - name: Deploy to Cloudflare Pages
+ uses: cloudflare/pages-action@v1
+ id: deploy
+ if: env.IS_PR_FROM_FORK != 'true'
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ projectName: newhelp
+ directory: ./help/_site # Deploy the built site
+
+ - name: Setup Cloudflare CLI
+ if: env.IS_PR_FROM_FORK != 'true'
+ run: pip3 install cloudflare==2.19.0
+
+ - name: Purge Cloudflare cache
+ if: env.IS_PR_FROM_FORK != 'true'
+ run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["newhelp.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache
+ env:
+ CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }}
+
+ - name: Leave a comment on the PR
+ uses: actions-cool/maintain-one-comment@v3.2.0
+ if: ${{ github.event_name == 'pull_request' && env.IS_PR_FROM_FORK != 'true' }}
+ with:
+ token: ${{ github.token }}
+ body: ${{ format('A preview of your New Help changes have been deployed to {0} :zap:️', steps.deploy.outputs.alias) }}
+
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/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml
index 796468170275..bfe860e60224 100644
--- a/.github/workflows/preDeploy.yml
+++ b/.github/workflows/preDeploy.yml
@@ -4,7 +4,7 @@ name: Process new code merged to main
on:
push:
branches: [main]
- paths-ignore: [docs/**, contributingGuides/**, jest/**, tests/**]
+ paths-ignore: [docs/**, help/**, contributingGuides/**, jest/**, tests/**]
jobs:
typecheck:
diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml
index d4a25a63952b..fb7a34d6fa01 100644
--- a/.github/workflows/reassurePerformanceTests.yml
+++ b/.github/workflows/reassurePerformanceTests.yml
@@ -4,7 +4,7 @@ on:
pull_request:
types: [opened, synchronize]
branches-ignore: [staging, production]
- paths-ignore: [docs/**, .github/**, contributingGuides/**, tests/**, '**.md', '**.sh']
+ paths-ignore: [docs/**, help/**, .github/**, contributingGuides/**, tests/**, '**.md', '**.sh']
jobs:
perf-tests:
diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml
index 42d946cece95..884182bfc896 100644
--- a/.github/workflows/sendReassurePerfData.yml
+++ b/.github/workflows/sendReassurePerfData.yml
@@ -3,7 +3,7 @@ name: Send Reassure Performance Tests to Graphite
on:
push:
branches: [main]
- paths-ignore: [docs/**, contributingGuides/**, jest/**]
+ paths-ignore: [docs/**, help/**, contributingGuides/**, jest/**]
jobs:
perf-tests:
diff --git a/.prettierignore b/.prettierignore
index a9f7e1464529..98d06e8c5f71 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -15,6 +15,7 @@ package-lock.json
*.css
*.scss
*.md
+*.markdown
# We need to modify the import here specifically, hence we disable prettier to get rid of the sorted imports
src/libs/E2E/reactNativeLaunchingTest.ts
# Temporary while we keep react-compiler in our repo
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 0594d6afc211..3a620c00fd95 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 1009003904
- versionName "9.0.39-4"
+ versionCode 1009004103
+ versionName "9.0.41-3"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/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/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/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md
index 787602337bd2..73e3340d41a2 100644
--- a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md
+++ b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md
@@ -1,9 +1,76 @@
---
title: Configure Quickbooks Online
-description: Coming Soon
+description: Configure your QuickBooks Online connection with Expensify
---
-# FAQ
+Once you've set up your QuickBooks Online connection, you'll be able to configure your import and export settings.
+
+# Step 1: Configure import settings
+
+The following steps help you determine how data will be imported from QuickBooks Online to Expensify.
+
+
+ Under the Accounting settings for your workspace, click Import under the QuickBooks Online connection.
+ Review each of the following import settings:
+
+ Chart of accounts : The chart of accounts are automatically imported from QuickBooks Online as categories. This cannot be amended.
+ Classes : Choose whether to import classes, which will be shown in Expensify as tags for expense-level coding.
+ Customers/projects : Choose whether to import customers/projects, which will be shown in Expensify as tags for expense-level coding.
+ Locations : Choose whether to import locations, which will be shown in Expensify as tags for expense-level coding.
+{% include info.html %}
+As Locations are only configurable as tags, you cannot export expense reports as vendor bills or checks to QuickBooks Online. To unlock these export options, either disable locations import or upgrade to the Control Plan to export locations encoded as a report field.
+{% include end-info.html %}
+ Taxes : Choose whether to import tax rates and defaults.
+
+
+
+# Step 2: Configure export settings
+
+The following steps help you determine how data will be exported from Expensify to QuickBooks Online.
+
+
+ Under the Accounting settings for your workspace, click Export under the QuickBooks Online connection.
+ Review each of the following export settings:
+
+ Preferred Exporter : Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
+
+{% include info.html %}
+* Other Workspace Admins will still be able to export to QuickBooks Online.
+* If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin.
+{% include end-info.html %}
+
+ Date : Choose whether to use the date of last expense, export date, or submitted date.
+ Export Out-of-Pocket Expenses as : Select whether out-of-pocket expenses will be exported as a check, journal entry, or vendor bill.
+
+{% include info.html %}
+These settings may vary based on whether tax is enabled for your workspace.
+* If tax is not enabled on the workspace, you’ll also select the Accounts Payable/AP.
+* If tax is enabled on the workspace, journal entry will not be available as an option. If you select the journal entries option first and later enable tax on the workspace, you will see a red dot and an error message under the “Export Out-of-Pocket Expenses as” options. To resolve this error, you must change your export option to vendor bill or check to successfully code and export expense reports.
+{% include end-info.html %}
+
+ Invoices : Select the QuickBooks Online invoice account that invoices will be exported to.
+ Export as : Select whether company cards export to QuickBooks Online as a credit card (the default), debit card, or vendor bill. Then select the account they will export to.
+ If you select vendor bill, you’ll also select the accounts payable account that vendor bills will be created from, as well as whether to set a default vendor for credit card transactions upon export. If this option is enabled, you will select the vendor that all credit card transactions will be applied to.
+
+
+
+# Step 3: Configure advanced settings
+
+The following steps help you determine the advanced settings for your connection, like auto-sync and employee invitation settings.
+
+
+ Under the Accounting settings for your workspace, click Advanced under the QuickBooks Online connection.
+ Select an option for each of the following settings:
+
+ Auto-sync : Choose whether to enable QuickBooks Online to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period.
+ Invite Employees : Choose whether to enable Expensify to import employee records from QuickBooks Online and invite them to this workspace.
+ Automatically Create Entities : Choose whether to enable Expensify to automatically create vendors and customers in QuickBooks Online if a matching vendor or customer does not exist.
+ Sync Reimbursed Reports : Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in QuickBooks Online will also show in Expensify as Paid. If enabled, you must also select the QuickBooks Online account that reimbursements are coming out of, and Expensify will automatically create the payment in QuickBooks Online.
+ Invoice Collection Account : Select the invoice collection account that you want invoices to appear under once the invoice is marked as paid.
+
+
+
+{% include faq-begin.md %}
## How do I know if a report is successfully exported to QuickBooks Online?
@@ -22,3 +89,5 @@ When an admin manually exports a report, Expensify will notify them if the repor
- If a report has been exported and marked as paid in QuickBooks Online, it will be automatically marked as reimbursed in Expensify during the next sync.
Reports that have yet to be exported to QuickBooks Online won’t be automatically exported.
+
+{% include faq-end.md %}
diff --git a/docs/articles/new-expensify/expenses-&-payments/Pay-an-invoice.md b/docs/articles/new-expensify/expenses-&-payments/Pay-an-invoice.md
index 727c6b86b7a6..615fac731c41 100644
--- a/docs/articles/new-expensify/expenses-&-payments/Pay-an-invoice.md
+++ b/docs/articles/new-expensify/expenses-&-payments/Pay-an-invoice.md
@@ -32,6 +32,8 @@ To pay an invoice,
You can also view all unpaid invoices by searching for the sender’s email or phone number on the left-hand side of the app. The invoices waiting for your payment will have a green dot.
+![Click Pay Button on the Invoice]({{site.url}}/assets/images/ExpensifyHelp-Invoice-1.png){:width="100%"}
+
{% include faq-begin.md %}
**Can someone else pay an invoice besides the person who received it?**
diff --git a/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md b/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md
index 85bd6b655186..57b81a031a01 100644
--- a/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md
+++ b/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md
@@ -57,6 +57,18 @@ Only workspace admins can send invoices. Invoices can be sent directly from Expe
{% include end-selector.html %}
+![Go to Account Settings click Workspace]({{site.url}}/assets/images/invoices_01.png){:width="100%"}
+
+![Click More Features for the workspace and enable Invoices]({{site.url}}/assets/images/invoices_02.png){:width="100%"}
+
+![Click the green button Send Invoice]({{site.url}}/assets/images/invoices_03.png){:width="100%"}
+
+![Enter Invoice amount]({{site.url}}/assets/images/invoices_04.png){:width="100%"}
+
+![Choose a recipient]({{site.url}}/assets/images/invoices_05.png){:width="100%"}
+
+![Add Invoice details and Send Invoice]({{site.url}}/assets/images/invoices_06.png){:width="100%"}
+
# Receive invoice payment
If you have not [connected a business bank account](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account) to receive invoice payments, you will see an **Invoice balance** in your [Wallet](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Set-up-your-wallet). Expensify will automatically transfer these invoice payments once a business bank account is connected.
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/help/.gitignore b/help/.gitignore
new file mode 100644
index 000000000000..f40fbd8ba564
--- /dev/null
+++ b/help/.gitignore
@@ -0,0 +1,5 @@
+_site
+.sass-cache
+.jekyll-cache
+.jekyll-metadata
+vendor
diff --git a/help/.ruby-version b/help/.ruby-version
new file mode 100644
index 000000000000..a0891f563f38
--- /dev/null
+++ b/help/.ruby-version
@@ -0,0 +1 @@
+3.3.4
diff --git a/help/404.html b/help/404.html
new file mode 100644
index 000000000000..086a5c9ea988
--- /dev/null
+++ b/help/404.html
@@ -0,0 +1,25 @@
+---
+permalink: /404.html
+layout: default
+---
+
+
+
+
+
404
+
+
Page not found :(
+
The requested page could not be found.
+
diff --git a/help/Gemfile b/help/Gemfile
new file mode 100644
index 000000000000..4f2e425b8aba
--- /dev/null
+++ b/help/Gemfile
@@ -0,0 +1,19 @@
+source "https://rubygems.org"
+
+gem "jekyll", "~> 4.3.4"
+gem "minima", "~> 2.5"
+gem "nokogiri"
+
+group :jekyll_plugins do
+ gem "jekyll-feed", "~> 0.12"
+end
+
+# If using tzinfo-data for timezone support, ensure it's bundled for relevant platforms
+platforms :mingw, :x64_mingw, :mswin, :jruby do
+ gem "tzinfo", ">= 1", "< 3"
+ gem "tzinfo-data"
+end
+
+gem "wdm", "~> 0.1", platforms: [:mingw, :x64_mingw, :mswin]
+gem "http_parser.rb", "~> 0.6.0", platforms: [:jruby]
+
diff --git a/help/Gemfile.lock b/help/Gemfile.lock
new file mode 100644
index 000000000000..7434e1c4e935
--- /dev/null
+++ b/help/Gemfile.lock
@@ -0,0 +1,200 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
+ bigdecimal (3.1.8)
+ colorator (1.1.0)
+ concurrent-ruby (1.3.4)
+ em-websocket (0.5.3)
+ eventmachine (>= 0.12.9)
+ http_parser.rb (~> 0)
+ eventmachine (1.2.7)
+ ffi (1.17.0)
+ ffi (1.17.0-aarch64-linux-gnu)
+ ffi (1.17.0-aarch64-linux-musl)
+ ffi (1.17.0-arm-linux-gnu)
+ ffi (1.17.0-arm-linux-musl)
+ ffi (1.17.0-arm64-darwin)
+ ffi (1.17.0-x86-linux-gnu)
+ ffi (1.17.0-x86-linux-musl)
+ ffi (1.17.0-x86_64-darwin)
+ ffi (1.17.0-x86_64-linux-gnu)
+ ffi (1.17.0-x86_64-linux-musl)
+ forwardable-extended (2.6.0)
+ google-protobuf (4.28.2)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.28.2-aarch64-linux)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.28.2-arm64-darwin)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.28.2-x86-linux)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.28.2-x86_64-darwin)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.28.2-x86_64-linux)
+ bigdecimal
+ rake (>= 13)
+ http_parser.rb (0.8.0)
+ i18n (1.14.6)
+ concurrent-ruby (~> 1.0)
+ jekyll (4.3.4)
+ addressable (~> 2.4)
+ colorator (~> 1.0)
+ em-websocket (~> 0.5)
+ i18n (~> 1.0)
+ jekyll-sass-converter (>= 2.0, < 4.0)
+ jekyll-watch (~> 2.0)
+ kramdown (~> 2.3, >= 2.3.1)
+ kramdown-parser-gfm (~> 1.0)
+ liquid (~> 4.0)
+ mercenary (>= 0.3.6, < 0.5)
+ pathutil (~> 0.9)
+ rouge (>= 3.0, < 5.0)
+ safe_yaml (~> 1.0)
+ terminal-table (>= 1.8, < 4.0)
+ webrick (~> 1.7)
+ jekyll-feed (0.17.0)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-sass-converter (3.0.0)
+ sass-embedded (~> 1.54)
+ jekyll-seo-tag (2.8.0)
+ jekyll (>= 3.8, < 5.0)
+ jekyll-watch (2.2.1)
+ listen (~> 3.0)
+ kramdown (2.4.0)
+ rexml
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ liquid (4.0.4)
+ listen (3.9.0)
+ rb-fsevent (~> 0.10, >= 0.10.3)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ mercenary (0.4.0)
+ mini_portile2 (2.8.7)
+ minima (2.5.2)
+ jekyll (>= 3.5, < 5.0)
+ jekyll-feed (~> 0.9)
+ jekyll-seo-tag (~> 2.1)
+ nokogiri (1.16.7)
+ mini_portile2 (~> 2.8.2)
+ racc (~> 1.4)
+ nokogiri (1.16.7-aarch64-linux)
+ racc (~> 1.4)
+ nokogiri (1.16.7-arm-linux)
+ racc (~> 1.4)
+ nokogiri (1.16.7-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.16.7-x86-linux)
+ racc (~> 1.4)
+ nokogiri (1.16.7-x86_64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.16.7-x86_64-linux)
+ racc (~> 1.4)
+ pathutil (0.16.2)
+ forwardable-extended (~> 2.6)
+ public_suffix (6.0.1)
+ racc (1.8.1)
+ rake (13.2.1)
+ rb-fsevent (0.11.2)
+ rb-inotify (0.11.1)
+ ffi (~> 1.0)
+ rexml (3.3.7)
+ rouge (4.4.0)
+ safe_yaml (1.0.5)
+ sass-embedded (1.79.3)
+ google-protobuf (~> 4.27)
+ rake (>= 13)
+ sass-embedded (1.79.3-aarch64-linux-android)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-aarch64-linux-gnu)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-aarch64-linux-musl)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-aarch64-mingw-ucrt)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-arm-linux-androideabi)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-arm-linux-gnueabihf)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-arm-linux-musleabihf)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-arm64-darwin)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-riscv64-linux-android)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-riscv64-linux-gnu)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-riscv64-linux-musl)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86-cygwin)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86-linux-android)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86-linux-gnu)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86-linux-musl)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86-mingw-ucrt)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86_64-cygwin)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86_64-darwin)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86_64-linux-android)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86_64-linux-gnu)
+ google-protobuf (~> 4.27)
+ sass-embedded (1.79.3-x86_64-linux-musl)
+ google-protobuf (~> 4.27)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ unicode-display_width (2.6.0)
+ webrick (1.8.2)
+
+PLATFORMS
+ aarch64-linux
+ aarch64-linux-android
+ aarch64-linux-gnu
+ aarch64-linux-musl
+ aarch64-mingw-ucrt
+ arm-linux-androideabi
+ arm-linux-gnu
+ arm-linux-gnueabihf
+ arm-linux-musl
+ arm-linux-musleabihf
+ arm64-darwin
+ riscv64-linux-android
+ riscv64-linux-gnu
+ riscv64-linux-musl
+ ruby
+ x86-cygwin
+ x86-linux
+ x86-linux-android
+ x86-linux-gnu
+ x86-linux-musl
+ x86-mingw-ucrt
+ x86_64-cygwin
+ x86_64-darwin
+ x86_64-linux
+ x86_64-linux-android
+ x86_64-linux-gnu
+ x86_64-linux-musl
+
+DEPENDENCIES
+ http_parser.rb (~> 0.6.0)
+ jekyll (~> 4.3.4)
+ jekyll-feed (~> 0.12)
+ minima (~> 2.5)
+ nokogiri
+ tzinfo (>= 1, < 3)
+ tzinfo-data
+ wdm (~> 0.1)
+
+BUNDLED WITH
+ 2.5.19
diff --git a/help/_config.yml b/help/_config.yml
new file mode 100644
index 000000000000..9135a372964e
--- /dev/null
+++ b/help/_config.yml
@@ -0,0 +1,7 @@
+title: New Expensify Help
+email: concierge@expensify.com
+description: Comprehensive help documentation for New Expensify.
+url: https://newhelp.expensify.com
+twitter_username: expensify
+github_username: expensify
+
diff --git a/help/_layouts/default.html b/help/_layouts/default.html
new file mode 100644
index 000000000000..cf95e1f54b06
--- /dev/null
+++ b/help/_layouts/default.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
{{ page.title }}
+
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
diff --git a/help/_layouts/product.html b/help/_layouts/product.html
new file mode 100644
index 000000000000..cb8b5e882f24
--- /dev/null
+++ b/help/_layouts/product.html
@@ -0,0 +1,11 @@
+---
+layout: default
+---
+
+
{{ page.title }}
+
+
+
+ {{ content }}
+
+
diff --git a/help/_plugins/51_HeaderIDPostRender.rb b/help/_plugins/51_HeaderIDPostRender.rb
new file mode 100644
index 000000000000..4af97cc788f6
--- /dev/null
+++ b/help/_plugins/51_HeaderIDPostRender.rb
@@ -0,0 +1,59 @@
+require 'nokogiri'
+require 'cgi' # Use CGI for URL encoding
+
+module Jekyll
+ class HeaderIDPostRender
+ # Hook into Jekyll's post_render stage to ensure we work with the final HTML
+ Jekyll::Hooks.register :pages, :post_render, priority: 51 do |page|
+ process_page(page)
+ end
+
+ Jekyll::Hooks.register :documents, :post_render, priority: 51 do |post|
+ process_page(post)
+ end
+
+ def self.process_page(page)
+ return unless page.output_ext == ".html" # Only apply to HTML pages
+ return if page.output.nil? # Skip if no output has been generated
+
+ puts " Processing page: #{page.path}"
+
+ # Parse the page's content for header elements
+ doc = Nokogiri::HTML(page.output)
+ h1_id = ""
+ h2_id = ""
+ h3_id = ""
+
+ # Process all
, , and elements
+ (2..4).each do |level|
+ doc.css("h#{level}").each do |header|
+ header_text = header.text.strip.downcase
+ header_id = CGI.escape(header_text.gsub(/\s+/, '-').gsub(/[^\w\-]/, ''))
+
+ puts " Found h#{level}: '#{header_text}' -> ID: '#{header_id}'"
+
+ # Create hierarchical IDs by appending to the parent header IDs
+ if level == 2
+ h2_id = header_id
+ header['id'] = h2_id
+ elsif level == 3
+ h3_id = "#{h2_id}:#{header_id}"
+ header['id'] = h3_id
+ elsif level == 4
+ h4_id = "#{h3_id}:#{header_id}"
+ header['id'] = h4_id
+ end
+
+ puts " Assigned ID: #{header['id']}"
+ end
+ end
+
+ # Log the final output being written
+ puts " Writing updated HTML for page: #{page.path}"
+
+ # Write the updated HTML back to the page
+ page.output = doc.to_html
+ end
+ end
+end
+
diff --git a/help/index.md b/help/index.md
new file mode 100644
index 000000000000..e5d075402ecb
--- /dev/null
+++ b/help/index.md
@@ -0,0 +1,5 @@
+---
+title: New Expensify Help
+---
+Pages:
+* [Expensify Superapp](/superapp.html)
diff --git a/help/robots.txt b/help/robots.txt
new file mode 100644
index 000000000000..6ffbc308f73e
--- /dev/null
+++ b/help/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Disallow: /
+
diff --git a/help/superapp.md b/help/superapp.md
new file mode 100644
index 000000000000..d09860a1ce7e
--- /dev/null
+++ b/help/superapp.md
@@ -0,0 +1,115 @@
+---
+layout: product
+title: Expensify Superapp
+---
+
+## Introduction
+The Expensify Superapp packs the full power of 6 world class business, finance, and collaboration products, into a single app that works identically on desktop and mobile, efficiently with your colleagues, and seamlessly with your customers, vendors, family, and friends.
+
+### When should I use Expensify?
+Expensify can do a lot. You should check us out whenever you need to:
+
+Track and manage expenses
+: Whether you are reimbursing employee receipts, deducting personal expenses, or just splitting the bill, Expensify Expense is for you.
+
+Issue corporate cards
+: Skip the reimbursement and capture receipts electronically in realtime by issuing the Expensify Card to yourself and your employees.
+
+Book and manage travel
+: If you are booking your own business trip, arranging a trip for a colleague, or managing the travel of your whole company, Expensify Travel has got you covered.
+
+Chat with friends and coworkers
+: Whether it's collaborating with your team, supporting you client, negotiating with your vendor, or just saying Hi to a friend, Expensify Chat connects you with anyone with an email address or SMS number
+
+Collect invoice payments online
+: Expensify Invoice allows you to collect online payments from consumers and businesses alike – anyone with an email address or SMS number.
+
+Approve and pay bills online
+: Scan, process, and approve bills online using Expensify Billpay, then we'll pay them electronically or via check, whatever they prefer.
+
+If you send, receive, or spend money – or even just talk to literally anyone, about literally anything – Expensify is the tool for you.
+
+### Who uses Expensify?
+Expensify offers something for everyone. Some people who commonly use us include:
+
+Individuals
+: Millions of individuals use Expensify to track personal expenses to maximize their tax deductions, stay within personal budgets, or just see where their money is going.
+
+Friends
+: Expensify is a great way to split bills with friends, whether it's monthly rent and household expenses, a big ticket bachelorette party, or just grabbing drinks with friends.
+
+Employees
+: Road warriors and desk jockeys alike count on Expensify to reimburse expense reports they create in international airports, swanky hotels, imposing conference centers, quaint coffeeshops, and boring office supply stores around the world.
+
+Managers
+: Bosses manage corporate spend with Expensify to empower their best (and keep tabs on their… not so best), staying ahead of schedule and under budget.
+
+Accountants
+: Internal accountants, fractional CFOs, CAS practices – you name it, they use Expensify to Invoice customers, process vendor bills, capture eReceipts, manage corporate spend: the whole shebang. If you're an accountant, we're already best friends.
+
+Travel managers
+: Anyone looking to manage employee travel has come to the right place.
+
+If you are a person online who does basically anything, you can probably do it with Expensify.
+
+### Why should I use Expensify?
+Though we do a lot, you've got a lot of options for everything we do. But you should use us because we are:
+Simple enough for individuals - We've worked extremely hard to make a product that strips out all the complex jargon and enterprise baggage, and gives you a simple tool that doesn't overwhelm you with functionality and language you don't understand.
+
+Powerful enough for enterprises
+: We've worked extremely hard to make a product that "scales up" to reveal increasingly sophisticated features, but only to those who need it, and only when they need it. Expensify is used by public companies, multinational companies, companies with tens of thousands of employees, non-profits, investment firms, accounting firms, manufacturers, and basically every industry in every currency and in every country around the world. If you are a company, we can support your needs, no matter how big or small.
+
+6 products for the price of 1
+: Do you pay for an expense management system? A corporate card? A travel management platform? An enterprise chat tool? An invoicing tool? A billpay tool? Now you don't need to. Expensify's superapp design allows us to offer ALL these features on a single platform, at probably less than what you pay for any of them individually.
+
+Supports everyone everywhere
+: Expensify works on iPhones and Androids, desktops and browsers. We support every currency, and can reimburse to almost any country. You don't need to be an IT wizard – if you can type in their email address or SMS number, you can do basically everything with them.
+
+You get paid to use it
+: Do you spend money? Spend it on the Expensify Card and we pay you up to 2% cashback. It's your money after all.
+
+Revenue share for accountants
+: Do you manage the books for a bunch of clients? Become an Expensify Approved Accountant and take home 0.5% revenue share. Or share it with your clients as a discount, up to you!
+
+You are in the driver's seat; we're here to earn your business. But we're going to work harder for you than the other guys, and you won't be disappointed.
+
+## Concepts
+The Expensify Superapp has a lot of moving pieces, so let's break them down one by one.
+
+### What makes Expensify a superapp?
+A "superapp" is a single app that combines multiple products into one seamlessly interconnected experience. Expensify isn't a "suite" of separate products linked through a single account – Expensify is a single app with a single core design that can perform multiple product functions. The secret to making such a seamless experience is that we build all product functions atop the same common core:
+
+App
+: The basis of the superapp experience is the actual app itself, which runs on your mobile phone or desktop computer. (What is the Expensify app?)
+
+Chats
+: Even if you don't plan on using Expensify Chat for enterprise-grade workspace collaboration, chat is infused through the entire product. (What is a chat?)
+
+Expense
+: Even if you aren't actively managing your expenses, you've still got them. Every product that deals with money is ultimately dealing with expenses of some kind. (What is an expense?)
+
+Workspace
+: Though Expensify works great for our millions of individual members, every product really shines when used between groups of members sharing a "workspace". (What is a workspace?)
+
+Domain
+: To support more advanced security features, many products provide extra functionality to members who are on the same email "domain". (What is a domain?)
+
+These are the foundational concepts you'll see again and again that underpin the superapp as a whole.
+
+### What is the Expensify app?
+Just like your eyes are a window to your soul, the Expensify App is the doorway through which you experience the entire global world of interconnected chat-centric collaborative data that comprises the Expensify network. The main tools of this app consist of:
+
+Inbox
+: The main screen of the app is the Inbox, which highlights exactly what you should do next, consolidated across all products. (What does the Inbox do?)
+
+Search
+: The next major screen is Search, which as you'd expect, let's you search everything across all products, from one convenient and powerful place. (What does Search do?)
+
+Settings
+: Settings wraps up all your personal, workspace, and domain configuration options, all in one helpful space. (What are Expensify's settings?)
+
+Create
+: Finally, the big green plus button is the Create button, which lets you create pretty much anything, across all the products. (What does the Create button do?)
+
+It's a deceptively simple app, with a few very familiar looking screens and buttons that unlock an incredible range of sophisticated multi-product power.
+
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 5f6051c85745..35d71276f8ef 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.0.39
+ 9.0.41
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.39.4
+ 9.0.41.3
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index c29a15f26438..ca0c99bb87a2 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.39
+ 9.0.41
CFBundleSignature
????
CFBundleVersion
- 9.0.39.4
+ 9.0.41.3
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 2b43cf12b38a..2d97209598e2 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.0.39
+ 9.0.41
CFBundleVersion
- 9.0.39.4
+ 9.0.41.3
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.lock b/ios/Podfile.lock
index a801a7c4de1c..beac64acd083 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -2451,7 +2451,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - RNReactNativeHapticFeedback (2.3.1):
+ - RNReactNativeHapticFeedback (2.3.3):
- DoubleConversion
- glog
- hermes-engine
@@ -3233,7 +3233,7 @@ SPEC CHECKSUMS:
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4
RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28
- RNReactNativeHapticFeedback: 31833c3ef341d716dbbd9d64e940f0c230db46f6
+ RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5
RNReanimated: 76901886830e1032f16bbf820153f7dc3f02d51d
RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2
RNShare: bd4fe9b95d1ee89a200778cc0753ebe650154bb0
diff --git a/package-lock.json b/package-lock.json
index a9569fc0a6dd..b50632923b00 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.39-4",
+ "version": "9.0.41-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.39-4",
+ "version": "9.0.41-3",
"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.94",
"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.94",
+ "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.94.tgz",
+ "integrity": "sha512-Cco5X6u4IL5aQlFqa2IgGgR+vAffYLxpPN2d7bzfptW/pRLY2L2JRJohgvXEswlCcTKFVt4nIJ4bx9YIOvzxBA==",
"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 5a0d4f13b12c..249c64e5e621 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.39-4",
+ "version": "9.0.41-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -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.94",
"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-draggable-flatlist+4.0.1.patch b/patches/react-native-draggable-flatlist+4.0.1.patch
new file mode 100644
index 000000000000..348f1aa5de8a
--- /dev/null
+++ b/patches/react-native-draggable-flatlist+4.0.1.patch
@@ -0,0 +1,94 @@
+diff --git a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
+index d7d98c2..2f59c7a 100644
+--- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
++++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
+@@ -295,7 +295,7 @@ function DraggableFlatListInner(props: DraggableFlatListProps) {
+ const springTo = placeholderOffset.value - activeCellOffset.value;
+ touchTranslate.value = withSpring(
+ springTo,
+- animationConfigRef.current,
++ animationConfigRef.value,
+ () => {
+ runOnJS(onDragEnd)({
+ from: activeIndexAnim.value,
+diff --git a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx
+index ea21575..66c5eed 100644
+--- a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx
++++ b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx
+@@ -1,14 +1,14 @@
+ import React, { useContext } from "react";
+ import { useMemo, useRef } from "react";
+ import { FlatList } from "react-native-gesture-handler";
+-import Animated, { WithSpringConfig } from "react-native-reanimated";
++import Animated, { type SharedValue, useSharedValue, WithSpringConfig } from "react-native-reanimated";
+ import { DEFAULT_PROPS } from "../constants";
+ import { useProps } from "./propsContext";
+ import { CellData, DraggableFlatListProps } from "../types";
+
+ type RefContextValue = {
+ propsRef: React.MutableRefObject>;
+- animationConfigRef: React.MutableRefObject;
++ animationConfigRef: SharedValue;
+ cellDataRef: React.MutableRefObject>;
+ keyToIndexRef: React.MutableRefObject>;
+ containerRef: React.RefObject;
+@@ -54,8 +54,8 @@ function useSetupRefs({
+ ...DEFAULT_PROPS.animationConfig,
+ ...animationConfig,
+ } as WithSpringConfig;
+- const animationConfigRef = useRef(animConfig);
+- animationConfigRef.current = animConfig;
++ const animationConfigRef = useSharedValue(animConfig);
++ animationConfigRef.value = animConfig;
+
+ const cellDataRef = useRef(new Map());
+ const keyToIndexRef = useRef(new Map());
+diff --git a/node_modules/react-native-draggable-flatlist/src/hooks/useCellTranslate.tsx b/node_modules/react-native-draggable-flatlist/src/hooks/useCellTranslate.tsx
+index ce4ab68..efea240 100644
+--- a/node_modules/react-native-draggable-flatlist/src/hooks/useCellTranslate.tsx
++++ b/node_modules/react-native-draggable-flatlist/src/hooks/useCellTranslate.tsx
+@@ -101,7 +101,7 @@ export function useCellTranslate({ cellIndex, cellSize, cellOffset }: Params) {
+ ? activeCellSize.value * (isAfterActive ? -1 : 1)
+ : 0;
+
+- return withSpring(translationAmt, animationConfigRef.current);
++ return withSpring(translationAmt, animationConfigRef.value);
+ }, [activeKey, cellIndex]);
+
+ return translate;
+diff --git a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts
+index 7c20587..857c7d0 100644
+--- a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts
++++ b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts
+@@ -1,8 +1,9 @@
+-import { useRef } from "react";
+-import Animated, {
++
++import {
+ useDerivedValue,
+ withSpring,
+ WithSpringConfig,
++ useSharedValue,
+ } from "react-native-reanimated";
+ import { DEFAULT_ANIMATION_CONFIG } from "../constants";
+ import { useAnimatedValues } from "../context/animatedValueContext";
+@@ -15,8 +16,8 @@ type Params = {
+ export function useOnCellActiveAnimation(
+ { animationConfig }: Params = { animationConfig: {} }
+ ) {
+- const animationConfigRef = useRef(animationConfig);
+- animationConfigRef.current = animationConfig;
++ const animationConfigRef = useSharedValue(animationConfig);
++ animationConfigRef.value = animationConfig;
+
+ const isActive = useIsActive();
+
+@@ -26,7 +27,7 @@ export function useOnCellActiveAnimation(
+ const toVal = isActive && isTouchActiveNative.value ? 1 : 0;
+ return withSpring(toVal, {
+ ...DEFAULT_ANIMATION_CONFIG,
+- ...animationConfigRef.current,
++ ...animationConfigRef.value,
+ });
+ }, [isActive]);
+
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/App.tsx b/src/App.tsx
index 35254fa29b2a..177cc00c7dee 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -50,9 +50,6 @@ LogBox.ignoreLogs([
// the timer is lost. Currently Expensify is using a 30 minutes interval to refresh personal details.
// More details here: https://git.io/JJYeb
'Setting a timer for a long period of time',
- // We silence this warning for now and will address all the places where it happens separately.
- // Then we can remove this line so the problem does not occur in the future.
- '[Reanimated] Tried to modify key `current`',
]);
const fill = {flex: 1};
diff --git a/src/CONST.ts b/src/CONST.ts
index 07352562838e..5af0b34c0252 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 c0ec944b71e1..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',
@@ -59,11 +59,9 @@ const ROUTES = {
SEARCH_ADVANCED_FILTERS_IN: 'search/filters/in',
SEARCH_REPORT: {
route: 'search/view/:reportID/:reportActionID?',
- getRoute: (reportID: string, reportActionID?: string) => {
- if (reportActionID) {
- return `search/view/${reportID}/${reportActionID}` as const;
- }
- return `search/view/${reportID}` as const;
+ getRoute: ({reportID, reportActionID, backTo}: {reportID: string; reportActionID?: string; backTo?: string}) => {
+ const baseRoute = reportActionID ? (`search/view/${reportID}/${reportActionID}` as const) : (`search/view/${reportID}` as const);
+ return getUrlWithBackToParam(baseRoute, backTo);
},
},
TRANSACTION_HOLD_REASON_RHP: 'search/hold',
@@ -74,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: {
@@ -287,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',
@@ -300,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',
@@ -320,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',
@@ -406,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?',
@@ -495,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 = '') =>
@@ -536,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',
@@ -1097,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',
@@ -1126,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',
@@ -1552,6 +1577,12 @@ type Route = {
type RoutesValidationError = 'Error: One or more routes defined within `ROUTES` have not correctly used `as const` in their `getRoute` function return value.';
+/**
+ * Represents all routes in the app as a union of literal strings.
+ *
+ * If TS throws on this line, it implies that one or more routes defined within `ROUTES` have not correctly used
+ * `as const` in their `getRoute` function return value.
+ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type RouteIsPlainString = AssertTypesNotEqual;
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 538009b2565e..9b5d21743bef 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 (
&
Pick;
@@ -75,6 +78,7 @@ function AmountForm(
displayAsTextInput = false,
isCurrencyPressable = true,
label,
+ fixedDecimals,
...rest
}: AmountFormProps,
forwardedRef: ForwardedRef,
@@ -84,7 +88,7 @@ function AmountForm(
const textInput = useRef(null);
- const decimals = CurrencyUtils.getCurrencyDecimals(currency) + extraDecimals;
+ const decimals = fixedDecimals ?? CurrencyUtils.getCurrencyDecimals(currency) + extraDecimals;
const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]);
const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true);
diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index 04fe4fb650f5..2ee34d502521 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -79,10 +79,15 @@ function AvatarWithDisplayName({
actorAccountID.current = parentReportAction?.actorAccountID ?? -1;
}, [parentReportActions, report]);
+ const goToDetailsPage = useCallback(() => {
+ 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/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index db52c45751b7..80f52c8053da 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -3,17 +3,15 @@ import lodashIsEqual from 'lodash/isEqual';
import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'react';
import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import * as ValidationUtils from '@libs/ValidationUtils';
import Visibility from '@libs/Visibility';
import * as FormActions from '@userActions/FormActions';
import CONST from '@src/CONST';
-import type {OnyxFormKey} from '@src/ONYXKEYS';
+import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Form} from '@src/types/form';
-import type {Network} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {RegisterInput} from './FormContext';
import FormContext from './FormContext';
@@ -41,46 +39,34 @@ function getInitialValueByType(valueType?: ValueTypeKey): InitialDefaultValue {
}
}
-type FormProviderOnyxProps = {
- /** Contains the form state that must be accessed outside the component */
- formState: OnyxEntry
diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx
index 8cbbd1199b33..da572e4b1a79 100644
--- a/src/components/ProcessMoneyReportHoldMenu.tsx
+++ b/src/components/ProcessMoneyReportHoldMenu.tsx
@@ -75,7 +75,7 @@ function ProcessMoneyReportHoldMenu({
if (nonHeldAmount) {
return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount');
}
- return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount});
+ return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {count: transactionCount});
}, [nonHeldAmount, transactionCount, translate, isApprove]);
return (
diff --git a/src/components/PromotedActionsBar.tsx b/src/components/PromotedActionsBar.tsx
index c374259f0447..e6ce3080ee0a 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'}`),
@@ -97,7 +100,7 @@ const PromotedActions = {
return;
}
- ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.SEARCH_REPORT.getRoute(targetedReportID));
+ ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.SEARCH_REPORT.getRoute({reportID: targetedReportID}), currentSearchHash);
},
}),
} satisfies PromotedActionsType;
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 7158ebccbc09..35fe1379fbab 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 2108797ed3fd..9329558d6531 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;
@@ -284,24 +288,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 f4815cd4e228..0a480b013d04 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -332,7 +332,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(),
+ ),
+ )
}
/>
@@ -344,7 +352,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')}
@@ -359,7 +375,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(),
+ ),
+ )
+ }
/>
);
@@ -427,7 +453,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}
@@ -504,7 +539,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
iouType,
transaction?.transactionID ?? '-1',
report?.reportID ?? '-1',
- Navigation.getActiveRouteWithoutParams(),
+ Navigation.getReportRHPActiveRoute(),
),
)
}
@@ -522,7 +557,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')}
@@ -537,7 +581,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}
@@ -556,7 +608,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}
@@ -573,7 +633,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')}
@@ -588,7 +656,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')}
@@ -615,7 +691,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')}
@@ -632,7 +716,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/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 6202bdf972ea..87f06f43d82a 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -391,9 +391,9 @@ function ReportPreview({
}
return {
supportText: translate('iou.expenseCount', {
- count: numberOfRequests,
scanningReceipts: numberOfScanningReceipts,
pendingReceipts: numberOfPendingRequests,
+ count: numberOfRequests,
}),
};
}, [formattedMerchant, formattedDescription, moneyRequestComment, translate, numberOfRequests, numberOfScanningReceipts, numberOfPendingRequests]);
diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx
index 053ad0c2c63e..a2ea7487df02 100644
--- a/src/components/ReportActionItem/TaskPreview.tsx
+++ b/src/components/ReportActionItem/TaskPreview.tsx
@@ -15,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';
@@ -58,7 +59,9 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
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
// Other linked reportActions will only contain the taskReportID and we will grab the details from there
@@ -71,7 +74,7 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
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')}`} />;
}
@@ -117,6 +120,14 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
${htmlForTaskPreview}` : htmlForTaskPreview} />
+ {shouldShowGreenDotIndicator && (
+
+
+
+ )}
[
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..2580298ac3ac 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}
@@ -317,7 +317,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
shouldAlwaysShowDropdownMenu
pressOnEnter
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
- customText={translate('workspace.common.selected', {selectedNumber: selectedTransactionsKeys.length})}
+ customText={translate('workspace.common.selected', {count: selectedTransactionsKeys.length})}
options={headerButtonsOptions}
isSplitButton={false}
shouldUseStyleUtilityForAnchorPosition
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index d1080da19932..b415d91b7ab4 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -294,13 +294,15 @@ function Search({queryJSON}: SearchProps) {
SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID);
}
+ const backTo = Navigation.getActiveRoute();
+
if (SearchUtils.isReportActionListItemType(item)) {
const reportActionID = item.reportActionID;
- Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(reportID, reportActionID));
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo}));
return;
}
- Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(reportID));
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo}));
};
const fetchMoreResults = () => {
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index f5128dde973d..09cde759e696 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -84,7 +84,9 @@ function ReportListItem({
};
const openReportInRHP = (transactionItem: TransactionListItemType) => {
- Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(transactionItem.transactionThreadReportID));
+ const backTo = Navigation.getActiveRoute();
+
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: transactionItem.transactionThreadReportID, backTo}));
};
if (!reportItem?.reportName && reportItem.transactions.length > 1) {
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 14c83ef25ed4..b0d657b202c6 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -233,6 +233,7 @@ type ReportListItemType = ListItem &
/** The personal details of the user paying the request */
to: SearchPersonalDetails;
+ /** List of transactions that belong to this report */
transactions: TransactionListItemType[];
};
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,
@@ -50,8 +50,17 @@ function BaseGenericTooltip({
useLayoutEffect(() => {
// Calculate the tooltip width and height before the browser repaints the screen to prevent flicker
// because of the late update of the width and the height from onLayout.
+ const rootWrapperStyle = rootWrapper?.current?.style;
+ const isScaled = rootWrapperStyle?.transform === 'scale(0)';
+ if (isScaled) {
+ // Temporarily reset the scale caused by animation to get the untransformed size.
+ rootWrapperStyle.transform = 'scale(1)';
+ }
setContentMeasuredWidth(contentRef.current?.getBoundingClientRect().width);
setWrapperMeasuredHeight(rootWrapper.current?.getBoundingClientRect().height);
+ if (isScaled) {
+ rootWrapperStyle.transform = 'scale(0)';
+ }
}, []);
const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo(
@@ -119,7 +128,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.length > 0 ? intersection.at(intersection.length - 1) : undefined);
+ return;
+ }
const currentIndex = intersection.indexOf(currentScreenName);
const nextScreenIndex = currentIndex + 1;
+ const prevScreenIndex = currentIndex - 1;
setCurrentScreenIndex(currentIndex);
- const value = intersection.at(nextScreenIndex);
- if (value) {
- setNextScreen(value);
- }
+ setNextScreen(intersection.at(nextScreenIndex));
+ setPrevScreen(prevScreenIndex !== -1 ? intersection.at(prevScreenIndex) : undefined);
}, [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 899aa9a92f9f..ac28729fe63f 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1,46 +1,87 @@
-import {CONST as COMMON_CONST, Str} from 'expensify-common';
+import {CONST as COMMON_CONST} 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,
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,
- 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 +89,15 @@ import type {
ManagerApprovedParams,
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
+ MissingPropertyParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
OOOEventSummaryFullDayParams,
OOOEventSummaryPartialDayParams,
+ OptionalParam,
OurEmailProviderParams,
+ OwnerOwesAmountParams,
PaidElsewhereWithAmountParams,
PaidWithExpensifyWithAmountParams,
ParentNavigationSummaryParams,
@@ -63,21 +107,27 @@ 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,
SetTheDistanceMerchantParams,
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
@@ -86,20 +136,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 +185,11 @@ import type {
WelcomeNoteParams,
WelcomeToRoomParams,
WeSentYouMagicSignInLinkParams,
+ WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams,
+ YourPlanPriceParams,
ZipCodeExampleFormatParams,
-} from './types';
+} from './params';
+import type {TranslationDeepObject} from './types';
type StateValue = {
stateISO: string;
@@ -136,7 +201,7 @@ type States = Record;
type AllCountries = Record;
/* eslint-disable max-len */
-export default {
+const translations = {
common: {
cancel: 'Cancel',
dismiss: 'Dismiss',
@@ -264,7 +329,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 +708,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 +749,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 +794,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 +818,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 +840,10 @@ 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: () => ({
+ one: 'Issue found',
+ other: 'Issues found',
+ }),
fieldPending: 'Pending...',
defaultRate: 'Default rate',
receiptMissingDetails: 'Receipt missing details',
@@ -792,19 +860,27 @@ export default {
yourCompanyWebsiteNote: "If you don't have a website, you can provide your company's LinkedIn or social media profile instead.",
invalidDomainError: 'You have entered an invalid domain. To continue, please enter a valid domain.',
publicDomainError: 'You have entered a public domain. To continue, please enter a private domain.',
- expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => {
- const expenseText = `${count} ${Str.pluralize('expense', 'expenses', count)}`;
- const statusText = [];
+ expenseCount: ({scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => {
+ const statusText: string[] = [];
if (scanningReceipts > 0) {
statusText.push(`${scanningReceipts} scanning`);
}
if (pendingReceipts > 0) {
statusText.push(`${pendingReceipts} pending`);
}
- return statusText.length > 0 ? `${expenseText} (${statusText.join(', ')})` : expenseText;
- },
- deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Delete ${Str.pluralize('expense', 'expenses', count)}`,
- deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Are you sure that you want to delete ${Str.pluralize('this expense', 'these expenses', count)}?`,
+ return {
+ one: statusText.length > 0 ? `1 expense (${statusText.join(', ')})` : `1 expense`,
+ other: (count: number) => (statusText.length > 0 ? `${count} expenses (${statusText.join(', ')})` : `${count} expenses`),
+ };
+ },
+ deleteExpense: () => ({
+ one: 'Delete expense',
+ other: 'Delete expenses',
+ }),
+ deleteConfirmation: () => ({
+ one: 'Are you sure that you want to delete this expense?',
+ other: 'Are you sure that you want to delete these expenses?',
+ }),
settledExpensify: 'Paid',
settledElsewhere: 'Paid elsewhere',
individual: 'Individual',
@@ -819,15 +895,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}`,
@@ -903,12 +981,16 @@ export default {
keepAll: 'Keep all',
confirmApprove: 'Confirm approval amount',
confirmApprovalAmount: 'Approve only compliant expenses, or approve the entire report.',
- confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
- `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`,
+ confirmApprovalAllHoldAmount: () => ({
+ one: 'This expense is on hold. Do you want to approve anyway?',
+ other: 'These expenses are on hold. Do you want to approve anyway?',
+ }),
confirmPay: 'Confirm payment amount',
confirmPayAmount: "Pay what's not on hold, or pay the entire report.",
- confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
- `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`,
+ confirmPayAllHoldAmount: () => ({
+ one: 'This expense is on hold. Do you want to pay anyway?',
+ other: 'These expenses are on hold. Do you want to pay anyway?',
+ }),
payOnly: 'Pay only',
approveOnly: 'Approve only',
holdEducationalTitle: 'This expense is on',
@@ -926,7 +1008,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 +1392,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 +1602,7 @@ export default {
},
},
reportDetailsPage: {
- inWorkspace: ({policyName}) => `in ${policyName}`,
+ inWorkspace: ({policyName}: ReportPolicyNameParams) => `in ${policyName}`,
},
reportDescriptionPage: {
roomDescription: 'Room description',
@@ -1532,7 +1615,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 +1747,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 +1764,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 +2223,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 +2282,10 @@ export default {
testTransactions: 'Test transactions',
issueAndManageCards: 'Issue and manage cards',
reconcileCards: 'Reconcile cards',
- selected: ({selectedNumber}) => `${selectedNumber} selected`,
+ selected: () => ({
+ one: '1 selected',
+ other: (count: number) => `${count} selected`,
+ }),
settlementFrequency: 'Settlement frequency',
deleteConfirmation: 'Are you sure you want to delete this workspace?',
unavailable: 'Unavailable workspace',
@@ -2216,7 +2304,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 +2321,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 +2452,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 +2550,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 +2764,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 +2810,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 +2844,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 +2886,11 @@ 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: () => ({
+ one: '1 UDD added',
+ other: (count: number) => `${count} UDDs added`,
+ }),
+ mappingTitle: ({mappingName}: IntacctMappingTitleParams) => {
switch (mappingName) {
case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS:
return 'departments';
@@ -2833,7 +2924,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 +2971,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 +3017,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 +3130,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 +3164,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 +3264,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 +3285,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 +3308,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 +3351,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 +3364,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 +3412,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 +3430,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 +3471,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 +3619,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,9 +3679,18 @@ export default {
rate: 'Rate',
addRate: 'Add rate',
trackTax: 'Track tax',
- deleteRates: ({count}: DistanceRateOperationsParams) => `Delete ${Str.pluralize('rate', 'rates', count)}`,
- enableRates: ({count}: DistanceRateOperationsParams) => `Enable ${Str.pluralize('rate', 'rates', count)}`,
- disableRates: ({count}: DistanceRateOperationsParams) => `Disable ${Str.pluralize('rate', 'rates', count)}`,
+ deleteRates: () => ({
+ one: 'Delete rate',
+ other: 'Delete rates',
+ }),
+ enableRates: () => ({
+ one: 'Enable rate',
+ other: 'Enable rates',
+ }),
+ disableRates: () => ({
+ one: 'Disable rate',
+ other: 'Disable rates',
+ }),
enableRate: 'Enable rate',
status: 'Status',
unit: 'Unit',
@@ -3597,7 +3698,10 @@ export default {
changePromptMessage: ' to make that change.',
defaultCategory: 'Default category',
deleteDistanceRate: 'Delete distance rate',
- areYouSureDelete: ({count}: DistanceRateOperationsParams) => `Are you sure you want to delete ${Str.pluralize('this rate', 'these rates', count)}?`,
+ areYouSureDelete: () => ({
+ one: 'Are you sure you want to delete this rate?',
+ other: 'Are you sure you want to delete these rates?',
+ }),
},
editor: {
descriptionInputLabel: 'Description',
@@ -3656,19 +3760,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 +3786,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 +3849,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 +3857,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 +3879,10 @@ 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: () => ({
+ one: '1 day',
+ other: (count: number) => `${count} days`,
+ }),
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 +3919,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 +3948,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 +4011,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.at(0) ?? '';
@@ -3914,9 +4021,10 @@ export default {
} else if (submittersNames.length > 2) {
joinedNames = `${submittersNames.slice(0, submittersNames.length - 1).join(', ')} and ${submittersNames.at(submittersNames.length - 1)}`;
}
- const workflowWord = Str.pluralize('workflow', 'workflows', submittersNames.length);
- const chatWord = Str.pluralize('chat', 'chats', submittersNames.length);
- return `removed you from ${joinedNames}'s approval ${workflowWord} and workspace ${chatWord}. Previously submitted reports will remain available for approval in your Inbox.`;
+ return {
+ one: `removed you from ${joinedNames}'s approval workflow and workspace chat. Previously submitted reports will remain available for approval in your Inbox.`,
+ other: `removed you from ${joinedNames}'s approval workflows and workspace chats. Previously submitted reports will remain available for approval in your Inbox.`,
+ };
},
},
roomMembersPage: {
@@ -3958,7 +4066,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 +4116,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 +4127,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 +4249,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 +4266,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 +4490,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 +4501,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 +4550,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 +4570,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 +4625,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 +4638,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 +4646,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 +4675,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 +4684,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 +4696,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 +4717,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 +4729,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 +4761,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 +4781,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 +4836,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 +4847,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 +4877,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 +4894,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 07ea28533c15..fa44c9e39eff 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1,45 +1,85 @@
-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,
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,
- 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 +87,15 @@ import type {
ManagerApprovedParams,
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
+ MissingPropertyParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
OOOEventSummaryFullDayParams,
OOOEventSummaryPartialDayParams,
+ OptionalParam,
OurEmailProviderParams,
+ OwnerOwesAmountParams,
PaidElsewhereWithAmountParams,
PaidWithExpensifyWithAmountParams,
ParentNavigationSummaryParams,
@@ -62,21 +105,27 @@ 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,
SetTheDistanceMerchantParams,
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
@@ -85,19 +134,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 +184,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 +319,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 +701,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 +742,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 +787,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 +816,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 +830,10 @@ 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: () => ({
+ one: 'Problema encontrado',
+ other: 'Problemas encontrados',
+ }),
fieldPending: 'Pendiente...',
receiptScanning: 'Escaneando recibo...',
receiptScanInProgress: 'Escaneado de recibo en proceso',
@@ -785,19 +853,27 @@ export default {
yourCompanyWebsiteNote: 'Si no tiene un sitio web, puede proporcionar el perfil de LinkedIn o de las redes sociales de su empresa.',
invalidDomainError: 'Ha introducido un dominio no válido. Para continuar, introduzca un dominio válido.',
publicDomainError: 'Ha introducido un dominio pĂşblico. Para continuar, introduzca un dominio privado.',
- expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => {
- const expenseText = `${count} ${Str.pluralize('gasto', 'gastos', count)}`;
- const statusText = [];
+ expenseCount: ({scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => {
+ const statusText: string[] = [];
if (scanningReceipts > 0) {
statusText.push(`${scanningReceipts} escaneando`);
}
if (pendingReceipts > 0) {
statusText.push(`${pendingReceipts} pendiente`);
}
- return statusText.length > 0 ? `${expenseText} (${statusText.join(', ')})` : expenseText;
- },
- deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Eliminar ${Str.pluralize('gasto', 'gastos', count)}`,
- deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `¿Estás seguro de que quieres eliminar ${Str.pluralize('esta solicitud', 'estas solicitudes', count)}?`,
+ return {
+ one: statusText.length > 0 ? `1 gasto (${statusText.join(', ')})` : `1 gasto`,
+ other: (count: number) => (statusText.length > 0 ? `${count} gastos (${statusText.join(', ')})` : `${count} gastos`),
+ };
+ },
+ deleteExpense: () => ({
+ one: 'Eliminar gasto',
+ other: 'Eliminar gastos',
+ }),
+ deleteConfirmation: () => ({
+ one: '¿Estás seguro de que quieres eliminar esta solicitud?',
+ other: '¿Estás seguro de que quieres eliminar estas solicitudes?',
+ }),
settledExpensify: 'Pagado',
settledElsewhere: 'Pagado de otra forma',
individual: 'Individual',
@@ -812,6 +888,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 +898,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}`,
@@ -895,20 +973,16 @@ export default {
keepAll: 'Mantener todos',
confirmApprove: 'Confirmar importe a aprobar',
confirmApprovalAmount: 'Aprueba sĂłlo los gastos conformes, o aprueba todo el informe.',
- confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
- `${Str.pluralize('Este gasto está bloqueado', 'Estos gastos están bloqueados', transactionCount)}. ¿Quieres ${Str.pluralize(
- 'aprobar',
- 'aprobarlos',
- transactionCount,
- )} de todos modos?`,
+ confirmApprovalAllHoldAmount: () => ({
+ one: 'Este gasto está bloqueado. ¿Quieres aprobarlo de todos modos?',
+ other: 'Estos gastos están bloqueados. ¿Quieres aprobarlos de todos modos?',
+ }),
confirmPay: 'Confirmar importe de pago',
confirmPayAmount: 'Paga lo que no está bloqueado, o paga el informe completo.',
- confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
- `${Str.pluralize('Este gasto está bloqueado', 'Estos gastos están bloqueados', transactionCount)}. ¿Quieres ${Str.pluralize(
- 'pagar',
- 'pagarlo',
- transactionCount,
- )} de todos modos?`,
+ confirmPayAllHoldAmount: () => ({
+ one: 'Este gasto está bloqueado. ¿Quieres pagarlo de todos modos?',
+ other: 'Estos gastos están bloqueados. ¿Quieres pagarlos de todos modos?',
+ }),
payOnly: 'Solo pagar',
approveOnly: 'Solo aprobar',
hold: 'Bloquear',
@@ -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,10 @@ export default {
testTransactions: 'Transacciones de prueba',
issueAndManageCards: 'Emitir y gestionar tarjetas',
reconcileCards: 'Reconciliar tarjetas',
- selected: ({selectedNumber}) => `${selectedNumber} seleccionados`,
+ selected: () => ({
+ one: '1 seleccionado',
+ other: (count: number) => `${count} 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 +2324,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 +2341,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 +2479,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 +2581,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 +2799,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 +2845,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 +2879,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 +2921,11 @@ 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: () => ({
+ one: '1 UDD añadido',
+ other: (count: number) => `${count} UDDs añadido`,
+ }),
+ mappingTitle: ({mappingName}: IntacctMappingTitleParams) => {
switch (mappingName) {
case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS:
return 'departamentos';
@@ -2877,7 +2959,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 +3007,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 +3055,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 +3168,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 +3204,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 +3304,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 +3325,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 +3348,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 +3391,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 +3404,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 +3416,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 +3434,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 +3474,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 +3622,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,9 +3720,18 @@ export default {
rate: 'Tasa',
addRate: 'Agregar tasa',
trackTax: 'Impuesto de seguimiento',
- deleteRates: ({count}: DistanceRateOperationsParams) => `Eliminar ${Str.pluralize('tasa', 'tasas', count)}`,
- enableRates: ({count}: DistanceRateOperationsParams) => `Activar ${Str.pluralize('tasa', 'tasas', count)}`,
- disableRates: ({count}: DistanceRateOperationsParams) => `Desactivar ${Str.pluralize('tasa', 'tasas', count)}`,
+ deleteRates: () => ({
+ one: 'Eliminar tasa',
+ other: 'Eliminar tasas',
+ }),
+ enableRates: () => ({
+ one: 'Activar tasa',
+ other: 'Activar tasas',
+ }),
+ disableRates: () => ({
+ one: 'Desactivar tasa',
+ other: 'Desactivar tasas',
+ }),
enableRate: 'Activar tasa',
status: 'Estado',
unit: 'Unidad',
@@ -3645,7 +3739,10 @@ export default {
changePromptMessage: ' para hacer ese cambio.',
defaultCategory: 'CategorĂa predeterminada',
deleteDistanceRate: 'Eliminar tasa de distancia',
- areYouSureDelete: ({count}: DistanceRateOperationsParams) => `¿Estás seguro de que quieres eliminar ${Str.pluralize('esta tasa', 'estas tasas', count)}?`,
+ areYouSureDelete: () => ({
+ one: '¿Estás seguro de que quieres eliminar esta tasa?',
+ other: '¿Estás seguro de que quieres eliminar estas tasas?',
+ }),
},
editor: {
nameInputLabel: 'Nombre',
@@ -3705,19 +3802,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 +3829,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 +3892,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 +3900,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 +3922,10 @@ 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: () => ({
+ one: '1 dĂa',
+ other: (count: number) => `${count} dĂas`,
+ }),
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 +3962,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 +3991,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 +4055,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.at(0) ?? '';
@@ -3965,9 +4065,10 @@ export default {
} else if (submittersNames.length > 2) {
joinedNames = `${submittersNames.slice(0, submittersNames.length - 1).join(', ')} y ${submittersNames.at(submittersNames.length - 1)}`;
}
- const workflowWord = Str.pluralize('del flujo', 'de los flujos', submittersNames.length);
- const chatWord = Str.pluralize('del chat', 'de los chats', submittersNames.length);
- return `te eliminó ${workflowWord} de trabajo de aprobaciones y ${chatWord} del espacio de trabajo de ${joinedNames}. Los informes enviados anteriormente seguirán estando disponibles para su aprobación en tu bandeja de entrada.`;
+ return {
+ one: `te eliminó del flujo de trabajo de aprobaciones y del chat del espacio de trabajo de ${joinedNames}. Los informes enviados anteriormente seguirán estando disponibles para su aprobación en tu bandeja de entrada.`,
+ other: `te eliminó de los flujos de trabajo de aprobaciones y de los chats del espacio de trabajo de ${joinedNames}. Los informes enviados anteriormente seguirán estando disponibles para su aprobación en tu bandeja de entrada.`,
+ };
},
},
roomMembersPage: {
@@ -4009,7 +4110,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 +4160,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 +4171,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 +4294,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 +4311,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 +4996,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 +5007,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 +5058,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 +5078,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 +5133,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 +5146,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 +5154,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 +5186,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 +5195,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 +5207,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 +5228,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 +5240,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 +5272,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 +5292,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 +5347,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 +5358,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 +5388,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 +5405,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..d51bb2d20e03
--- /dev/null
+++ b/src/languages/params.ts
@@ -0,0 +1,733 @@
+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 = {
+ 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 ReimbursementRateParams = {unit: Unit};
+
+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 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 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 IntacctMappingTitleParams = {mappingName: SageIntacctMappingName};
+
+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,
+ RequiredFieldParams,
+ 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,
+ AmountWithCurrencyParams,
+ LowerUpperParams,
+ LogSizeAndDateParams,
+ AddressLineParams,
+ AdminCanceledRequestParams,
+ AlreadySignedInParams,
+ ApprovedAmountParams,
+ BeginningOfChatHistoryAdminRoomPartOneParams,
+ BeginningOfChatHistoryAnnounceRoomPartOneParams,
+ BeginningOfChatHistoryAnnounceRoomPartTwo,
+ BeginningOfChatHistoryDomainRoomPartOneParams,
+ CanceledRequestParams,
+ CharacterLimitParams,
+ ConfirmThatParams,
+ CompanyCardFeedNameParams,
+ DateShouldBeAfterParams,
+ DateShouldBeBeforeParams,
+ DeleteActionParams,
+ DeleteConfirmationParams,
+ DidSplitAmountMessageParams,
+ 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,
+ 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/AddMembersToWorkspaceParams.ts b/src/libs/API/parameters/AddMembersToWorkspaceParams.ts
index 4e96fd07d301..abfed55e2df3 100644
--- a/src/libs/API/parameters/AddMembersToWorkspaceParams.ts
+++ b/src/libs/API/parameters/AddMembersToWorkspaceParams.ts
@@ -3,6 +3,8 @@ type AddMembersToWorkspaceParams = {
welcomeNote: string;
policyID: string;
reportCreationData?: string;
+ announceChatReportID?: string;
+ announceCreatedReportActionID?: string;
};
export default AddMembersToWorkspaceParams;
diff --git a/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts b/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts
index 761a6c2f5008..a1256f5ad051 100644
--- a/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts
+++ b/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts
@@ -1,13 +1,11 @@
type CreateWorkspaceFromIOUPaymentParams = {
policyID: string;
- announceChatReportID: string;
adminsChatReportID: string;
expenseChatReportID: string;
ownerEmail: string;
makeMeAdmin: boolean;
policyName: string;
type: string;
- announceCreatedReportActionID: string;
adminsCreatedReportActionID: string;
expenseCreatedReportActionID: string;
customUnitID: string;
diff --git a/src/libs/API/parameters/CreateWorkspaceParams.ts b/src/libs/API/parameters/CreateWorkspaceParams.ts
index c86598b48953..18ef4a0e763f 100644
--- a/src/libs/API/parameters/CreateWorkspaceParams.ts
+++ b/src/libs/API/parameters/CreateWorkspaceParams.ts
@@ -1,13 +1,11 @@
type CreateWorkspaceParams = {
policyID: string;
- announceChatReportID: string;
adminsChatReportID: string;
expenseChatReportID: string;
ownerEmail: string;
makeMeAdmin: boolean;
policyName: string;
type: string;
- announceCreatedReportActionID: string;
adminsCreatedReportActionID: string;
expenseCreatedReportActionID: string;
customUnitID: string;
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/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts
index 09328763bb35..f9ac681cb468 100644
--- a/src/libs/CurrencyUtils.ts
+++ b/src/libs/CurrencyUtils.ts
@@ -164,7 +164,7 @@ function convertAmountToDisplayString(amount = 0, currency: string = CONST.CURRE
return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
style: 'currency',
currency,
- minimumFractionDigits: getCurrencyDecimals(currency) + 1,
+ minimumFractionDigits: CONST.MAX_TAX_RATE_DECIMAL_PLACES,
});
}
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 e5b8c38c30a6..ba2a33a367d4 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/IntlPolyfill/index.android.ts b/src/libs/IntlPolyfill/index.android.ts
index 37647b9d2939..e6ab02d15c25 100644
--- a/src/libs/IntlPolyfill/index.android.ts
+++ b/src/libs/IntlPolyfill/index.android.ts
@@ -11,6 +11,10 @@ const intlPolyfill: IntlPolyfill = () => {
require('@formatjs/intl-locale/polyfill-force');
+ require('@formatjs/intl-pluralrules/polyfill-force');
+ require('@formatjs/intl-pluralrules/locale-data/en');
+ require('@formatjs/intl-pluralrules/locale-data/es');
+
polyfillListFormat();
};
diff --git a/src/libs/IntlPolyfill/index.ios.ts b/src/libs/IntlPolyfill/index.ios.ts
index 4701737c2b1c..ecde57ddd21e 100644
--- a/src/libs/IntlPolyfill/index.ios.ts
+++ b/src/libs/IntlPolyfill/index.ios.ts
@@ -13,9 +13,12 @@ const intlPolyfill: IntlPolyfill = () => {
require('@formatjs/intl-locale/polyfill-force');
+ require('@formatjs/intl-pluralrules/polyfill-force');
+ require('@formatjs/intl-pluralrules/locale-data/en');
+ require('@formatjs/intl-pluralrules/locale-data/es');
+
// Required to polyfill NumberFormat on iOS
// see: https://github.com/facebook/hermes/issues/1172#issuecomment-1776156538
- require('@formatjs/intl-pluralrules/polyfill-force');
polyfillNumberFormat();
// Required to polyfill DateTimeFormat on iOS
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/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index da77fc7be98a..c84213918f70 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -114,15 +114,8 @@ function getMatchingRootRouteForRHPRoute(route: NavigationPartialRoute): Navigat
if (route.params && 'backTo' in route.params && typeof route.params.backTo === 'string') {
const stateForBackTo = getStateFromPath(route.params.backTo, config);
if (stateForBackTo) {
- // eslint-disable-next-line @typescript-eslint/no-shadow
- const rhpNavigator = stateForBackTo.routes.find((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
-
- const centralPaneOrFullScreenNavigator = stateForBackTo.routes.find(
- // eslint-disable-next-line @typescript-eslint/no-shadow
- (route) => isCentralPaneName(route.name) || route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR,
- );
-
// If there is rhpNavigator in the state generated for backTo url, we want to get root route matching to this rhp screen.
+ const rhpNavigator = stateForBackTo.routes.find((rt) => rt.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
if (rhpNavigator && rhpNavigator.state) {
const isRHPinState = stateForBackTo.routes.at(0)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
@@ -131,9 +124,16 @@ function getMatchingRootRouteForRHPRoute(route: NavigationPartialRoute): Navigat
}
}
- // If we know that backTo targets the root route (central pane or full screen) we want to use it.
- if (centralPaneOrFullScreenNavigator && centralPaneOrFullScreenNavigator.state) {
- return centralPaneOrFullScreenNavigator as NavigationPartialRoute;
+ // If we know that backTo targets the root route (full screen) we want to use it.
+ const fullScreenNavigator = stateForBackTo.routes.find((rt) => rt.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR);
+ if (fullScreenNavigator && fullScreenNavigator.state) {
+ return fullScreenNavigator as NavigationPartialRoute;
+ }
+
+ // If we know that backTo targets a central pane screen we want to use it.
+ const centralPaneScreen = stateForBackTo.routes.find((rt) => isCentralPaneName(rt.name));
+ if (centralPaneScreen) {
+ return centralPaneScreen as NavigationPartialRoute;
}
}
}
@@ -195,7 +195,7 @@ function getAdaptedState(state: PartialState
if (focusedRHPRoute) {
let matchingRootRoute = getMatchingRootRouteForRHPRoute(focusedRHPRoute);
const isRHPScreenOpenedFromLHN = focusedRHPRoute?.name && RHP_SCREENS_OPENED_FROM_LHN.includes(focusedRHPRoute?.name as RHPScreenOpenedFromLHN);
- // This may happen if this RHP doens't have a route that should be under the overlay defined.
+ // This may happen if this RHP doesn't have a route that should be under the overlay defined.
if (!matchingRootRoute || isRHPScreenOpenedFromLHN) {
metainfo.isCentralPaneAndBottomTabMandatory = false;
metainfo.isFullScreenNavigatorMandatory = false;
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 aa9d8c59fb5b..fe90aa87495e 100644
--- a/src/libs/Network/NetworkStore.ts
+++ b/src/libs/Network/NetworkStore.ts
@@ -100,9 +100,9 @@ function getAuthToken(): string | null | undefined {
function isSupportRequest(command: string): boolean {
return [
WRITE_COMMANDS.OPEN_APP,
+ WRITE_COMMANDS.SEARCH,
SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP,
SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT,
- READ_COMMANDS.SEARCH,
READ_COMMANDS.OPEN_CARD_DETAILS_PAGE,
READ_COMMANDS.OPEN_POLICY_CATEGORIES_PAGE,
READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_PAGE,
diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts
index 23045c8a7215..a000c604d5c8 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.at(0) : approvalChain.at(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;
};
/**
@@ -672,7 +673,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);
@@ -1586,7 +1587,11 @@ function createOptionFromReport(report: Report, personalDetails: OnyxEntry option?.lastIOUCreationDate ?? '' : '',
+ preferRecentExpenseReports ? (option) => option?.isPolicyExpenseChat : 0,
],
- ['asc'],
+ ['asc', 'desc', 'desc'],
);
}
@@ -1936,6 +1950,8 @@ function getOptions(
let recentReportOptions = [];
let personalDetailsOptions: ReportUtils.OptionData[] = [];
+ const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE;
+
if (includeRecentReports) {
for (const reportOption of allReportOptions) {
/**
@@ -1958,9 +1974,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:
@@ -1994,6 +2012,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});
@@ -2042,7 +2076,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 {
@@ -2406,6 +2444,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)};
@@ -2487,7 +2526,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/PolicyDistanceRatesUtils.ts b/src/libs/PolicyDistanceRatesUtils.ts
index 0c5493f2f97b..8e4d68f78b4c 100644
--- a/src/libs/PolicyDistanceRatesUtils.ts
+++ b/src/libs/PolicyDistanceRatesUtils.ts
@@ -2,7 +2,6 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import CONST from '@src/CONST';
import type ONYXKEYS from '@src/ONYXKEYS';
import type {Rate} from '@src/types/onyx/Policy';
-import * as CurrencyUtils from './CurrencyUtils';
import getPermittedDecimalSeparator from './getPermittedDecimalSeparator';
import * as Localize from './Localize';
import * as MoneyRequestUtils from './MoneyRequestUtils';
@@ -18,7 +17,7 @@ function validateRateValue(values: FormOnyxValues, currency: stri
const decimalSeparator = toLocaleDigit('.');
// Allow one more decimal place for accuracy
- const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{0,${CurrencyUtils.getCurrencyDecimals(currency) + 1}})?$`, 'i');
+ const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{0,${CONST.MAX_TAX_RATE_DECIMAL_PLACES}})?$`, 'i');
if (!rateValueRegex.test(parsedRate) || parsedRate === '') {
errors.rate = Localize.translateLocal('common.error.invalidRateError');
} else if (NumberUtils.parseFloatAnyLocale(parsedRate) <= 0) {
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 8c0280e203e5..9664c6cca68c 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -221,7 +221,7 @@ const isPolicyOwner = (policy: OnyxInputOrEntry, currentUserAccountID: n
*
* If includeMemberWithErrors is false, We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error.
*/
-function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | undefined, includeMemberWithErrors = false): MemberEmailsToAccountIDs {
+function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | undefined, includeMemberWithErrors = false, includeMemberWithPendingDelete = true): MemberEmailsToAccountIDs {
const members = employeeList ?? {};
const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {};
Object.keys(members).forEach((email) => {
@@ -231,6 +231,12 @@ function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | unde
return;
}
}
+ if (!includeMemberWithPendingDelete) {
+ const member = members?.[email];
+ if (member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return;
+ }
+ }
const personalDetail = getPersonalDetailByEmail(email);
if (!personalDetail?.login) {
return;
@@ -590,9 +596,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) {
@@ -825,7 +829,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 1a0daa7fbd03..86ce8ed3472e 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;
@@ -757,6 +760,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> = {},
@@ -781,7 +796,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,
@@ -998,20 +1013,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 = [];
@@ -1039,6 +1054,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) {
@@ -1058,6 +1082,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
@@ -1270,7 +1315,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');
@@ -1647,7 +1692,7 @@ function getPolicyChangeLogAddEmployeeMessage(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
@@ -1662,7 +1707,7 @@ function getPolicyChangeLogChangeRoleMessage(reportAction: OnyxInputOrEntry>) {
@@ -1694,7 +1739,7 @@ function getRemovedFromApprovalChainMessage(reportAction: OnyxEntry displayName ?? login ?? 'Unknown Submitter',
);
- return Localize.translateLocal('workspaceActions.removedFromApprovalWorkflow', {submittersNames});
+ return Localize.translateLocal('workspaceActions.removedFromApprovalWorkflow', {submittersNames, count: submittersNames.length});
}
function isCardIssuedAction(reportAction: OnyxEntry) {
@@ -1702,7 +1747,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).at(0);
const assignee = shouldRenderHTML ? ` ` : assigneeDetails?.firstName ?? assigneeDetails?.login ?? '';
@@ -1723,11 +1768,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 '';
}
@@ -1736,6 +1781,7 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende
export {
doesReportHaveVisibleActions,
extractLinksFromMessageHtml,
+ formatLastMessageText,
getActionableMentionWhisperMessage,
getAllReportActions,
getCombinedReportActions,
@@ -1758,6 +1804,7 @@ export {
getNumberOfMoneyRequests,
getOneTransactionThreadReportID,
getOriginalMessage,
+ // eslint-disable-next-line deprecation/deprecation
getParentReportAction,
getRemovedFromApprovalChainMessage,
getReportAction,
@@ -1838,6 +1885,7 @@ export {
getRenamedAction,
isCardIssuedAction,
getCardIssuedMessage,
+ hasSameActorForAllTransactions,
};
export type {LastVisibleMessage};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index eaf128974d7a..8cd635a3082d 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,13 +348,22 @@ type OptimisticTaskReportAction = Pick<
| 'previousMessage'
| 'errors'
| 'linkMetadata'
+ | 'delegateAccountID'
>;
-type OptimisticWorkspaceChats = {
+type AnnounceRoomOnyxData = {
+ onyxOptimisticData: OnyxUpdate[];
+ onyxSuccessData: OnyxUpdate[];
+ onyxFailureData: OnyxUpdate[];
+};
+
+type OptimisticAnnounceChat = {
announceChatReportID: string;
- announceChatData: OptimisticChatReport;
- announceReportActionData: Record;
- announceCreatedReportActionID: string;
+ announceChatReportActionID: string;
+ announceChatData: AnnounceRoomOnyxData;
+};
+
+type OptimisticWorkspaceChats = {
adminsChatReportID: string;
adminsChatData: OptimisticChatReport;
adminsReportActionData: Record;
@@ -340,7 +376,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 +507,7 @@ type OptionData = {
tabIndex?: 0 | -1;
isConciergeChat?: boolean;
isBold?: boolean;
+ lastIOUCreationDate?: string;
} & Report;
type OnyxDataTaskAssigneeChat = {
@@ -636,6 +685,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 +1637,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 +1895,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);
}
/**
@@ -1939,7 +2005,7 @@ function getWorkspaceIcon(report: OnyxInputOrEntry, policy?: OnyxInputOr
const iconFromCache = workSpaceIconsCache.get(cacheKey);
// disabling to protect against empty strings
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const policyAvatarURL = report?.policyAvatar || allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatarURL;
+ const policyAvatarURL = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatarURL || report?.policyAvatar;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const policyExpenseChatAvatarSource = policyAvatarURL || getDefaultWorkspaceAvatar(workspaceName);
@@ -2143,7 +2209,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 +2283,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 +2378,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 +3193,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 +3210,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 +3781,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 +3830,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 +3995,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.at(0)));
+ Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountID.at(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.at(0)));
+ Navigation.goBack(ROUTES.PROFILE.getRoute(participantAccountID.at(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 +4045,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 +4054,16 @@ function goBackFromPrivateNotes(report: OnyxEntry, session: OnyxEntry) {
+ return Localize.translateLocal('iou.automaticallySubmittedAmount', {formattedAmount: getFormattedAmount(reportAction)});
+}
+
+function getIOUSubmittedMessage(reportAction: ReportAction) {
return Localize.translateLocal('iou.submittedAmount', {formattedAmount: getFormattedAmount(reportAction)});
}
@@ -4593,6 +4674,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) {
@@ -4644,6 +4727,7 @@ function buildOptimisticIOUReportAction(
shouldShow: true,
created,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4656,6 +4740,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e
currency,
expenseReportID,
};
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
return {
actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED,
@@ -4676,6 +4761,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e
shouldShow: true,
created: DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4683,6 +4769,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,
@@ -4706,6 +4793,7 @@ function buildOptimisticUnapprovedReportAction(amount: number, currency: string,
shouldShow: true,
created: DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4762,6 +4850,8 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
expenseReportID,
};
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+
return {
actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
actorAccountID: currentUserAccountID,
@@ -4782,6 +4872,7 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
shouldShow: true,
created: DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4822,9 +4913,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,
@@ -4882,6 +4973,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,
@@ -4909,6 +5002,7 @@ function buildOptimisticModifiedExpenseReportAction(
reportActionID: NumberUtils.rand64(),
reportID: transactionThread?.reportID,
shouldShow: true,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -4918,6 +5012,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,
@@ -4947,6 +5043,7 @@ function buildOptimisticMovedTrackedExpenseModifiedReportAction(transactionThrea
reportActionID: NumberUtils.rand64(),
reportID: transactionThreadID,
shouldShow: true,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -5022,6 +5119,8 @@ function buildOptimisticTaskReportAction(
html: message,
whisperedTo: [],
};
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+
return {
actionName,
actorAccountID,
@@ -5048,6 +5147,7 @@ function buildOptimisticTaskReportAction(
created: DateUtils.getDBTimeWithSkew(Date.now() + createdOffset),
isFirstItem: false,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -5377,6 +5477,7 @@ function buildOptimisticEditedTaskFieldReportAction({title, description}: Task):
} else if (field) {
changelog = `removed the ${field}`;
}
+ const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
return {
reportActionID: NumberUtils.rand64(),
@@ -5401,10 +5502,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,
@@ -5428,6 +5532,7 @@ function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: numbe
avatar: getCurrentUserAvatar(),
created: DateUtils.getDBTime(),
shouldShow: false,
+ delegateAccountID: delegateAccountDetails?.accountID,
};
}
@@ -5509,24 +5614,113 @@ function buildOptimisticDismissedViolationReportAction(
};
}
-function buildOptimisticWorkspaceChats(policyID: string, policyName: string, expenseReportId?: string): OptimisticWorkspaceChats {
+function buildOptimisticAnnounceChat(policyID: string, accountIDs: number[]): OptimisticAnnounceChat {
+ const announceReport = getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID);
+ const policy = getPolicy(policyID);
+ const announceRoomOnyxData: AnnounceRoomOnyxData = {
+ onyxOptimisticData: [],
+ onyxSuccessData: [],
+ onyxFailureData: [],
+ };
+
+ // Do not create #announce room if the room already exists or if there are less than 3 participants in workspace
+ if (accountIDs.length < 3 || announceReport) {
+ return {
+ announceChatReportID: '',
+ announceChatReportActionID: '',
+ announceChatData: announceRoomOnyxData,
+ };
+ }
+
const announceChatData = buildOptimisticChatReport(
- currentUserAccountID ? [currentUserAccountID] : [],
+ accountIDs,
CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE,
CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE,
policyID,
CONST.POLICY.OWNER_ACCOUNT_ID_FAKE,
false,
- policyName,
+ policy?.name,
undefined,
CONST.REPORT.WRITE_CAPABILITIES.ADMINS,
CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
);
- const announceChatReportID = announceChatData.reportID;
const announceCreatedAction = buildOptimisticCreatedReportAction(CONST.POLICY.OWNER_EMAIL_FAKE);
- const announceReportActionData = {
- [announceCreatedAction.reportActionID]: announceCreatedAction,
+ announceRoomOnyxData.onyxOptimisticData.push(
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatData.reportID}`,
+ value: {
+ pendingFields: {
+ addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ ...announceChatData,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${announceChatData.reportID}`,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatData.reportID}`,
+ value: {
+ [announceCreatedAction.reportActionID]: announceCreatedAction,
+ },
+ },
+ );
+ announceRoomOnyxData.onyxSuccessData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatData.reportID}`,
+ value: {
+ pendingFields: {
+ addWorkspaceRoom: null,
+ },
+ pendingAction: null,
+ isOptimisticReport: false,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatData.reportID}`,
+ value: {
+ [announceCreatedAction.reportActionID]: {
+ pendingAction: null,
+ },
+ },
+ },
+ );
+ announceRoomOnyxData.onyxFailureData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatData.reportID}`,
+ value: {
+ pendingFields: {
+ addWorkspaceRoom: null,
+ },
+ pendingAction: null,
+ isOptimisticReport: false,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatData.reportID}`,
+ value: {
+ [announceCreatedAction.reportActionID]: {
+ pendingAction: null,
+ },
+ },
+ },
+ );
+ return {
+ announceChatReportID: announceChatData.reportID,
+ announceChatReportActionID: announceCreatedAction.reportActionID,
+ announceChatData: announceRoomOnyxData,
};
+}
+
+function buildOptimisticWorkspaceChats(policyID: string, policyName: string, expenseReportId?: string): OptimisticWorkspaceChats {
const pendingChatMembers = getPendingChatMembers(currentUserAccountID ? [currentUserAccountID] : [], [], CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
const adminsChatData = {
...buildOptimisticChatReport(
@@ -5571,10 +5765,6 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string, exp
};
return {
- announceChatReportID,
- announceChatData,
- announceReportActionData,
- announceCreatedReportActionID: announceCreatedAction.reportActionID,
adminsChatReportID,
adminsChatData,
adminsReportActionData,
@@ -6442,9 +6632,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry): boolean {
return true;
}
- if (isExpenseReport(report) && isOneTransactionReport(report?.reportID ?? '-1')) {
+ if (isExpenseReport(report)) {
return true;
}
@@ -6659,7 +6847,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean {
return true;
}
- if (isInvoiceRoom(report)) {
+ if (isInvoiceRoom(report) || isInvoiceReport(report)) {
return true;
}
@@ -7141,16 +7329,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));
}
/**
@@ -7541,10 +7729,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;
@@ -7699,7 +7887,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 '';
}
@@ -7813,8 +8001,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 {
@@ -7850,6 +8038,7 @@ export {
buildOptimisticTaskReport,
buildOptimisticTaskReportAction,
buildOptimisticUnHoldReportAction,
+ buildOptimisticAnnounceChat,
buildOptimisticWorkspaceChats,
buildParticipantsFromAccountIDs,
buildTransactionThread,
@@ -7915,6 +8104,7 @@ export {
getIOUForwardedMessage,
getRejectedReportMessage,
getWorkspaceNameUpdatedMessage,
+ getReportAutomaticallySubmittedMessage,
getIOUSubmittedMessage,
getIcons,
getIconsForParticipants,
@@ -8012,7 +8202,7 @@ export {
isEmptyReport,
isRootGroupChat,
isExpenseReport,
- isExpenseReportManagerWithoutParentAccess,
+ isExpenseReportWithoutParentAccess,
isExpenseRequest,
isExpensifyOnlyParticipantInReport,
isGroupChat,
@@ -8125,6 +8315,7 @@ export {
isIndividualInvoiceRoom,
isAuditor,
hasMissingInvoiceBankAccount,
+ isSingleActorMoneyReport,
};
export type {
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index 252fbe9d230f..8a406f374032 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -311,21 +311,30 @@ function getListItem(type: SearchDataTypes, status: SearchStatus): ListItemType<
if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
return ChatListItem;
}
- return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? TransactionListItem : ReportListItem;
+ if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
+ return TransactionListItem;
+ }
+ return ReportListItem;
}
function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) {
if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
return getReportActionsSections(data);
}
- return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? getTransactionsSections(data, metadata) : getReportSections(data, metadata);
+ if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
+ return getTransactionsSections(data, metadata);
+ }
+ return getReportSections(data, metadata);
}
function getSortedSections(type: SearchDataTypes, status: SearchStatus, data: ListItemDataType, sortBy?: SearchColumnType, sortOrder?: SortOrder) {
if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
return getSortedReportActionData(data as ReportActionListItemType[]);
}
- return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder) : getSortedReportData(data as ReportListItemType[]);
+ if (status === CONST.SEARCH.STATUS.EXPENSE.ALL) {
+ return getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder);
+ }
+ return getSortedReportData(data as ReportListItemType[]);
}
function getSortedTransactionData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) {
@@ -784,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 2f8b4f7b1cbc..9745730a51c9 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -133,10 +133,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 d5c93d382637..6eafec9f9528 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 4bf3035bb03c..617db42f184c 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,
@@ -3395,8 +3398,6 @@ function categorizeTrackedExpense(
receipt,
policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID,
policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID,
- announceChatReportID: createdWorkspaceParams?.announceChatReportID,
- announceCreatedReportActionID: createdWorkspaceParams?.announceCreatedReportActionID,
adminsChatReportID: createdWorkspaceParams?.adminsChatReportID,
adminsCreatedReportActionID: createdWorkspaceParams?.adminsCreatedReportActionID,
};
@@ -3472,8 +3473,6 @@ function shareTrackedExpense(
receipt,
policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID,
policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID,
- announceChatReportID: createdWorkspaceParams?.announceChatReportID,
- announceCreatedReportActionID: createdWorkspaceParams?.announceCreatedReportActionID,
adminsChatReportID: createdWorkspaceParams?.adminsChatReportID,
adminsCreatedReportActionID: createdWorkspaceParams?.adminsCreatedReportActionID,
};
@@ -6643,23 +6642,10 @@ function getPayMoneyRequestParams(
successData: policySuccessData,
params,
} = Policy.buildPolicyData(currentUserEmail, true, undefined, payerPolicyID);
- const {
- announceChatReportID,
- announceCreatedReportActionID,
- adminsChatReportID,
- adminsCreatedReportActionID,
- expenseChatReportID,
- expenseCreatedReportActionID,
- customUnitRateID,
- customUnitID,
- ownerEmail,
- policyName,
- } = params;
+ const {adminsChatReportID, adminsCreatedReportActionID, expenseChatReportID, expenseCreatedReportActionID, customUnitRateID, customUnitID, ownerEmail, policyName} = params;
policyParams = {
policyID: payerPolicyID,
- announceChatReportID,
- announceCreatedReportActionID,
adminsChatReportID,
adminsCreatedReportActionID,
expenseChatReportID,
@@ -7016,6 +7002,24 @@ function isLastApprover(approvalChain: string[]): boolean {
return approvalChain.at(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));
@@ -7034,6 +7038,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);
@@ -7057,6 +7062,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,
},
@@ -7561,8 +7567,6 @@ function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.
params: {
reportActionID,
policyID,
- announceChatReportID,
- announceCreatedReportActionID,
adminsChatReportID,
adminsCreatedReportActionID,
expenseChatReportID,
@@ -7588,8 +7592,6 @@ function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.
params = {
...params,
policyID,
- announceChatReportID,
- announceCreatedReportActionID,
adminsChatReportID,
adminsCreatedReportActionID,
expenseChatReportID,
@@ -7837,7 +7839,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',
{
@@ -7909,7 +7939,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}`];
@@ -7967,6 +7997,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',
{
@@ -8014,6 +8072,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}`];
@@ -8098,18 +8171,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 = {
@@ -8170,8 +8232,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,
@@ -8240,5 +8422,7 @@ export {
updateMoneyRequestTaxAmount,
updateMoneyRequestTaxRate,
mergeDuplicates,
+ resolveDuplicates,
+ prepareToCleanUpMoneyRequest,
};
export type {GPSPoint as GpsPoint, IOURequestType};
diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts
index 01ac832336ab..00853e9546d5 100644
--- a/src/libs/actions/Modal.ts
+++ b/src/libs/actions/Modal.ts
@@ -32,9 +32,11 @@ function closeTop() {
}
if (onModalClose) {
closeModals[closeModals.length - 1](isNavigate);
+ closeModals.pop();
return;
}
closeModals[closeModals.length - 1]();
+ closeModals.pop();
}
/**
diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts
index 672f325be58a..1ba50d08e449 100644
--- a/src/libs/actions/OnyxUpdates.ts
+++ b/src/libs/actions/OnyxUpdates.ts
@@ -42,6 +42,14 @@ function applyHTTPSOnyxUpdates(request: Request, response: Response) {
return updateHandler(request.successData);
}
if (response.jsonCode !== 200 && request.failureData) {
+ // 460 jsonCode in Expensify world means "admin required".
+ // Typically, this would only happen if a user attempts an API command that requires policy admin access when they aren't an admin.
+ // In this case, we don't want to apply failureData because it will likely result in a RedBrickRoad error on a policy field which is not accessible.
+ // Meaning that there's a red dot you can't dismiss.
+ if (response.jsonCode === 460) {
+ Log.info('[OnyxUpdateManager] Received 460 status code, not applying failure data');
+ return Promise.resolve();
+ }
return updateHandler(request.failureData);
}
return Promise.resolve();
diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts
index 9fdc0b665faa..84da3be6a70d 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 c3b8ba7a52a2..d5b2adc54de3 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}),
+ },
},
},
],
@@ -602,7 +605,7 @@ function clearWorkspaceOwnerChangeFlow(policyID: string) {
* Adds members to the specified workspace/policyID
* Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
*/
-function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string) {
+function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string, policyMemberAccountIDs: number[]) {
const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const;
const logins = Object.keys(invitedEmailsToAccountIDs).map((memberLogin) => PhoneNumber.addSMSDomainIfPhoneNumber(memberLogin));
const accountIDs = Object.values(invitedEmailsToAccountIDs);
@@ -611,6 +614,8 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
const newPersonalDetailsOnyxData = PersonalDetailsUtils.getPersonalDetailsOnyxDataForOptimisticUsers(newLogins, newAccountIDs);
const announceRoomMembers = buildAnnounceRoomMembersOnyxData(policyID, accountIDs);
+ const optimisticAnnounceChat = ReportUtils.buildOptimisticAnnounceChat(policyID, [...policyMemberAccountIDs, ...accountIDs]);
+ const announceRoomChat = optimisticAnnounceChat.announceChatData;
// create onyx data for policy expense chats for each new member
const membersChats = createPolicyExpenseChats(policyID, invitedEmailsToAccountIDs);
@@ -637,7 +642,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
},
},
];
- optimisticData.push(...newPersonalDetailsOnyxData.optimisticData, ...membersChats.onyxOptimisticData, ...announceRoomMembers.onyxOptimisticData);
+ optimisticData.push(...newPersonalDetailsOnyxData.optimisticData, ...membersChats.onyxOptimisticData, ...announceRoomChat.onyxOptimisticData, ...announceRoomMembers.onyxOptimisticData);
const successData: OnyxUpdate[] = [
{
@@ -648,7 +653,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
},
},
];
- successData.push(...newPersonalDetailsOnyxData.finallyData, ...membersChats.onyxSuccessData, ...announceRoomMembers.onyxSuccessData);
+ successData.push(...newPersonalDetailsOnyxData.finallyData, ...membersChats.onyxSuccessData, ...announceRoomChat.onyxSuccessData, ...announceRoomMembers.onyxSuccessData);
const failureData: OnyxUpdate[] = [
{
@@ -662,10 +667,12 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
},
},
];
- failureData.push(...membersChats.onyxFailureData, ...announceRoomMembers.onyxFailureData);
+ failureData.push(...membersChats.onyxFailureData, ...announceRoomChat.onyxFailureData, ...announceRoomMembers.onyxFailureData);
const params: AddMembersToWorkspaceParams = {
employees: JSON.stringify(logins.map((login) => ({email: login}))),
+ ...(optimisticAnnounceChat.announceChatReportID ? {announceChatReportID: optimisticAnnounceChat.announceChatReportID} : {}),
+ ...(optimisticAnnounceChat.announceChatReportActionID ? {announceCreatedReportActionID: optimisticAnnounceChat.announceChatReportActionID} : {}),
welcomeNote: Parser.replace(welcomeNote),
policyID,
};
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index e0af17241c5c..499d7f943fa7 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);
@@ -1533,10 +1532,6 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits();
const {
- announceChatReportID,
- announceChatData,
- announceReportActionData,
- announceCreatedReportActionID,
adminsChatReportID,
adminsChatData,
adminsReportActionData,
@@ -1594,26 +1589,6 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
},
},
},
- {
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`,
- value: {
- pendingFields: {
- addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- },
- ...announceChatData,
- },
- },
- {
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${announceChatReportID}`,
- value: null,
- },
- {
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`,
- value: announceReportActionData,
- },
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`,
@@ -1673,26 +1648,6 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
},
},
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`,
- value: {
- pendingFields: {
- addWorkspaceRoom: null,
- },
- pendingAction: null,
- isOptimisticReport: false,
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`,
- value: {
- [announceCreatedReportActionID]: {
- pendingAction: null,
- },
- },
- },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`,
@@ -1742,16 +1697,6 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {employeeList: null},
},
- {
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`,
- value: null,
- },
- {
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`,
- value: null,
- },
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`,
@@ -1788,14 +1733,12 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
const params: CreateWorkspaceParams = {
policyID,
- announceChatReportID,
adminsChatReportID,
expenseChatReportID,
ownerEmail: policyOwnerEmail,
makeMeAdmin,
policyName: workspaceName,
type: CONST.POLICY.TYPE.TEAM,
- announceCreatedReportActionID,
adminsCreatedReportActionID,
expenseCreatedReportActionID,
customUnitID,
@@ -1833,8 +1776,10 @@ function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policy
const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits();
- const {expenseChatData, announceChatReportID, announceCreatedReportActionID, adminsChatReportID, adminsCreatedReportActionID, expenseChatReportID, expenseCreatedReportActionID} =
- ReportUtils.buildOptimisticWorkspaceChats(policyID, workspaceName);
+ const {expenseChatData, adminsChatReportID, adminsCreatedReportActionID, expenseChatReportID, expenseCreatedReportActionID} = ReportUtils.buildOptimisticWorkspaceChats(
+ policyID,
+ workspaceName,
+ );
const optimisticData: OnyxUpdate[] = [
{
@@ -1898,14 +1843,12 @@ function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policy
const params: CreateWorkspaceParams = {
policyID,
- announceChatReportID,
adminsChatReportID,
expenseChatReportID,
ownerEmail: policyOwnerEmail,
makeMeAdmin,
policyName: workspaceName,
type: CONST.POLICY.TYPE.TEAM,
- announceCreatedReportActionID,
adminsCreatedReportActionID,
expenseCreatedReportActionID,
customUnitID,
@@ -2169,10 +2112,6 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF
const iouReportID = iouReport.reportID;
const {
- announceChatReportID,
- announceChatData,
- announceReportActionData,
- announceCreatedReportActionID,
adminsChatReportID,
adminsChatData,
adminsReportActionData,
@@ -2239,21 +2178,6 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: newWorkspace,
},
- {
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`,
- value: {
- pendingFields: {
- addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- },
- ...announceChatData,
- },
- },
- {
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`,
- value: announceReportActionData,
- },
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`,
@@ -2310,25 +2234,6 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF
},
},
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`,
- value: {
- pendingFields: {
- addWorkspaceRoom: null,
- },
- pendingAction: null,
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`,
- value: {
- [Object.keys(announceChatData).at(0) ?? '']: {
- pendingAction: null,
- },
- },
- },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`,
@@ -2371,23 +2276,6 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF
successData.push(...employeeWorkspaceChat.onyxSuccessData);
const failureData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`,
- value: {
- pendingFields: {
- addWorkspaceRoom: null,
- },
- pendingAction: null,
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`,
- value: {
- pendingAction: null,
- },
- },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`,
@@ -2590,14 +2478,12 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF
const params: CreateWorkspaceFromIOUPaymentParams = {
policyID,
- announceChatReportID,
adminsChatReportID,
expenseChatReportID: workspaceChatReportID,
ownerEmail: '',
makeMeAdmin: false,
policyName: workspaceName,
type: CONST.POLICY.TYPE.TEAM,
- announceCreatedReportActionID,
adminsCreatedReportActionID,
expenseCreatedReportActionID: workspaceChatCreatedReportActionID,
customUnitID,
@@ -4161,9 +4047,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 +4126,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 +4153,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 +4246,7 @@ function setPolicyAutoReimbursementLimit(policyID: string, limit: string) {
];
const parameters: SetPolicyAutoReimbursementLimitParams = {
- autoReimbursement: {limit: parsedLimit},
+ limit: parsedLimit,
policyID,
};
@@ -4381,6 +4259,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 +4270,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 +4296,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 092ca1fbbe0c..650f297a967b 100644
--- a/src/libs/actions/Policy/Tag.ts
+++ b/src/libs/actions/Policy/Tag.ts
@@ -137,7 +137,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}),
+ },
},
},
],
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 91bc41f54250..b5b9ecef100d 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;
},
});
@@ -1360,6 +1360,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.
@@ -1708,15 +1717,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;
}
@@ -1764,9 +1769,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) {
@@ -1808,9 +1810,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)])];
@@ -1834,7 +1836,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);
}
}
@@ -2055,9 +2057,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;
}
@@ -2123,12 +2124,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;
}
@@ -2150,8 +2150,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));
}
/**
@@ -2317,13 +2315,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;
}
@@ -2389,7 +2386,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));
}
/**
@@ -2696,28 +2692,31 @@ function openReportFromDeepLink(url: string) {
return;
}
- // We need skip deeplinking if the user hasn't completed the guided setup flow.
- Welcome.isOnboardingFlowCompleted({
- onNotCompleted: () => OnboardingFlow.startOnboardingFlow(),
- onCompleted: () => {
- const state = navigationRef.getRootState();
- const currentFocusedRoute = findFocusedRoute(state);
+ const handleDeeplinkNavigation = () => {
+ const state = navigationRef.getRootState();
+ const currentFocusedRoute = findFocusedRoute(state);
- if (isOnboardingFlowName(currentFocusedRoute?.name)) {
- Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
- return;
- }
+ if (isOnboardingFlowName(currentFocusedRoute?.name)) {
+ Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
+ return;
+ }
- if (shouldSkipDeepLinkNavigation(route)) {
- return;
- }
+ if (shouldSkipDeepLinkNavigation(route)) {
+ return;
+ }
- if (isAuthenticated) {
- return;
- }
+ if (isAuthenticated) {
+ return;
+ }
- Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
- },
+ Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
+ };
+
+ // We need skip deeplinking if the user hasn't completed the guided setup flow.
+ Welcome.isOnboardingFlowCompleted({
+ onNotCompleted: OnboardingFlow.startOnboardingFlow,
+ onCompleted: handleDeeplinkNavigation,
+ onCanceled: handleDeeplinkNavigation,
});
});
},
@@ -2761,10 +2760,8 @@ function joinRoom(report: OnyxEntry) {
report.reportID,
ReportUtils.getReportNotificationPreference(report),
CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
- false,
report.parentReportID,
report.parentReportActionID,
- report,
);
}
@@ -4103,7 +4100,7 @@ export {
addComment,
addAttachment,
updateDescription,
- updateWriteCapabilityAndNavigate,
+ updateWriteCapability,
updateNotificationPreference,
subscribeToReportTypingEvents,
subscribeToReportLeavingEvents,
@@ -4132,7 +4129,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 745b4b3b01d1..1273cc3c8784 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/Welcome/index.ts b/src/libs/actions/Welcome/index.ts
index 75529a879104..fc921b16f4cf 100644
--- a/src/libs/actions/Welcome/index.ts
+++ b/src/libs/actions/Welcome/index.ts
@@ -23,6 +23,7 @@ let onboarding: OnboardingData;
type HasCompletedOnboardingFlowProps = {
onCompleted?: () => void;
onNotCompleted?: () => void;
+ onCanceled?: () => void;
};
type HasOpenedForTheFirstTimeFromHybridAppProps = {
@@ -50,9 +51,10 @@ function onServerDataReady(): Promise {
}
let isOnboardingInProgress = false;
-function isOnboardingFlowCompleted({onCompleted, onNotCompleted}: HasCompletedOnboardingFlowProps) {
+function isOnboardingFlowCompleted({onCompleted, onNotCompleted, onCanceled}: HasCompletedOnboardingFlowProps) {
isOnboardingFlowStatusKnownPromise.then(() => {
if (Array.isArray(onboarding) || onboarding?.hasCompletedGuidedSetupFlow === undefined) {
+ onCanceled?.();
return;
}
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 true;
+const willBlurTextInputOnTapOutside: WillBlurTextInputOnTapOutside = () => !getIsNarrowLayout();
export default willBlurTextInputOnTapOutside;
diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx
index ee6cfd90f611..3157a65ff76f 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}) => (