diff --git a/.claude/agents/code-inline-reviewer.md b/.claude/agents/code-inline-reviewer.md
index e364abf46e663..fc05d9ef5f4c7 100644
--- a/.claude/agents/code-inline-reviewer.md
+++ b/.claude/agents/code-inline-reviewer.md
@@ -248,17 +248,17 @@ const {amountColumnSize, dateColumnSize, taxAmountColumnSize} = useMemo(() => {
- `body`: Concise and actionable description of the violation and fix, following the below Comment Format
6. **Each comment must reference exactly one Rule ID.**
7. **Output must consist exclusively of calls to mcp__github_inline_comment__create_inline_comment in the required format.** No other text, Markdown, or prose is allowed.
-8. **If no violations are found, create a comment** (with no quotes, markdown, or additional text):
- LGTM đ Thank you for your hard work!
-9. **Output LGTM if and only if**:
+8. **If no violations are found, add a reaction to the PR**:
+ Add a đ (+1) reaction to the PR body using the `.github/scripts/addPrReaction.sh` script.
+9. **Add reaction if and only if**:
- You examined EVERY changed line in EVERY changed file (via diff + targeted grep/read)
- You checked EVERY changed file against ALL rules
- You found ZERO violations matching the exact rule criteria
- You verified no false negatives by checking each rule systematically
- If you found even ONE violation or have ANY uncertainty do NOT create LGTM comment - create inline comments instead.
+ If you found even ONE violation or have ANY uncertainty do NOT add the reaction - create inline comments instead.
10. **DO NOT invent new rules, stylistic preferences, or commentary outside the listed rules.**
11. **DO NOT describe what you are doing, create comments with a summary, explanations, extra content, comments on rules that are NOT violated or ANYTHING ELSE.**
- Only inline comments regarding rules violations or general comment with LGTM message are allowed.
+ Only inline comments regarding rules violations are allowed. If no violations are found, add a reaction instead of creating any comment.
EXCEPTION: If you believe something MIGHT be a Rule violation but are uncertain, err on the side of creating an inline comment with your concern rather than skipping it.
## Tool Usage Example
@@ -273,22 +273,13 @@ mcp__github_inline_comment__create_inline_comment:
body: '
'
```
-If ZERO violations are found, use the Bash tool to create a top-level PR comment.:
+If ZERO violations are found, use the Bash tool to add a reaction to the PR body:
```bash
-gh pr comment --body 'LGTM :feelsgood:. Thank you for your hard work!'
+.github/scripts/addPrReaction.sh
```
-**IMPORTANT**: When using the Bash tool, always use **single quotes** (not double quotes) around content arguments.
-
-Example:
-```bash
-# Good
-gh pr comment --body 'Use `useMemo` to optimize performance'
-
-# Bad
-gh pr comment --body "Use `useMemo` to optimize performance"
-```
+**IMPORTANT**: Always use the `.github/scripts/addPrReaction.sh` script instead of calling `gh api` directly. This script provides a secure, restricted interface that only allows adding +1 reactions to PRs, preventing arbitrary GitHub API calls.
## Comment Format
diff --git a/.claude/commands/review-code-pr.md b/.claude/commands/review-code-pr.md
index 970b023af675e..214bb70fa5396 100644
--- a/.claude/commands/review-code-pr.md
+++ b/.claude/commands/review-code-pr.md
@@ -1,5 +1,5 @@
---
-allowed-tools: Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),mcp__github_inline_comment__create_inline_comment
+allowed-tools: Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(.github/scripts/addPrReaction.sh:*),mcp__github_inline_comment__create_inline_comment
description: Review a code contribution pull request
---
diff --git a/.env.example b/.env.example
index 31f963dd39254..a5a4997375214 100644
--- a/.env.example
+++ b/.env.example
@@ -40,3 +40,5 @@ FB_PROJECT_ID=YOUR_PROJECT_ID
GITHUB_TOKEN=YOUR_TOKEN
OPENAI_API_KEY=YOUR_TOKEN
+
+SENTRY_AUTH_TOKEN=SENTRY_AUTH_TOKEN
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 0a822a9afdd49..f0737b247f521 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,9 +1,12 @@
# Every PR gets a review from an internal Expensify engineer
* @Expensify/pullerbear
+# PRs that touch the front end source code, get an additional review from the product-pr team
+src/ @Expensify/product-pr @Expensify/pullerbear
+
# PRs that touch the front end style or assets, get an additional review from the Design team
-src/styles/ @Expensify/design @Expensify/pullerbear
-assets/ @Expensify/design @Expensify/pullerbear
+src/styles/ @Expensify/design @Expensify/product-pr @Expensify/pullerbear
+assets/ @Expensify/design @Expensify/product-pr @Expensify/pullerbear
# Philosophy docs are in their early stages and need to be reviewed by Tim to ensure they have consistent formatting and organization
contributingGuides/philosophies/ @tgolen
diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js
index 4dd1a907257d2..8703e99a38839 100644
--- a/.github/actions/javascript/authorChecklist/index.js
+++ b/.github/actions/javascript/authorChecklist/index.js
@@ -15875,6 +15875,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js
index 8c07232d29dee..67dd505705f0a 100644
--- a/.github/actions/javascript/awaitStagingDeploys/index.js
+++ b/.github/actions/javascript/awaitStagingDeploys/index.js
@@ -12679,6 +12679,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/checkAndroidStatus/index.js b/.github/actions/javascript/checkAndroidStatus/index.js
index 2f347064b8bc3..d926380a03063 100644
--- a/.github/actions/javascript/checkAndroidStatus/index.js
+++ b/.github/actions/javascript/checkAndroidStatus/index.js
@@ -737422,6 +737422,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js
index 33e12d3a2bb5b..bde652bb65b8e 100644
--- a/.github/actions/javascript/checkDeployBlockers/index.js
+++ b/.github/actions/javascript/checkDeployBlockers/index.js
@@ -11946,6 +11946,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/checkSVGCompression/index.js b/.github/actions/javascript/checkSVGCompression/index.js
index 1a8494265e3b1..bd1bcbeda95ef 100644
--- a/.github/actions/javascript/checkSVGCompression/index.js
+++ b/.github/actions/javascript/checkSVGCompression/index.js
@@ -20471,6 +20471,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js
index 98f1ecea463d7..3b3a72b093f21 100644
--- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js
+++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js
@@ -12233,6 +12233,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js
index 9774ff3669267..410e81c8e543d 100644
--- a/.github/actions/javascript/getArtifactInfo/index.js
+++ b/.github/actions/javascript/getArtifactInfo/index.js
@@ -11907,6 +11907,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js
index ea09737a39638..7f8090725d3c9 100644
--- a/.github/actions/javascript/getDeployPullRequestList/index.js
+++ b/.github/actions/javascript/getDeployPullRequestList/index.js
@@ -12252,6 +12252,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js
index 3d7d341a8ddbf..ad5e2462e86d6 100644
--- a/.github/actions/javascript/getPreviousVersion/index.js
+++ b/.github/actions/javascript/getPreviousVersion/index.js
@@ -12063,6 +12063,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js
index b495c41be6b9d..3fb0799f7f941 100644
--- a/.github/actions/javascript/getPullRequestDetails/index.js
+++ b/.github/actions/javascript/getPullRequestDetails/index.js
@@ -12036,6 +12036,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/getPullRequestIncrementalChanges/index.js b/.github/actions/javascript/getPullRequestIncrementalChanges/index.js
index 281f9e64a9fec..86b18f8a1f546 100644
--- a/.github/actions/javascript/getPullRequestIncrementalChanges/index.js
+++ b/.github/actions/javascript/getPullRequestIncrementalChanges/index.js
@@ -12138,6 +12138,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js
index c30d651dba42a..58cea7e0027da 100644
--- a/.github/actions/javascript/isStagingDeployLocked/index.js
+++ b/.github/actions/javascript/isStagingDeployLocked/index.js
@@ -11907,6 +11907,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js
index b019da7e5e796..e1f5bdb9afa10 100644
--- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js
+++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js
@@ -13360,6 +13360,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js
index 75ad6573dd5cd..5062a8823db52 100644
--- a/.github/actions/javascript/postTestBuildComment/index.js
+++ b/.github/actions/javascript/postTestBuildComment/index.js
@@ -12037,6 +12037,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js
index 997a7577a8732..65c1c568adec8 100644
--- a/.github/actions/javascript/proposalPoliceComment/index.js
+++ b/.github/actions/javascript/proposalPoliceComment/index.js
@@ -12176,6 +12176,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js
index 48d990ca57ff3..2eb37a3ad9f76 100644
--- a/.github/actions/javascript/reopenIssueWithComment/index.js
+++ b/.github/actions/javascript/reopenIssueWithComment/index.js
@@ -11917,6 +11917,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js
index 3b8cb41338269..073820e9f47bf 100644
--- a/.github/actions/javascript/reviewerChecklist/index.js
+++ b/.github/actions/javascript/reviewerChecklist/index.js
@@ -12009,6 +12009,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js
index 1f444b15d7b80..d3ef98f575997 100644
--- a/.github/actions/javascript/verifySignedCommits/index.js
+++ b/.github/actions/javascript/verifySignedCommits/index.js
@@ -11949,6 +11949,7 @@ class GithubUtils {
console.log('Found the following Internal QA PRs:', internalQAPRMap);
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set((0, arrayDifference_1.default)(PRList, Object.keys(internalQAPRMap)))].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
const sortedPRListMobileExpensify = [...new Set(PRListMobileExpensify)].sort((a, b) => GithubUtils.getPullRequestNumberFromURL(a) - GithubUtils.getPullRequestNumberFromURL(b));
diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts
index a9983db69dc70..1743cc592c9f4 100644
--- a/.github/libs/GithubUtils.ts
+++ b/.github/libs/GithubUtils.ts
@@ -334,6 +334,7 @@ class GithubUtils {
const noQAPRs = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.html_url) : [];
console.log('Found the following NO QA PRs:', noQAPRs);
+ // eslint-disable-next-line unicorn/prefer-set-has
const verifiedOrNoQAPRs = [...new Set([...verifiedPRList, ...verifiedPRListMobileExpensify, ...noQAPRs])];
const sortedPRList = [...new Set(arrayDifference(PRList, Object.keys(internalQAPRMap)))].sort(
diff --git a/.github/scripts/addPrReaction.sh b/.github/scripts/addPrReaction.sh
new file mode 100755
index 0000000000000..9c1f15f740127
--- /dev/null
+++ b/.github/scripts/addPrReaction.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# Secure proxy script to add a +1 reaction to a GitHub PR
+set -eu
+
+if [[ $# -lt 1 ]] || ! [[ "$1" =~ ^[0-9]+$ ]]; then
+ echo "Usage: $0 " >&2
+ exit 1
+fi
+
+PR_NUMBER="$1"
+REPO="${GITHUB_REPOSITORY}"
+
+gh api -X POST "/repos/$REPO/issues/$PR_NUMBER/reactions" -f content="+1"
+
+
diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml
index 3537319e3d344..805c589c7eb44 100644
--- a/.github/workflows/claude-review.yml
+++ b/.github/workflows/claude-review.yml
@@ -14,23 +14,9 @@ concurrency:
jobs:
review:
+ if: github.event.pull_request.draft != true && !contains(github.event.pull_request.title, 'Revert')
runs-on: ubuntu-latest
steps:
- - name: Skip draft PRs on opened event
- if: github.event.action == 'opened' && github.event.pull_request.draft == true
- run: |
- echo "::notice::Skipping review because the PR is a draft"
- exit 0
-
- - name: Check for excluded PRs
- env:
- PR_TITLE: ${{ github.event.pull_request.title }}
- run: |
- if [[ "$PR_TITLE" == *"Revert"* ]]; then
- echo "::notice::Skipping review because the PR is a revert"
- exit 0
- fi
-
- name: Checkout repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
with:
@@ -56,7 +42,7 @@ jobs:
allowed_non_write_users: "*"
prompt: "/review-code-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}"
claude_args: |
- --allowedTools "mcp__github_inline_comment__create_inline_comment"
+ --allowedTools "Task,Glob,Grep,Read,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(.github/scripts/addPrReaction.sh:*),mcp__github_inline_comment__create_inline_comment"
- name: Run Claude Code (docs)
if: steps.filter.outputs.docs == 'true'
@@ -67,4 +53,4 @@ jobs:
allowed_non_write_users: "*"
prompt: "/review-helpdot-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}"
claude_args: |
- --allowedTools "mcp__github_inline_comment__create_inline_comment"
+ --allowedTools "Task,Glob,Grep,Read,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),mcp__github_inline_comment__create_inline_comment"
diff --git a/.github/workflows/react-compiler-compliance.yml b/.github/workflows/react-compiler-compliance.yml
index 3afcdf1bbc1eb..e8900a6ed6344 100644
--- a/.github/workflows/react-compiler-compliance.yml
+++ b/.github/workflows/react-compiler-compliance.yml
@@ -5,7 +5,7 @@ on:
pull_request:
types: [opened, synchronize]
branches-ignore: [staging, production]
- paths: ['**.tsx']
+ paths: ["**.tsx"]
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-react-compiler-compliance
@@ -25,7 +25,10 @@ jobs:
uses: ./.github/actions/composite/setupNode
- name: Run React Compiler Compliance Check
- run: npm run react-compiler-compliance-check check-changed
+ # In phase 0 of the React Compiler compliance check rollout,
+ # we want to report failures but don't fail the check.
+ # See https://github.com/Expensify/App/issues/68765#issuecomment-3487317881
+ run: npm run react-compiler-compliance-check check-changed || true
env:
CI: true
GITHUB_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml
index 33e0551bf2d43..694f841cb1d43 100644
--- a/.github/workflows/reassurePerformanceTests.yml
+++ b/.github/workflows/reassurePerformanceTests.yml
@@ -9,7 +9,7 @@ on:
jobs:
perf-tests:
if: ${{ github.actor != 'OSBotify' }}
- runs-on: ubuntu-24.04-v4
+ runs-on: ubuntu-latest
steps:
- name: Checkout
# v4
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b6607a88d80be..3574e74eb68f4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,6 +17,7 @@ jobs:
runs-on: ubuntu-latest
env:
CI: true
+ NODE_OPTIONS: --max_old_space_size=8192
strategy:
fail-fast: false
matrix:
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 2308fba180ad1..13139f888ed74 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -227,6 +227,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
GCP_GEOLOCATION_API_KEY: ${{ secrets.GCP_GEOLOCATION_API_KEY_STAGING }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
web:
name: Build and deploy Web
@@ -463,6 +464,8 @@ jobs:
- name: Build AdHoc app
run: bundle exec fastlane ios build_adhoc_hybrid
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Configure AWS Credentials
# v4
diff --git a/Gemfile.lock b/Gemfile.lock
index 95a250ee3a52a..3dae6208e9af2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -172,10 +172,10 @@ GEM
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
- ffi (1.17.0)
- ffi (1.17.0-arm64-darwin)
- ffi (1.17.0-x86_64-darwin)
- ffi (1.17.0-x86_64-linux)
+ ffi (1.17.2)
+ ffi (1.17.2-arm64-darwin)
+ ffi (1.17.2-x86_64-darwin)
+ ffi (1.17.2-x86_64-linux-gnu)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
diff --git a/Mobile-Expensify b/Mobile-Expensify
index 04e2aa998bf04..5541484620c02 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 04e2aa998bf046c06f2e8848dd73dc1b1b565187
+Subproject commit 5541484620c02b5541ad6862584dea5c245b3a7f
diff --git a/README.md b/README.md
index 5d16cfd4a0726..7b0c369ec6fee 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,6 @@ We use Reassure for monitoring performance regression. More detailed information
## CodeCov
-[CodeCov] is the service we use to measure and track code coverage. You can find out more about it [here](contributingGuides/CodeCov.md)
+[CodeCov](https://about.codecov.io/) is the service we use to measure and track code coverage. You can find out more about it [here](contributingGuides/CodeCov.md)
----
diff --git a/__mocks__/react-native-permissions.ts b/__mocks__/react-native-permissions.ts
index d98b7f32a611b..d56f5296877d8 100644
--- a/__mocks__/react-native-permissions.ts
+++ b/__mocks__/react-native-permissions.ts
@@ -13,6 +13,7 @@ const request = jest.fn(() => RESULTS.GRANTED as string);
const checkLocationAccuracy: jest.Mock = jest.fn(() => 'full');
const requestLocationAccuracy: jest.Mock = jest.fn(() => 'full');
+// eslint-disable-next-line unicorn/prefer-set-has
const notificationOptions: string[] = ['alert', 'badge', 'sound', 'carPlay', 'criticalAlert', 'provisional'];
const notificationSettings: NotificationSettings = {
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 9319c3c1d994c..8d23305461a95 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -114,8 +114,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009024200
- versionName "9.2.42-0"
+ versionCode 1009024600
+ versionName "9.2.46-0"
// 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"
@@ -249,8 +249,8 @@ dependencies {
implementation "com.google.firebase:firebase-crashlytics"
// GIF support
- implementation 'com.facebook.fresco:fresco:2.5.0'
- implementation 'com.facebook.fresco:animated-gif:2.5.0'
+ implementation 'com.facebook.fresco:fresco:3.4.0'
+ implementation 'com.facebook.fresco:animated-gif:3.4.0'
// AndroidX support library
implementation 'androidx.legacy:legacy-support-core-utils:1.0.0'
diff --git a/assets/images/simple-illustrations/simple-illustration__blueshield.svg b/assets/images/simple-illustrations/simple-illustration__blueshield.svg
new file mode 100644
index 0000000000000..543b879638246
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__blueshield.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/babel.config.js b/babel.config.js
index e1add8f250e45..2ce2cd62b04b1 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -42,9 +42,9 @@ const defaultPluginsForWebpack = [
// We use `@babel/plugin-transform-class-properties` for transforming ReactNative libraries and do not use it for our own
// source code transformation as we do not use class property assignment.
'@babel/plugin-transform-class-properties',
-
+ '@babel/plugin-proposal-export-namespace-from',
// Keep it last
- 'react-native-reanimated/plugin',
+ 'react-native-worklets/plugin',
'@babel/plugin-transform-export-namespace-from',
];
@@ -81,8 +81,6 @@ const metro = {
['@babel/plugin-proposal-class-properties', {loose: true}],
['@babel/plugin-proposal-private-methods', {loose: true}],
['@babel/plugin-proposal-private-property-in-object', {loose: true}],
- // The reanimated babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation
- 'react-native-reanimated/plugin',
/* Fullstory */
'@fullstory/react-native',
@@ -134,6 +132,8 @@ const metro = {
},
],
'@babel/plugin-transform-export-namespace-from',
+ // The worklets babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/
+ 'react-native-worklets/plugin',
],
env: {
production: {
diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts
index ad3124aed0495..4450bb12d66db 100644
--- a/config/webpack/webpack.common.ts
+++ b/config/webpack/webpack.common.ts
@@ -1,3 +1,4 @@
+import {sentryWebpackPlugin} from '@sentry/webpack-plugin';
import {CleanWebpackPlugin} from 'clean-webpack-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import dotenv from 'dotenv';
@@ -29,6 +30,7 @@ const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin') as PreloadWe
const includeModules = [
'react-native-reanimated',
+ 'react-native-worklets',
'react-native-picker-select',
'react-native-web',
'react-native-webview',
@@ -69,6 +71,12 @@ function mapEnvironmentToLogoSuffix(environmentFile: string): string {
const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): Configuration => {
const isDevelopment = file === '.env' || file === '.env.development';
+ if (!isDevelopment) {
+ const releaseName = `${process.env.npm_package_name}@${process.env.npm_package_version}`;
+ console.debug(`[SENTRY ${platform.toUpperCase()}] Release: ${releaseName}`);
+ console.debug(`[SENTRY ${platform.toUpperCase()}] Assets Path: ${platform === 'desktop' ? './desktop/dist/www/**/*.{js,map}' : './dist/**/*.{js,map}'}`);
+ }
+
return {
mode: isDevelopment ? 'development' : 'production',
devtool: 'source-map',
@@ -169,6 +177,28 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment):
}),
...(isDevelopment ? [] : [new MiniCssExtractPlugin()]),
+ // Upload source maps to Sentry
+ ...(isDevelopment
+ ? []
+ : ([
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ sentryWebpackPlugin({
+ authToken: process.env.SENTRY_AUTH_TOKEN as string | undefined,
+ org: 'expensify',
+ project: 'app',
+ release: {
+ name: `${process.env.npm_package_name}@${process.env.npm_package_version}`,
+ },
+ sourcemaps: {
+ // Use relative path from project root - works for both web (dist/) and desktop (desktop/dist/www/)
+ assets: platform === 'desktop' ? './desktop/dist/www/**/*.{js,map}' : './dist/**/*.{js,map}',
+ filesToDeleteAfterUpload: platform === 'desktop' ? './desktop/dist/www/**/*.map' : './dist/**/*.map',
+ },
+ debug: false,
+ telemetry: false,
+ }),
+ ] as WebpackPluginInstance[])),
+
// This allows us to interactively inspect JS bundle contents
...(process.env.ANALYZE_BUNDLE === 'true' ? [new BundleAnalyzerPlugin()] : []),
],
diff --git a/contributingGuides/PAYMENT_VIA_EXPENSIFY.md b/contributingGuides/PAYMENT_VIA_EXPENSIFY.md
new file mode 100644
index 0000000000000..5afdfb57c8081
--- /dev/null
+++ b/contributingGuides/PAYMENT_VIA_EXPENSIFY.md
@@ -0,0 +1,11 @@
+Contributors are eligible to be paid via Expensify 18 months after they were assigned to their first GitHub issue. Around this time an Expensify employee should email the eligible contributor if theyâre active and have completed 5 or more jobs. If itâs been two weeks after your eligibility date and you havenât received an email, please contact [contributors@expensify.com](mailto:contributors@expensify.com?subject=Request%20to%20be%20added%20to%20Expensify%20Payments&body=-%20GitHub%20handle%20%3D%20%0A-%20Legal%20name%20%3D%20%0A-%20Link%20to%20first%20issue%20you%20were%20assigned%20to%20%3D) with the subject âRequest to be added to Expensify Paymentsâ. Please include your GitHub handle, legal name and a link to the first issue you were assigned to. When the employee replies to initiate setup they will ask for a [W9](https://www.irs.gov/pub/irs-pdf/fw9.pdf) (US) or [W8-BEN](https://www.irs.gov/forms-pubs/about-form-w-8-ben).
+
+## Global Reimbursement Details
+- Youâll need to create an Expensify account and Connect Personal Bank Account. To receive payments in USD, GBP, or EUR, add receiving accounts in those currencies via platforms like Wise, Payoneer, PingPong, Skydo, etc.
+ - Payoneer provides a U.S.-based USD account through Citibank. Since you wonât have direct access to a Citibank online account, youâll need to connect the bank account manually instead of using Plaid.
+ - You can do this in **oldDot** by navigating to: Settings > Account > Wallet > Add Personal Bank Account > Connect manually.
+ - Skydo is a service that contributors in India have used.
+- View [Enable Global Reimbursement](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Enable-Global-Reimbursement) to get more details on setting up reimbursements in United States (USD), Canada (CAD), United Kingdom (GBP), European Union (EUR), Australia (AUD) and Singapore (SGD) currencies.
+- View [Receive Payments](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments) for details on receiving payments in the US and internationally, including via Wise (formerly TransferWise), Paypal, Venmo and more.
+
+After approval it can take between 1 business day and a week, depending on the country and service you use.
diff --git a/cspell.json b/cspell.json
index 63de6b187d275..af24e776c01d2 100644
--- a/cspell.json
+++ b/cspell.json
@@ -144,6 +144,8 @@
"deburred",
"REJECTEDTRANSACTION",
"Deel",
+ "deapex",
+ "deapexer",
"deeplink",
"deeplinked",
"deeplinking",
@@ -447,6 +449,7 @@
"nullptr",
"numberformat",
"objc",
+ "objdump",
"oblador",
"OCBC",
"octocat",
@@ -478,6 +481,7 @@
"passwordless",
"Passwordless",
"pathspec",
+ "Payoneer",
"payrollcode",
"pbxproj",
"pdfreport",
@@ -514,6 +518,7 @@
"Pressable",
"Pressables",
"prettierrc",
+ "progname",
"proguard",
"Proofpoint",
"Protip",
@@ -584,6 +589,7 @@
"Schengen",
"Schiffli",
"SCIM",
+ "sdkmanager",
"scriptname",
"seamless",
"Segoe",
@@ -594,6 +600,7 @@
"Sharons",
"shellcheck",
"shellenv",
+ "Skydo",
"shipit",
"shouldshowellipsis",
"signingkey",
@@ -771,7 +778,9 @@
"mple",
"Selec",
"setuptools",
- "DYNAMICEXTERNAL"
+ "DYNAMICEXTERNAL",
+ "RNCORE",
+ "Wooo"
],
"ignorePaths": [
"src/languages/de.ts",
diff --git a/desktop/main.ts b/desktop/main.ts
index a5a65ca7762d5..6c45cf208ef34 100644
--- a/desktop/main.ts
+++ b/desktop/main.ts
@@ -404,6 +404,8 @@ const mainWindow = (): Promise => {
backgroundColor: '#FAFAFA',
width: 1200,
height: 900,
+ minWidth: 375,
+ minHeight: 600,
webPreferences: {
preload: `${__dirname}/contextBridge.js`,
contextIsolation: true,
diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml
index 20d0a60e8487d..ed641feee32bd 100644
--- a/docs/_data/_routes.yml
+++ b/docs/_data/_routes.yml
@@ -104,6 +104,11 @@ platforms:
icon: /assets/images/chat-bubble.svg
description: Use Expensify's chat feature to split bills, chat with employees, and manage payments.
+ - href: concierge-ai
+ title: Concierge AI
+ icon: /assets/images/concierge-avatar-small.svg
+ description: Learn about Concierge AI and what it can do.
+
- href: workspaces
title: Workspaces
icon: /assets/images/shield.svg
diff --git a/docs/articles/expensify-classic/reports/Edit-and-Submit-Expense-Reports.md b/docs/articles/expensify-classic/reports/Edit-and-Submit-Expense-Reports.md
index 9e0f8e6c8a077..24cbb7a810278 100644
--- a/docs/articles/expensify-classic/reports/Edit-and-Submit-Expense-Reports.md
+++ b/docs/articles/expensify-classic/reports/Edit-and-Submit-Expense-Reports.md
@@ -154,7 +154,7 @@ Some Workspaces automatically handle this. Otherwise, submit manually.
**On Desktop:**
1. Open the report.
-2. Click **Undo Submit**.
+2. Click **Retract**.
**On Mobile:**
1. Tap the report.
diff --git a/docs/articles/new-expensify/concierge-ai/Concierge-Basics.md b/docs/articles/new-expensify/concierge-ai/Concierge-Basics.md
new file mode 100644
index 0000000000000..2bf8c48dc8a62
--- /dev/null
+++ b/docs/articles/new-expensify/concierge-ai/Concierge-Basics.md
@@ -0,0 +1,94 @@
+---
+title: Concierge Basics
+description: Get to know Concierge - your built-in support agent, expense assistant, and workflow manager.
+keywords: Concierge AI, support chat, contact Concierge, submit expenses, approve reports, AI help, human support, escalate
+order: 1
+---
+
+
+Concierge is your built-in Expensify assistantâhere to answer questions, create expenses, manage reports, and speed up your workflows. Itâs part AI, part human, and always available.
+
+# What is Concierge?
+
+Concierge is an intelligent support agent that lives inside Expensify. Itâs powered by AI and backed by real humans when needed. Whether youâre asking about a feature, creating an expense, or troubleshooting an issue, Concierge is designed to help.
+
+Think of Concierge as your:
+- Product expert
+- Expense assistant
+- Workflow manager
+
+All rolled into one!
+
+# Where to find Concierge
+
+You can chat with Concierge from anywhere in the app:
+
+- **On web:** Press the **+** button at the bottom left corner of the screen, select **Start Chat**, and type **Concierge** into the composer box.
+- **On mobile:** Tap **Create** at the bottom of your screen, select **Start Chat**, and type **Concierge **into the composer box.
+
+You can also contact Concierge by:
+- Email: **concierge@expensify.com**
+- Text: **47777**
+
+# How to use Concierge AI
+
+Just start chattingâno special commands required. Ask a question, give an instruction, or upload a receipt.
+
+Here are a few examples you can type:
+- âHow do I change my Workspace settings?â
+- âCreate a $12 lunch expense for today.â
+- âSubmit my April travel report.â
+- âApprove Aliceâs report with the Uber expense.â
+
+**Tip:** The more specific you are, the better the response.
+
+# What can Concierge do?
+
+Concierge can help you:
+- Answer questions about Expensifyâs features and settings
+- Diagnose and troubleshoot issues
+- Create or edit expenses
+- Submit, approve, reimburse, or export reports
+- Schedule a call with a human (when available)
+
+New features are added regularly.
+
+# Can I ask Concierge multiple questions at once?
+
+Yes! Just be clear in your message. Concierge can understand multi-step instructions like:
+
+âIs this report reimbursable? And can you create a $25 taxi expense for it?â
+
+# Can I talk to a real person instead of Concierge?
+
+Absolutely. Just say youâd like to chat with a human, and weâll connect youâ24/7.
+
+If youâre working with an onboarding specialist or account manager, Concierge can schedule a call with them for you.
+
+# FAQ
+
+## Is Concierge a real person?
+
+Not exactly. Concierge is an AI-powered assistant that helps with support, expenses, and approvals. But if it canât helpâor if you prefer a humanâitâll bring in a real person automatically.
+
+## Do I have to use special commands to talk to Concierge?
+
+Nope! Just type naturally, like youâre messaging a coworker. You can ask questions, give instructions, or even send receipts directly in the chat.
+
+## What happens if Concierge doesnât understand my question?
+
+If somethingâs unclear, Concierge will ask for more details. And if it still canât help, itâll escalate to a human who can jump in.
+
+## Can I use Concierge on both web and mobile?
+
+Yes! Concierge is available everywhereâdesktop, mobile app, email, and even SMS. Wherever you start a chat, Concierge will follow the conversation across platforms.
+
+## Is it safe to send receipts or sensitive info to Concierge?
+
+Yes. Concierge only sees what it needs to help with your request. Expensify doesnât use your data to train public AI models, and we have zero-retention agreements with our partners.
+
+## What if I change my mind and want to talk to a human?
+
+Just say so! You can type âTalk to a humanâ at any time and youâll be connected with a real support rep.
+
+
diff --git a/docs/articles/new-expensify/concierge-ai/Concierge-Intelligence.md b/docs/articles/new-expensify/concierge-ai/Concierge-Intelligence.md
new file mode 100644
index 0000000000000..4ec3bf338b9ea
--- /dev/null
+++ b/docs/articles/new-expensify/concierge-ai/Concierge-Intelligence.md
@@ -0,0 +1,133 @@
+---
+title: Concierge Intelligence
+description: Learn how Concierge uses context, AI, and automation to understand what you need and get it done fast.
+keywords: Concierge Intelligence, how Concierge works, AI context, multi-modal agent, contextual chatbot, Expensify support AI, Concierge automation
+order: 2
+---
+
+
+Concierge isnât just smartâitâs context-aware, multi-skilled, and built to help you get more done with less effort. This guide explains how Concierge works, what makes it intelligent, and how to get the most out of it.
+
+# How Concierge Intelligence works
+
+Concierge is a **hybrid AI agent** built with generative AI, automation tools, and a powerful rules engine. It figures out what you're trying to do, pulls in the right tools, and responds with answersâor takes action automatically.
+
+If Concierge doesnât know something, it escalates to a real person.
+
+# What makes Concierge intelligent
+
+Concierge uses a few smart capabilities to figure out how to help:
+
+- **Context-aware** â Understands *where* youâre asking from to tailor the response.
+- **Multi-modal** â Acts like multiple assistants (support agent, expense assistant, etc.) in one.
+- **Hybrid support** â Combines AI and humans, switching seamlessly when needed.
+- **Natural language understanding** â Responds to conversational input like a coworker would.
+
+# Understanding Concierge's contextual behavior
+
+Concierge adapts based on where and how you ask questions:
+
+- If you chat inside a **report** â It answers about that report.
+- If you're in a **DM** â It considers your full expense history.
+- If you're in a **Workspace chat** â It focuses on that Workspace.
+- If you're on a specific **expense** â It assumes you're referring to that expense.
+
+You donât need to explain every detailâjust speak naturally and Concierge will fill in the blanks.
+
+# What is a multi-modal agent?
+
+Instead of having different bots for different tasks, Concierge is all-in-one. That means:
+
+- You can ask anythingâno need to figure out who to ask.
+- You can combine requests in one message.
+ - Example: âIs this reimbursable? Can you add a $12 taxi expense too?â
+
+# Can I ask multiple things at once?
+
+Yes! Concierge can understand complex or multi-part requests, as long as itâs clear what you want.
+
+Hereâs what works well:
+- âCreate a $5 lunch expense and add it to my April report.â
+- âWhat does ânon-reimbursableâ mean, and can you mark this expense as such?â
+
+The more specific you are, the better Concierge can help.
+
+# What if Concierge doesnât know something?
+
+If Concierge canât answer a question, it will escalate to a real personâoften without you needing to ask.
+
+You can also type **âTalk to a humanâ** at any time to request an escalation.
+
+# How Concierge is built
+
+Concierge is powered by a mix of technology and human backup:
+
+- A custom-trained GenAI model
+- A rules engine that handles logic and automation
+- Real-time access to your Expensify data (just enough to help)
+- Escalation paths to live support when needed
+
+# How does Concierge protect my data?
+
+Concierge is built with privacy at its core:
+- Your data stays inside Expensify and is only accessed as needed.
+- AI systems never see more than what's required to respond to your message.
+- We have **zero-retention** agreements with AI providers, meaning your data is never stored or reused.
+
+Thereâs no risk of another customer seeing your dataâbecause they canât.
+
+# Why Concierge is different
+
+- Understands where youâre chatting from and adjusts accordingly
+- Handles natural, multi-part questions with ease
+- Manages support, expenses, and reporting in one place
+- Escalates to a human when needed
+- No setup or training requiredâit just works
+
+# FAQ
+
+## How does Concierge determine what Iâm referring to?
+
+Concierge uses the context of your messageâlike the chat location (report, Workspace, expense), prior messages, and the specific wording of your request. It pairs this with internal identifiers (like report IDs or expense metadata) to match your request to the right object, without you needing to specify it directly.
+
+## How is context "understood" technically?
+
+Context is inferred using a combination of:
+- Chat metadata (where you're messaging from)
+- Your role and permissions
+- Previous conversation turns
+- Structured data (like expense amounts, merchant names, and report statuses)
+
+Concierge uses this context to understand your request and provide a relevant response.
+
+## Whatâs the difference between the AI and the rules engine?
+
+- The **AI (LLM)** interprets what youâre asking and generates a human-like response.
+- The **rules engine** executes structured logic behind the scenesâfor example, determining which reports are ready to submit, or applying Workspace rules to an expense.
+- The two work together: AI figures out intent; the rules engine ensures valid outcomes.
+
+## What does "multi-modal" mean in this context?
+
+In Expensify, "multi-modal" means Concierge can handle multiple functionsâsupport, expense management, and approvalsâin a single thread. You donât need to choose a specific agent or mode. Concierge identifies your intent based on the language used and responds accordingly, even if you blend requests.
+
+## How does Concierge know when to escalate to a human?
+
+If the model is unsure, detects missing context, or encounters something outside its capabilities (e.g., Workspace reconfiguration), it uses fallback logic to escalate. You can also manually escalate by saying âTalk to a human.â
+
+## Is Concierge really reading my data?
+
+Not exactly. The AI model doesnât browse your data freely. It only receives the specific structured data relevant to your requestâfor example, the report name, amount, or receipt image you reference. This is passed securely via the system prompt, and the model canât access or recall any other customerâs data.
+
+## Is my data ever stored or used to train Concierge?
+
+No. Concierge runs on AI models with **zero-retention agreements**. That means your data is not logged, stored, or used to train future versions of the modelâby Expensify or by any third party.
+
+## Can Concierge give different answers in different places?
+
+Yesâand thatâs by design. Because itâs context-aware, asking âIs this reimbursable?â in a report chat will return a different result than asking it in a DM. This makes responses faster and more relevant.
+
+## Can I test how Concierge behaves in different modes?
+
+Absolutely. Try sending the same question in different contexts (a report, a Workspace chat, a DM) and youâll see how Concierge tailors its response to match.
+
+
diff --git a/docs/articles/new-expensify/concierge-ai/Expense-Assistant.md b/docs/articles/new-expensify/concierge-ai/Expense-Assistant.md
new file mode 100644
index 0000000000000..41896fdbb4299
--- /dev/null
+++ b/docs/articles/new-expensify/concierge-ai/Expense-Assistant.md
@@ -0,0 +1,124 @@
+---
+title: Expense Assistant
+description: Learn how to use Concierge to create, edit, and manage expenses just by chatting â no forms required.
+keywords: Expense Assistant, Concierge, create expense, edit expense, manage expense, chatbot, mileage tracking, receipt capture
+order: 4
+---
+
+
+
+Need to create or update an expense? Just ask Concierge. Whether you're uploading a receipt, tracking mileage, or fixing a merchant name, the Expense Assistant can handle it â no forms required.
+
+# What is Concierge Expense Assistant?
+
+Concierge's Expense Assistant is a built-in feature that helps you manage expenses by chatting with Concierge. You can create, edit, and ask questions about receipts, purchases, and mileage â all in plain language.
+
+Instead of filling out fields manually, just say what you need, and Concierge will do the rest.
+
+# What tasks can Concierge handle automatically?
+
+Concierge handles many expense tasks for you behind the scenes:
+
+- Categorizes expenses based on your Workspace rules
+- Applies categories based on your past behavior
+- Adds expenses to the correct report
+- Moves held expenses to a new report when other expenses are submitted
+
+# How to use Concierge Expense Assistant
+
+You can ask Concierge to take action on expenses directly in chat. Hereâs what you can do:
+
+## Create expenses
+
+Say things like:
+
+- âCreate a $5 Starbucks expense for coffee with Aliceâ
+- âRecord 25 miles driving for lunch with Bobâ
+- âAdd a $60 dinner expense to my New York trip reportâ
+
+You can also:
+- Upload a receipt image directly in the chat
+- Email a receipt to **concierge@expensify.com**
+- Text a receipt to **47777** (US only)
+
+## Modify expenses
+
+Ask Concierge to:
+- Change the merchant
+- Update the amount
+- Categorize the expense
+- Mark it as non-reimbursable
+- Add or update tags
+- Edit the description
+
+**Example requests:**
+- âChange the merchant to Taco Tim'sâ
+- âChange the amount to $6â
+- âCategorize this as Client Mealsâ
+- âTag this to Bobâs Bananasâ
+- âUpdate the description to matcha with Aliceâ
+
+## Add attendees or notes
+
+Just include this info in your request:
+
+- âCreate a $15 lunch expense with Alice and Bobâ
+- âAdd a note saying 'team celebration lunch'â
+- âAdd Alice as an attendeeâ
+
+If something isn't supported yet, Concierge will either ask for clarification or escalate your request.
+
+# How does Concierge know which expense to update?
+
+Concierge uses context to figure out which expense you mean:
+
+- **On an individual expense:** It assumes you're referring to that expense.
+- **In a report chat:** It narrows the search using the merchant, amount, or description.
+- **In a workspace chat:** It focuses on expenses tied to that Workspace.
+- **In a direct message (DM):** It may ask for more details if itâs unclear.
+
+The more details you provide, the easier it is for Concierge to help.
+
+# What if I make a mistake?
+
+No problem â just ask Concierge to fix it.
+
+**Example:**
+> âThat Starbucks expense was actually $7, not $5. Can you update it?â
+
+Concierge will update the expense and confirm the change.
+
+# FAQ
+
+## What features are not yet supported by the Expense Assistant?
+
+Some expense actions are not currently supported in chat, but may be added in the future:
+
+- Deleting an expense
+- Categorizing all expenses from a specific merchant (e.g., âCategorize all Starbucks expenses as Coffee Meetingsâ)
+- Attaching a receipt to an existing transaction via chat
+- Tagging expenses based on past behavior
+
+If you request these actions, Concierge will notify you that the feature is not yet available."
+
+---
+
+## Can I use the Expense Assistant on mobile?
+
+Yes! The Expense Assistant works the same way on both web and mobile.
+
+---
+
+## Can I create mileage expenses with Concierge?
+
+Yes â just include the number of miles and reason in your request:
+
+> âRecord 20 miles for client meeting with Alice.â
+
+---
+
+## Can I create an expense without a receipt?
+
+Yes â you can log expenses with just a message. Receipts are optional unless required by your Workspace rules.
+
+
diff --git a/docs/articles/new-expensify/concierge-ai/Support-Agent.md b/docs/articles/new-expensify/concierge-ai/Support-Agent.md
new file mode 100644
index 0000000000000..592c1e048941b
--- /dev/null
+++ b/docs/articles/new-expensify/concierge-ai/Support-Agent.md
@@ -0,0 +1,68 @@
+---
+title: Support Agent
+description: Learn how the Concierge Support Agent can answer questions, troubleshoot issues, and connect you to real-time help in Expensify.
+keywords: Concierge Support, Support Agent, Expensify help, AI support, troubleshoot Expensify, how to contact Concierge, onboarding call, Expensify chat support
+order: 3
+---
+
+Concierge is your AI-powered support agent inside Expensifyâavailable 24/7 to answer questions, troubleshoot problems, and connect you with a human if needed.
+
+# Concierge Support Agent
+
+## Who can use Concierge Support Agent
+
+Anyone with an Expensify account can contact Concierge for helpâwhether you're a new member setting up your Workspace or a Workspace Admin looking to troubleshoot account issues.
+
+## Where to find Concierge Support Agent
+
+You can ask Concierge for help from anywhere in Expensify:
+
+- **Web:** Click the chat icon in the lower-right corner
+- **Mobile:** Tap the hamburger menu in the top-left corner, then tap **Concierge**
+- **Workspace chat:** Mention Concierge in a chat room (e.g., `#admins`)
+- **Report or expense threads:** Ask a question in the thread
+- **Email:** Send a message to concierge@expensify.com
+- **Text (US only):** Text 47777
+
+Wherever you reach out, Concierge uses context from your account to tailor a relevant response.
+
+## What Concierge Support can help with
+
+Concierge can help with most common support requests:
+
+- â Explain how Expensify features work
+- â Walk you through setup or configuration
+- â Troubleshoot issues or errors
+- â Schedule calls with onboarding or account specialists
+- â Reconfigure your Workspace *on your behalf* (coming soon)
+
+If youâre wondering âhow do IâŚ?â, Concierge is the fastest way to get answers.
+
+## Can I talk to a human instead?
+
+Yes! Just ask. If your question is complex, unclear, or you prefer human help, Concierge will connect you with a real support team member.
+
+Expensify offers 24/7 human support via chat.
+
+## Can I speak with someone on the phone?
+
+If you're working with an onboarding specialist or account manager, Concierge can help schedule a call. Just ask to set one up.
+
+## What happens when Concierge Support Agent doesnât know something?
+
+If Concierge doesnât have an immediate answer, it will escalate your request to a human automaticallyâno hoops, no repeats. Youâll stay in the same chat thread the entire time.
+
+# FAQ
+
+## How can I get faster responses from Concierge?
+
+Providing more detail upfront leads to quicker answers. Include:
+
+- Report name or amount
+- Expense description or merchant
+- Exact error message (if any)
+- What you were doing when the issue occurred
+
+## Can Concierge update my Workspace settings?
+
+Not yet! Concierge can walk you through how to make changes, but it wonât make updates on your behalfâfor now.
diff --git a/docs/articles/new-expensify/connect-credit-cards/Commercial-feeds.md b/docs/articles/new-expensify/connect-credit-cards/Commercial-feeds.md
index f721c4728997a..76863789c4229 100644
--- a/docs/articles/new-expensify/connect-credit-cards/Commercial-feeds.md
+++ b/docs/articles/new-expensify/connect-credit-cards/Commercial-feeds.md
@@ -1,7 +1,7 @@
---
title: Commercial Card Feeds
description: Learn how to set up and manage commercial card feeds (Visa, Mastercard, Amex) in Expensify.
-keywords: [New Expensify, commercial feed, Mastercard feed, Visa feed, Amex feed, company cards, corporate cards, CDF, VCF, control account]
+keywords: [New Expensify, commercial feed, Mastercard feed, Visa feed, Amex feed, company cards, corporate cards, CDF, VCF, GL1025, control account]
---
Commercial feeds are the most reliable way to import company card expenses. These feeds are not affected by login credential changes or banking UI updates, making them ideal for growing teams and finance admins.
diff --git a/docs/articles/new-expensify/connections/Uber-for-Business.md b/docs/articles/new-expensify/connections/Uber-for-Business.md
new file mode 100644
index 0000000000000..21a392ed2b7c1
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Uber-for-Business.md
@@ -0,0 +1,77 @@
+---
+title: Uber for Business
+description: Learn how to connect Uber for Business to your Expensify workspace, invite employees, and automate receipt collection for Uber Rides and Uber Eats.
+keywords: [New Expensify, Uber for Business, Uber integration, automate Uber receipts, connect Uber to Expensify, U4B, Uber Eats, Uber Rides, receipt automation]
+---
+
+
+
+**Note: This feature is currently in beta. Please reach out to Concierge to request access!**
+
+Connect your Expensify workspace to Uber for Business to automatically import Uber Rides and Uber Eats receipts for your team. This guide walks you through setup, employee management, and receipt import.
+
+# Connect the Uber for Business Integration
+
+## Step 1: Enable Receipt Partners in your Workspace
+1. Go to **Workspaces > [Workspace Name] > More features**.
+2. Toggle on **Receipt partners**.
+3. Once enabled, a new **Receipt partners** section will appear in the left-hand menu.
+
+**Note:** To enable Uber for Business from Expensify Classic, go to **Settings > Workspaces > [Workspace Name] > Accounting**. Under **Travel Integrations**, click **Uber for Business**, then click **Take me to New Expensify**.
+
+## Step 2: Connect Uber for Business
+1. Go to **Workspaces > [Workspace Name] > Receipt partners**
+3. Click **Connect** next to Uber for Business.
+4. Authorize or create your Uber for Business (U4B) account in the new browser tab that opens.
+5. When complete, the tab will close, and a list of workspace members not yet invited to U4B will appear.
+6. Confirm which workspace members should be invited to U4B (theyâll all be selected by default), which will trigger an invite from Uber that each member must accept.
+
+If your connection fails or expires, a warning will appear under **Workspaces > Receipt partners > Uber for Business**. To resolve the error, click the three-dot menu and choose **Enter credentials** to reconnect.
+
+# Manage the Uber for Business connection
+
+## Manage invites
+
+To manually invite members to Uber for Business later:
+
+1. Go to **Workspaces > [Workspace Name] > Receipt partners**
+2. Click **Manage invites**.
+3. From here, you can filter the members list, invite new employees, or resend any outstanding invites that haven't been accepted yet.
+
+As an admin, you can also automatically keep your Uber for Business roster in sync with your Expensify workspace members list. We offer two configurable settings:
+
+- Invite new workspace members to Uber for Business
+- Deactivate removed workspace members from Uber for Business
+
+## Configure a Centralized Billing Account (Optional)
+
+We will automatically detect if your Uber for Business organization uses centralized billing, but youâll still need to choose which Expensify account should receive the receipts:
+
+1. Go to **Workspaces > Receipt partners > Uber for Business**.
+2. Click **Edit** next to **Central billing account**.
+3. Select a workspace member (The workspace owner is selected by default).
+4. Save your changes.
+
+# Disconnect Uber for Business
+
+1. Go to **Workspaces > Receipt partners > Uber for Business**.
+2. Click the three-dot menu.
+3. Select **Disconnect** and confirm in the modal window.
+
+
+# FAQs
+
+## Can I use this integration in Expensify Classic?
+The Uber for Business connection can only be connected and managed in New Expensify, but once connected, imported Uber receipts will show up in both New Expensify and Expensify Classic.
+
+## Can I invite employees again if they didnât accept the first time?
+Yes! Go to **Manage invites**, find their name or email, and click **Resend**.
+
+## What do the different buttons and badges on the Manage Invites page mean?
+- **Invite button** â The employee has never been invited.
+- **Resend button** - The employee has been invited, but has not yet accepted.
+- **Pending badge** â The invite has been accepted, but additional approval is required within Uber.
+- **Linked badge** â The employee has accepted the invite.
+- **Suspended badge** â The employeeâs Uber account is linked, but has been temporarily suspended by Uber (i.e rider disputes, etc).
+
+
diff --git a/docs/articles/new-expensify/reports-and-expenses/Create-and-Submit-Reports.md b/docs/articles/new-expensify/reports-and-expenses/Create-and-Submit-Reports.md
index 0395d9d114c7e..dbb5845d6d352 100644
--- a/docs/articles/new-expensify/reports-and-expenses/Create-and-Submit-Reports.md
+++ b/docs/articles/new-expensify/reports-and-expenses/Create-and-Submit-Reports.md
@@ -1,7 +1,7 @@
---
title: Create-and-Submit-Reports.md
description: Learn how to use New Expensifyâs report-first flow to create, edit, submit, and retract expense reports.
-keywords: [New Expensify, create report, submit report, retract report, undo submit, undo close, add expenses, fix report]
+keywords: [New Expensify, create report, submit report, retract report, add expenses, fix report]
---
Easily manage your business expenses in New Expensify with our streamlined report-first workflow. This guide walks you through creating, editing, submitting, and even retracting expense reports when needed.
@@ -56,14 +56,11 @@ Once the report includes at least one expense, the Add Expense option will be un
# How to retract or edit submitted reports in New Expensify
-Submitted a report too early? Need to add or remove an expense? You can **retract** submitted reports using the **More** > **Undo Submit** or **Undo Close** actions.
+Submitted a report too early? Need to add or remove an expense? You can **retract** submitted reports using the **More** > **Retract** actions.
-**Retract** means returning a report from **closed** or **processing** back to the **open** state so you can edit it.
+**Retract** means returning a report from **done** or **outstanding** back to the **draft** state so you can edit it.
-- **Undo submit**: The retract button for **processing reports**
-- **Undo close**: The retract button for **closed reports**
-
-**Note:** Only the person who submitted the report can undo or retract it.
+**Note:** Only the person who submitted the report can retract it.
---
@@ -135,4 +132,4 @@ The Submit button only appears once your report includes at least one valid expe
## Can I remove an expense after submitting?
-Yes. Youâll need to retract the report using More > Undo Submit or Undo Close, depending on the report's state. Then you can remove or edit expenses before resubmitting.
+Yes. Youâll need to retract the report using More > Retract, depending on the report's state. Then you can remove or edit expenses before resubmitting.
diff --git a/docs/articles/new-expensify/reports-and-expenses/Delete-Expenses.md b/docs/articles/new-expensify/reports-and-expenses/Delete-Expenses.md
index e4aee94dcf559..cf0d07a60e9bc 100644
--- a/docs/articles/new-expensify/reports-and-expenses/Delete-Expenses.md
+++ b/docs/articles/new-expensify/reports-and-expenses/Delete-Expenses.md
@@ -1,7 +1,7 @@
---
title: Delete Expenses
description: Learn how to delete personal or company card expenses in New Expensify, including rules for submitted reports, admin-only cases, and account-level deletion limits.
-keywords: [New Expensify, delete expenses, remove expense, company card, undo submit, draft expense, report expense, expense deletion]
+keywords: [New Expensify, delete expenses, remove expense, company card, retract, draft expense, report expense, expense deletion]
---
@@ -25,12 +25,12 @@ You can delete any personal (out-of-pocket) expense that hasnât been submitted
# The report is submitted or marked as "Done"
-To delete an expense on a submitted report, you'll need to reopen it first:
+To delete an expense on a submitted report, you'll need to retract it first:
-1. Click **Undo submit** or **Undo close**.
+1. Click **Retract**.
2. Then follow the deletion steps above.
-**Note:** You can only undo reports that are in the **Closed** or **Processing** state. Reports that are **Approved** or **Paid** cannot be reopened.
+**Note:** You can only retract reports that are **Done** or **Outstanding**. Reports that are **Approved** or **Paid** cannot be retracted.
# Deleting company card expenses
@@ -57,9 +57,9 @@ Company card expenses canât be deleted if **Allow deleting transactions** is t
Expense actions like deleting, editing, or retracting can only be done in your own account. Even Workspace Admins canât delete another memberâs expenses. If you need to help a teammate, ask them to add you as a [Copilot](https://help.expensify.com/articles/new-expensify/settings/Copilot-Access) so you can assist from their account.
-## Can I reopen a report after itâs been approved or paid?
+## Can I retract a report after itâs been approved or paid?
-No, reports in **Approved** or **Paid** status canât be reopened. Only reports in a **Closed** or **Processing** state can be reopened to allow expense deletion.
+No, reports in **Approved** or **Paid** status canât be retracted. Only reports that are **Done** or **Outstanding** can be retracted to allow expense deletion.
## Can Expensify permanently delete expenses for me?
diff --git a/docs/articles/new-expensify/reports-and-expenses/Edit-Expense-Reports.md b/docs/articles/new-expensify/reports-and-expenses/Edit-Expense-Reports.md
index ff823dae5a8ef..e739f2960678f 100644
--- a/docs/articles/new-expensify/reports-and-expenses/Edit-Expense-Reports.md
+++ b/docs/articles/new-expensify/reports-and-expenses/Edit-Expense-Reports.md
@@ -1,11 +1,11 @@
---
title: Edit Expense Reports
description: Learn how to retract, edit, and manage expense reports in New Expensify.
-keywords: [New Expensify, retract report, undo submit, undo close, edit submitted report, reopen report, resubmit report, accounting export]
+keywords: [New Expensify, retract report, edit submitted report, resubmit report, accounting export]
---
-If you submitted a report too early or need to make changes, you can easily update it by retracting the report using **Undo submit** or **Undo close**. This returns the report to an editable state.
+If you submitted a report too early or need to make changes, you can easily update it by retracting the report using **Retract**. This returns the report to an editable state.
**Note:** Report actions like creating, submitting, or retracting can only be done in your own account. If you need to help a teammate, consider asking them to add you as a [Copilot](https://help.expensify.com/articles/new-expensify/settings/Copilot-Access).
@@ -18,48 +18,48 @@ This option is helpful when:
- You want to move expenses to a different report.
You can retract reports with the following statuses:
-- **Processing reports**: Only the submitter (or a Workspace Admin submitting their own report) can use **Undo submit**.
-- **Closed reports**: Only **Workspace Admins** can use **Undo close**.
+- **Outstanding reports**: Only the submitter (or a Workspace Admin submitting their own report) can use **Retract**.
+- **Done reports**: Only **Workspace Admins** can use **Retract**.
**Note:** Held expenses or violations wonât stop you from retracting a report.
---
-## How to retract a Processing report
+## How to retract an outstanding report
1. Open the report.
2. Tap the **More** menu (three dots in the top-right corner).
-3. Tap **Undo submit**.
-4. The report will change to **Open** and display a **Retracted** system message.
- - You can only retract reports you submitted from your own account. Reports submitted by other members require CoPilot access to retract.
+3. Tap **Retract**.
+4. The report will change to **Draft** and display a **Retracted** system message.
+ - You can only retract outstanding reports you submitted from your own account. Reports submitted by other members require CoPilot access to retract.
**Note:** Submitters wonât receive a notification when retracting their own report.
---
-## How to use Undo Close on a Closed Report
+## How to retract a done report
-1. Open the closed report.
-2. Tap **More** > **Undo close**.
-3. A system message confirms the report is reopened and editable.
+1. Open the done report.
+2. Tap **More** > **Retract**.
+3. A system message confirms the report is retracted and editable.
-**Note:** Only Workspace Admins can retract their own closed reports. Admins canât retract other membersâ reports unless theyâve been added as a [Copilot](https://help.expensify.com/articles/new-expensify/settings/Copilot-Access).
+**Note:** Only Workspace Admins can retract done reports.
---
## Editing Expense Reports After Exporting to an Accounting System
-Admins can still retract closed reports even if theyâve already been exported to an accounting system such as QuickBooks or NetSuite.
+Admins can still retract done reports even if theyâve already been exported to an accounting system such as QuickBooks or NetSuite.
-When you click **Undo close**, a warning modal will appear to let you know:
-- Reopening the report may cause **data mismatches** between Expensify and the accounting software.
+When you click **Retract**, a warning modal will appear to let you know:
+- Retracting the report may cause **data mismatches** between Expensify and the accounting software.
- Any edits made to the expense report **wonât sync** with the report thatâs already been exported.
From there, you can choose:
-- **Reopen report**: Confirms and returns the report to an editable state.
+- **Retract**: Confirms and returns the report to an editable state.
- **Cancel**: Dismisses the warning message without making changes to the expense report.
-Once reopened, a Workspace Admin can fully edit the reportâadding or deleting expenses, changing categories, and more.
+After retracting, a Workspace Admin can fully edit the reportâadding or deleting expenses, changing categories, and more.
---
@@ -75,10 +75,8 @@ You can't retract a report thatâs already been **approved** or **paid**.
## What happens if I retract and edit a report after itâs exported to our accounting system?
-Youâll see a warning modal before reopening. Any edits wonât sync to the external system. If you make changes to the report, those will need to be manually reconciled in the accounting system.
-
-## Can I edit or retract someone elseâs report?
-
-No. You can only manage reports you submitted from your own account. If you need to take these actions for another employee, ask them to add you as a [Copilot](https://help.expensify.com/articles/new-expensify/settings/Copilot-Access).
+Youâll see a warning modal before retracting. Any edits wonât sync to the external system. If you make changes to the report, those will need to be manually reconciled in the accounting system.
+## Can I edit or retract someone elseâs outstanding report?
+No. You can only retract outstanding reports you submitted from your own account. If you need to take these actions for another employee, ask them to add you as a [Copilot](https://help.expensify.com/articles/new-expensify/settings/Copilot-Access). Admins can retract done reports for their colleagues.
diff --git a/docs/new-expensify/hubs/concierge-ai/index.html b/docs/new-expensify/hubs/concierge-ai/index.html
new file mode 100644
index 0000000000000..bef3c05f826b2
--- /dev/null
+++ b/docs/new-expensify/hubs/concierge-ai/index.html
@@ -0,0 +1,6 @@
+---
+layout: default
+title: Billing & Subscriptions
+---
+
+{% include hub.html %}
diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj
index e9fbab1133e49..9f605a6763899 100644
--- a/ios/NewExpensify.xcodeproj/project.pbxproj
+++ b/ios/NewExpensify.xcodeproj/project.pbxproj
@@ -15,7 +15,6 @@
0CDA8E37287DD6A0004ECBEC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */; };
0DFC45942C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */; };
0F5E5350263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; };
- 0F749C2B3B8F4562B816DEAB /* BuildFile in Resources */ = {isa = PBXBuildFile; };
1246A3EF20E54E7A9494C8B9 /* ExpensifyNeue-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = F4F8A052A22040339996324B /* ExpensifyNeue-Regular.otf */; };
18D050E0262400AF000D658B /* BridgingFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D050DF262400AF000D658B /* BridgingFile.swift */; };
1F7170E8E7867C00D32F03FE /* Pods_NotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4521D653AC9D36713686E739 /* Pods_NotificationServiceExtension.framework */; };
@@ -502,7 +501,6 @@
1246A3EF20E54E7A9494C8B9 /* ExpensifyNeue-Regular.otf in Resources */,
D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */,
524F95D57E75496EBD14B0AA /* ExpensifyMono-BoldItalic.otf in Resources */,
- 0F749C2B3B8F4562B816DEAB /* BuildFile in Resources */,
59164B2F48344A53975791A9 /* CustomEmojiNativeFont.ttf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -787,7 +785,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\nexport RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > `$NODE_BINARY --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'\"`\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open `$NODE_BINARY --print \"require('path').dirname(require.resolve('expo/package.json')) + '/scripts/launchPackager.command'\"` || echo \"Can't start packager automatically\"\n fi\nfi\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 129f1a693200f..ae910f65aeaaf 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -23,7 +23,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 9.2.42
+ 9.2.46CFBundleSignature????CFBundleURLTypes
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.2.42.0
+ 9.2.46.0FullStoryOrgId
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 92fd5f128a89c..c99a0d4b3a7e4 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.2.42
+ 9.2.46CFBundleVersion
- 9.2.42.0
+ 9.2.46.0NSExtensionNSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index d0255ff13c931..c116b04322b8e 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -3559,7 +3559,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.308):
+ - RNLiveMarkdown (0.1.310):
- boost
- DoubleConversion
- fast_float
@@ -3587,7 +3587,7 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNReanimated/worklets
+ - RNWorklets
- SocketRocket
- Yoga
- RNLocalize (3.5.4):
@@ -3735,7 +3735,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - RNReanimated (3.19.1):
+ - RNReanimated (4.1.2):
- boost
- DoubleConversion
- fast_float
@@ -3762,11 +3762,11 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNReanimated/reanimated (= 3.19.1)
- - RNReanimated/worklets (= 3.19.1)
+ - RNReanimated/reanimated (= 4.1.2)
+ - RNWorklets
- SocketRocket
- Yoga
- - RNReanimated/reanimated (3.19.1):
+ - RNReanimated/reanimated (4.1.2):
- boost
- DoubleConversion
- fast_float
@@ -3793,10 +3793,11 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNReanimated/reanimated/apple (= 3.19.1)
+ - RNReanimated/reanimated/apple (= 4.1.2)
+ - RNWorklets
- SocketRocket
- Yoga
- - RNReanimated/reanimated/apple (3.19.1):
+ - RNReanimated/reanimated/apple (4.1.2):
- boost
- DoubleConversion
- fast_float
@@ -3823,9 +3824,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
+ - RNWorklets
- SocketRocket
- Yoga
- - RNReanimated/worklets (3.19.1):
+ - RNScreens (4.15.4):
- boost
- DoubleConversion
- fast_float
@@ -3841,21 +3843,21 @@ PODS:
- React-Fabric
- React-featureflags
- React-graphics
- - React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
+ - React-RCTImage
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNReanimated/worklets/apple (= 3.19.1)
+ - RNScreens/common (= 4.15.4)
- SocketRocket
- Yoga
- - RNReanimated/worklets/apple (3.19.1):
+ - RNScreens/common (4.15.4):
- boost
- DoubleConversion
- fast_float
@@ -3871,11 +3873,11 @@ PODS:
- React-Fabric
- React-featureflags
- React-graphics
- - React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
+ - React-RCTImage
- React-renderercss
- React-rendererdebug
- React-utils
@@ -3884,7 +3886,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - RNScreens (4.15.4):
+ - RNSentry (7.4.0):
- boost
- DoubleConversion
- fast_float
@@ -3900,21 +3902,21 @@ PODS:
- React-Fabric
- React-featureflags
- React-graphics
+ - React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- - React-RCTImage
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNScreens/common (= 4.15.4)
+ - Sentry/HybridSDK (= 8.57.0)
- SocketRocket
- Yoga
- - RNScreens/common (4.15.4):
+ - RNShare (11.0.2):
- boost
- DoubleConversion
- fast_float
@@ -3934,7 +3936,6 @@ PODS:
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- - React-RCTImage
- React-renderercss
- React-rendererdebug
- React-utils
@@ -3943,7 +3944,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - RNSentry (7.4.0):
+ - RNSound (0.13.0):
- boost
- DoubleConversion
- fast_float
@@ -3959,7 +3960,6 @@ PODS:
- React-Fabric
- React-featureflags
- React-graphics
- - React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
@@ -3970,10 +3970,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - Sentry/HybridSDK (= 8.57.0)
- SocketRocket
- Yoga
- - RNShare (11.0.2):
+ - RNSVG (15.12.1):
- boost
- DoubleConversion
- fast_float
@@ -3999,14 +3998,68 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
+ - RNSVG/common (= 15.12.1)
- SocketRocket
- Yoga
- - RNSound (0.11.2):
+ - RNSVG/common (15.12.1):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
- React-Core
- - RNSound/Core (= 0.11.2)
- - RNSound/Core (0.11.2):
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
+ - RNWorklets (0.6.0):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
- React-Core
- - RNSVG (15.12.1):
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - RNWorklets/worklets (= 0.6.0)
+ - SocketRocket
+ - Yoga
+ - RNWorklets/worklets (0.6.0):
- boost
- DoubleConversion
- fast_float
@@ -4022,6 +4075,7 @@ PODS:
- React-Fabric
- React-featureflags
- React-graphics
+ - React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
@@ -4032,10 +4086,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNSVG/common (= 15.12.1)
+ - RNWorklets/worklets/apple (= 0.6.0)
- SocketRocket
- Yoga
- - RNSVG/common (15.12.1):
+ - RNWorklets/worklets/apple (0.6.0):
- boost
- DoubleConversion
- fast_float
@@ -4051,6 +4105,7 @@ PODS:
- React-Fabric
- React-featureflags
- React-graphics
+ - React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
@@ -4227,6 +4282,7 @@ DEPENDENCIES:
- RNShare (from `../node_modules/react-native-share`)
- RNSound (from `../node_modules/react-native-sound`)
- RNSVG (from `../node_modules/react-native-svg`)
+ - RNWorklets (from `../node_modules/react-native-worklets`)
- SocketRocket (~> 0.7.1)
- VisionCamera (from `../node_modules/react-native-vision-camera`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -4559,6 +4615,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-sound"
RNSVG:
:path: "../node_modules/react-native-svg"
+ RNWorklets:
+ :path: "../node_modules/react-native-worklets"
VisionCamera:
:path: "../node_modules/react-native-vision-camera"
Yoga:
@@ -4735,18 +4793,19 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 9339994ea5d1ff6ad2679b7d0cc3d49053111369
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: 835074fb8fd0afb493c7c2b73076a0f2738a8f63
+ RNLiveMarkdown: 51e46a8300e2eb6f2d3e898b7734b3833532dba6
RNLocalize: 0aa716b7d0f1316ca5f8f16ed42b79b9e3093279
rnmapbox-maps: 870cd752e1d132e05465f3074b582ce6c7e742e9
RNNitroSQLite: d5cf8c550c51015e1ecc93ff6e7509187c6c1c9e
RNPermissions: d507f1fe0ee109c8f6f882808fcf8a173645e40b
RNReactNativeHapticFeedback: 43c09eb41d8321be2e1375cb87ae734e58f677b0
- RNReanimated: 8d0f830cd94d0d9cc86eed5850e567a89807586e
+ RNReanimated: 91d075aaf0c89d51a0708cd64cd6c77f7fa42cdc
RNScreens: 0c5341cc352632758171de6453502a6a757d0369
RNSentry: 34bead6afbfce1bb89837b84da1dec001f890329
RNShare: 385bf6a127cb075d4fdbe7bb85587cad88fe9afe
- RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852
+ RNSound: 9a85701a35e3132484515e73b694610997c7fdfb
RNSVG: 9be2bc57df95a874e8c4b0f7dd71866139f321d2
+ RNWorklets: e752b7443b51916158d3b6ca6700294827e8a1ea
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist
index f3adaae7c9b92..218043c528c95 100644
--- a/ios/ShareViewController/Info.plist
+++ b/ios/ShareViewController/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.2.42
+ 9.2.46CFBundleVersion
- 9.2.42.0
+ 9.2.46.0NSExtensionNSExtensionAttributes
diff --git a/jest/setup.ts b/jest/setup.ts
index 161133fbd6b71..c12d8aa09cf1f 100644
--- a/jest/setup.ts
+++ b/jest/setup.ts
@@ -90,6 +90,7 @@ jest.mock('react-native-reanimated', () => ({
useScrollViewOffset: jest.fn(() => 0),
useAnimatedRef: jest.fn(() => jest.fn()),
LayoutAnimationConfig: jest.fn,
+ makeShareableCloneRecursive: jest.fn,
}));
jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest'));
diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts
index 82226627dfbbb..343703ee2f08a 100644
--- a/jest/setupMockFullstoryLib.ts
+++ b/jest/setupMockFullstoryLib.ts
@@ -17,12 +17,12 @@ export default function mockFSLibrary() {
Page: FSPage,
getChatFSClass: jest.fn(),
init: jest.fn(),
- onReady: jest.fn().mockResolvedValue(undefined),
+ onReady: jest.fn(),
consent: jest.fn(),
identify: jest.fn(),
consentAndIdentify: jest.fn(),
anonymize: jest.fn(),
- getSessionId: jest.fn(),
+ getSessionId: jest.fn().mockResolvedValue(undefined),
};
});
}
diff --git a/modules/ExpensifyNitroUtils/android/CMakeLists.txt b/modules/ExpensifyNitroUtils/android/CMakeLists.txt
index 5cc9576136ec0..0f384e8dd874c 100644
--- a/modules/ExpensifyNitroUtils/android/CMakeLists.txt
+++ b/modules/ExpensifyNitroUtils/android/CMakeLists.txt
@@ -1,29 +1,32 @@
project(ExpensifyNitroUtils)
cmake_minimum_required(VERSION 3.9.0)
-set (PACKAGE_NAME ExpensifyNitroUtils)
-set (CMAKE_VERBOSE_MAKEFILE ON)
-set (CMAKE_CXX_STANDARD 20)
+set(PACKAGE_NAME ExpensifyNitroUtils)
+set(CMAKE_VERBOSE_MAKEFILE ON)
+set(CMAKE_CXX_STANDARD 20)
# Define C++ library and add all sources
add_library(${PACKAGE_NAME} SHARED
- src/main/cpp/cpp-adapter.cpp
+ src/main/cpp/cpp-adapter.cpp
)
-# Add Nitrogen specs :)
+# Add Nitrogen specs
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake)
# Set up local includes
include_directories(
- "src/main/cpp"
- "../cpp"
+ "src/main/cpp"
+ "../cpp"
)
find_library(LOG_LIB log)
+# đ§ Ensure 16KB ELF page alignment
+set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
+
# Link all libraries together
target_link_libraries(
- ${PACKAGE_NAME}
- ${LOG_LIB}
- android # <-- Android core
-)
+ ${PACKAGE_NAME}
+ ${LOG_LIB}
+ android
+)
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index b74e967387b58..2db31eeb8501f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.2.42-0",
+ "version": "9.2.46-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.2.42-0",
+ "version": "9.2.46-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -18,7 +18,7 @@
"@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils",
"@expensify/react-native-background-task": "file:./modules/background-task",
"@expensify/react-native-hybrid-app": "file:./modules/hybrid-app",
- "@expensify/react-native-live-markdown": "0.1.308",
+ "@expensify/react-native-live-markdown": "0.1.310",
"@expensify/react-native-wallet": "0.1.11",
"@expo/metro-runtime": "^6.0.2",
"@firebase/app": "^0.13.2",
@@ -67,7 +67,7 @@
"expensify-common": "2.0.162",
"expo": "54.0.10",
"expo-asset": "12.0.8",
- "expo-av": "^15.1.5",
+ "expo-av": "15.1.7",
"expo-font": "14.0.8",
"expo-image": "3.0.8",
"expo-image-manipulator": "^13.1.5",
@@ -126,13 +126,13 @@
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806",
"react-native-plaid-link-sdk": "12.5.3",
"react-native-qrcode-svg": "6.3.14",
- "react-native-reanimated": "3.19.1",
+ "react-native-reanimated": "4.1.2",
"react-native-release-profiler": "0.4.2",
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "4.15.4",
"react-native-share": "11.0.2",
- "react-native-sound": "^0.11.2",
+ "react-native-sound": "^0.13.0",
"react-native-svg": "15.12.1",
"react-native-tab-view": "^4.1.0",
"react-native-url-polyfill": "^2.0.0",
@@ -140,6 +140,7 @@
"react-native-vision-camera": "^4.7.2",
"react-native-web": "0.21.2",
"react-native-webview": "13.16.0",
+ "react-native-worklets": "0.6.0",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
"react-webcam": "^7.1.1",
@@ -183,10 +184,11 @@
"@react-native/babel-preset": "0.81.4",
"@react-native/metro-config": "0.81.4",
"@react-navigation/devtools": "^6.0.10",
- "@rock-js/platform-android": "0.11.5",
- "@rock-js/platform-ios": "0.11.5",
- "@rock-js/plugin-metro": "0.11.5",
- "@rock-js/provider-github": "0.11.5",
+ "@rock-js/platform-android": "0.11.9",
+ "@rock-js/platform-ios": "0.11.9",
+ "@rock-js/plugin-metro": "0.11.9",
+ "@rock-js/provider-github": "0.11.9",
+ "@sentry/webpack-plugin": "4.6.0",
"@storybook/addon-a11y": "^8.6.9",
"@storybook/addon-essentials": "^8.6.9",
"@storybook/addon-webpack5-compiler-babel": "^3.0.5",
@@ -242,7 +244,7 @@
"electron-builder": "26.0.19",
"eslint": "^9.36.0",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-config-expensify": "2.0.91",
+ "eslint-config-expensify": "2.0.94",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^60.7.0",
@@ -278,7 +280,7 @@
"react-refresh": "^0.14.2",
"react-test-renderer": "19.1.0",
"reassure": "^1.0.0-rc.4",
- "rock": "0.11.5",
+ "rock": "0.11.9",
"semver": "7.5.2",
"setimmediate": "^1.0.5",
"shellcheck": "^1.1.0",
@@ -4191,9 +4193,9 @@
"link": true
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.308",
- "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.308.tgz",
- "integrity": "sha512-KRmWSL2KVidNzaDS4RnyUx68suF6i/UPcaIHHvmK90LhnpmkOa9wUrxO3RIpmkJWf2AYk9BfMtN0fpXLxl/arw==",
+ "version": "0.1.310",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.310.tgz",
+ "integrity": "sha512-3uFLW1YWrliLFYcPzQkSanhyLDaA5MzBQGtvm4bh68aH5KwpHUv9Z3HByDBbIrrTsXc3pkncunIHM0g23KW/aw==",
"license": "MIT",
"workspaces": [
"./example",
@@ -11175,40 +11177,28 @@
}
},
"node_modules/@rock-js/config": {
- "version": "0.11.6",
- "resolved": "https://registry.npmjs.org/@rock-js/config/-/config-0.11.6.tgz",
- "integrity": "sha512-0oWrrlw36avPcd4XdmeR2tfhEt8aJqhBZT+afrsGxzKUYUYUj0Uh/OcGyP0YtqZe6bf70P0Ey95sZHlz6SCcsA==",
+ "version": "0.11.9",
+ "resolved": "https://registry.npmjs.org/@rock-js/config/-/config-0.11.9.tgz",
+ "integrity": "sha512-RMJi3l9cq6VaIacyNQZ8F7pucxukX4iBECp1yMHQx400b5c+fgsBW3O6vaMNugQXBEbqr4j79tI09G0zW3tZ2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
- "@rock-js/provider-github": "^0.11.6",
- "@rock-js/tools": "^0.11.6",
+ "@rock-js/provider-github": "^0.11.9",
+ "@rock-js/tools": "^0.11.9",
"joi": "^17.13.3",
"tslib": "^2.3.0"
}
},
- "node_modules/@rock-js/config/node_modules/@rock-js/provider-github": {
- "version": "0.11.6",
- "resolved": "https://registry.npmjs.org/@rock-js/provider-github/-/provider-github-0.11.6.tgz",
- "integrity": "sha512-EtwHh83XketOVnoOX4CeeRfHLzgmhhfvlslXedYlK6AgtMotPe7XQ1btFV/x473tY8eC/pBHofuc5oCOXKWlJg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@rock-js/tools": "^0.11.6",
- "ts-regex-builder": "^1.8.2",
- "tslib": "^2.3.0"
- }
- },
"node_modules/@rock-js/platform-android": {
- "version": "0.11.5",
- "resolved": "https://registry.npmjs.org/@rock-js/platform-android/-/platform-android-0.11.5.tgz",
- "integrity": "sha512-iY7ssjRg26mkeuWSwJ4RhuzKcd8VaIEmmiFaOr7k0lY8msetu6nczyUo7v1H36W5mBAYt3JMSmojsix6C8y9xA==",
+ "version": "0.11.9",
+ "resolved": "https://registry.npmjs.org/@rock-js/platform-android/-/platform-android-0.11.9.tgz",
+ "integrity": "sha512-SeX4ZXi3fcfv1yMA5GbFozZOyiE5mOpg+aZhpuMLvJt+DVRiRHleegS0zMx548qXC0DJuf1J+pprXQEayF11Ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config-android": "^20.0.0",
- "@rock-js/tools": "^0.11.5",
+ "@rock-js/tools": "^0.11.9",
"adm-zip": "^0.5.16",
"tslib": "^2.3.0"
}
@@ -11322,201 +11312,31 @@
}
},
"node_modules/@rock-js/platform-apple-helpers": {
- "version": "0.11.6",
- "resolved": "https://registry.npmjs.org/@rock-js/platform-apple-helpers/-/platform-apple-helpers-0.11.6.tgz",
- "integrity": "sha512-labmm6uPdp3BjGEtilcrCEwne6tsXAsCTDdj4IlPaqszuEkuwxmKwMyMffFt4qETL8RQDZky6rhx57TBpz8aOg==",
+ "version": "0.11.9",
+ "resolved": "https://registry.npmjs.org/@rock-js/platform-apple-helpers/-/platform-apple-helpers-0.11.9.tgz",
+ "integrity": "sha512-T/+keSlUcZwC2Y1LdRU0EfvKdgrSKkysTOiJy1oR1hYeQruhlslvVTN+45+BCKWjsXUedTfMU+e72cU5YmsYdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config": "^20.0.0",
"@react-native-community/cli-config-apple": "^20.0.0",
- "@rock-js/tools": "^0.11.6",
+ "@rock-js/tools": "^0.11.9",
"adm-zip": "^0.5.16",
"fast-xml-parser": "^4.5.0",
"tslib": "^2.3.0"
}
},
- "node_modules/@rock-js/platform-apple-helpers/node_modules/@react-native-community/cli-config": {
- "version": "20.0.2",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-20.0.2.tgz",
- "integrity": "sha512-OuSAyqTv0MBbRqSyO+80IKasHnwLESydZBTrLjIGwGhDokMH07mZo8Io2H8X300WWa57LC2L8vQf73TzGS3ikQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@react-native-community/cli-tools": "20.0.2",
- "chalk": "^4.1.2",
- "cosmiconfig": "^9.0.0",
- "deepmerge": "^4.3.0",
- "fast-glob": "^3.3.2",
- "joi": "^17.2.1"
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/@react-native-community/cli-config-apple": {
- "version": "20.0.2",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-20.0.2.tgz",
- "integrity": "sha512-6MLL9Duu/JytqI6XfYuc78LSkRGfJoCqTSfqTJzBNSnz6S7XJps9spGBlgvrGh/j0howBpQlFH0J8Ws4N4mCxA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@react-native-community/cli-tools": "20.0.2",
- "chalk": "^4.1.2",
- "execa": "^5.0.0",
- "fast-glob": "^3.3.2"
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/@react-native-community/cli-tools": {
- "version": "20.0.2",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-20.0.2.tgz",
- "integrity": "sha512-bPYhRYggW9IIM8pvrZF/0r6HaxCyEWDn6zfPQPMWlkQUwkzFZ8GBY/M7yiHgDzozWKPT4DqZPumrq806Vcksow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vscode/sudo-prompt": "^9.0.0",
- "appdirsjs": "^1.2.4",
- "chalk": "^4.1.2",
- "execa": "^5.0.0",
- "find-up": "^5.0.0",
- "launch-editor": "^2.9.1",
- "mime": "^2.4.1",
- "ora": "^5.4.1",
- "prompts": "^2.4.2",
- "semver": "^7.5.2"
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
- "license": "Python-2.0"
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/cosmiconfig": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
- "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "env-paths": "^2.2.1",
- "import-fresh": "^3.3.0",
- "js-yaml": "^4.1.0",
- "parse-json": "^5.2.0"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/d-fischer"
- },
- "peerDependencies": {
- "typescript": ">=4.9.5"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/@rock-js/platform-apple-helpers/node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@rock-js/platform-ios": {
- "version": "0.11.5",
- "resolved": "https://registry.npmjs.org/@rock-js/platform-ios/-/platform-ios-0.11.5.tgz",
- "integrity": "sha512-W3JvNEpa/RsOBVBjakihKxWS3ySathmdkQROaj/MZBhfAKupdxCguWzpZW/tvKGXYtfQSMcWFLzwFLj08VkNgQ==",
+ "version": "0.11.9",
+ "resolved": "https://registry.npmjs.org/@rock-js/platform-ios/-/platform-ios-0.11.9.tgz",
+ "integrity": "sha512-RIUnbtS7k6YonUWc/Iz+Q+/oIggoYVIrcU9wElStdItJikTUBNovtKk/pVYbXR5tRZ4N6Hp7w0mvnpMZE/EqRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config-apple": "^20.0.0",
"@react-native-community/cli-types": "^20.0.0",
- "@rock-js/platform-apple-helpers": "^0.11.5",
- "@rock-js/tools": "^0.11.5",
+ "@rock-js/platform-apple-helpers": "^0.11.9",
+ "@rock-js/tools": "^0.11.9",
"tslib": "^2.3.0"
}
},
@@ -11639,14 +11459,14 @@
}
},
"node_modules/@rock-js/plugin-metro": {
- "version": "0.11.5",
- "resolved": "https://registry.npmjs.org/@rock-js/plugin-metro/-/plugin-metro-0.11.5.tgz",
- "integrity": "sha512-xuwfuFZ/2hd7fEk73/MBjUaPGGZcqZKZ6HPEq8n+5q0a7hzNquPZCJhxTA5IMWCWOj4nnCBWVtreeE7eIsJsNQ==",
+ "version": "0.11.9",
+ "resolved": "https://registry.npmjs.org/@rock-js/plugin-metro/-/plugin-metro-0.11.9.tgz",
+ "integrity": "sha512-ZOI477DJs3bnanKph0WrzR7p/Vet2uKPDKq5acUgV6uo35dxDoehITI0JFuEp/eTdBlVBa9noZiBsshxshucmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-server-api": "^20.0.0",
- "@rock-js/tools": "^0.11.5",
+ "@rock-js/tools": "^0.11.9",
"metro": "^0.83.1",
"metro-config": "^0.83.1",
"metro-core": "^0.83.1",
@@ -12190,21 +12010,21 @@
}
},
"node_modules/@rock-js/provider-github": {
- "version": "0.11.5",
- "resolved": "https://registry.npmjs.org/@rock-js/provider-github/-/provider-github-0.11.5.tgz",
- "integrity": "sha512-xXcLJ3GI0eDY2GL3p8Ejzr7myXunV1W+bveCnDBKzggs4n/975nGFxxVqojdc00R9h6kKCCwMHXUiKapcXzLIQ==",
+ "version": "0.11.9",
+ "resolved": "https://registry.npmjs.org/@rock-js/provider-github/-/provider-github-0.11.9.tgz",
+ "integrity": "sha512-fchHu0Zg7oNbI2GfFcwvQIkGZ4/lHYyxRDYdR+iARrhL0zKK1EPCCk/S8kI3pC9Sm33fQhelZbqcIqW6gh6z/A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@rock-js/tools": "^0.11.5",
+ "@rock-js/tools": "^0.11.9",
"ts-regex-builder": "^1.8.2",
"tslib": "^2.3.0"
}
},
"node_modules/@rock-js/tools": {
- "version": "0.11.6",
- "resolved": "https://registry.npmjs.org/@rock-js/tools/-/tools-0.11.6.tgz",
- "integrity": "sha512-UfjUennTNsMlVeJQd6kLx4gYq9NFnNQl0ai2V0Cws/QNX1Zeq1ubgVNdfV9dkVB87Jlji6awJxOIRQAYlgkROQ==",
+ "version": "0.11.9",
+ "resolved": "https://registry.npmjs.org/@rock-js/tools/-/tools-0.11.9.tgz",
+ "integrity": "sha512-tswYFPTethkp2GxIhMVD64zOp4SX7IyFpx3gtH8rw+2gn0EP8cxkG8S+SU0+IwFRVw1Of8yARRyyzq1XVKA04w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12375,6 +12195,298 @@
"node": ">=18"
}
},
+ "node_modules/@sentry/bundler-plugin-core": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.0.tgz",
+ "integrity": "sha512-Fub2XQqrS258jjS8qAxLLU1k1h5UCNJ76i8m4qZJJdogWWaF8t00KnnTyp9TEDJzrVD64tRXS8+HHENxmeUo3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.18.5",
+ "@sentry/babel-plugin-component-annotate": "4.6.0",
+ "@sentry/cli": "^2.57.0",
+ "dotenv": "^16.3.1",
+ "find-up": "^5.0.0",
+ "glob": "^9.3.2",
+ "magic-string": "0.30.8",
+ "unplugin": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/babel-plugin-component-annotate": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.0.tgz",
+ "integrity": "sha512-3soTX50JPQQ51FSbb4qvNBf4z/yP7jTdn43vMTp9E4IxvJ9HKJR7OEuKkCMszrZmWsVABXl02msqO7QisePdiQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.57.0.tgz",
+ "integrity": "sha512-oC4HPrVIX06GvUTgK0i+WbNgIA9Zl5YEcwf9N4eWFJJmjonr2j4SML9Hn2yNENbUWDgwepy4MLod3P8rM4bk/w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "https-proxy-agent": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "progress": "^2.0.3",
+ "proxy-from-env": "^1.1.0",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "sentry-cli": "bin/sentry-cli"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@sentry/cli-darwin": "2.57.0",
+ "@sentry/cli-linux-arm": "2.57.0",
+ "@sentry/cli-linux-arm64": "2.57.0",
+ "@sentry/cli-linux-i686": "2.57.0",
+ "@sentry/cli-linux-x64": "2.57.0",
+ "@sentry/cli-win32-arm64": "2.57.0",
+ "@sentry/cli-win32-i686": "2.57.0",
+ "@sentry/cli-win32-x64": "2.57.0"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-darwin": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.57.0.tgz",
+ "integrity": "sha512-v1wYQU3BcCO+Z3OVxxO+EnaW4oQhuOza6CXeYZ0z5ftza9r0QQBLz3bcZKTVta86xraNm0z8GDlREwinyddOxQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-arm": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.57.0.tgz",
+ "integrity": "sha512-uNHB8xyygqfMd1/6tFzl9NUkuVefg7jdZtM/vVCQVaF/rJLWZ++Wms+LLhYyKXKN8yd7J9wy7kTEl4Qu4jWbGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd",
+ "android"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-arm64": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.57.0.tgz",
+ "integrity": "sha512-Kh1jTsMV5Fy/RvB381N/woXe1qclRMqsG6kM3Gq6m6afEF/+k3PyQdNW3HXAola6d63EptokLtxPG2xjWQ+w9Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd",
+ "android"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-i686": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.57.0.tgz",
+ "integrity": "sha512-EYXghoK/tKd0zqz+KD/ewXXE3u1HLCwG89krweveytBy/qw7M5z58eFvw+iGb1Vnbl1f/fRD0G4E0AbEsPfmpg==",
+ "cpu": [
+ "x86",
+ "ia32"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd",
+ "android"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-x64": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.57.0.tgz",
+ "integrity": "sha512-CyZrP/ssHmAPLSzfd4ydy7icDnwmDD6o3QjhkWwVFmCd+9slSBMQxpIqpamZmrWE6X4R+xBRbSUjmdoJoZ5yMw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd",
+ "android"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-win32-arm64": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.57.0.tgz",
+ "integrity": "sha512-wji/GGE4Lh5I/dNCsuVbg6fRvttvZRG6db1yPW1BSvQRh8DdnVy1CVp+HMqSq0SRy/S4z60j2u+m4yXMoCL+5g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-win32-i686": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.57.0.tgz",
+ "integrity": "sha512-hWvzyD7bTPh3b55qvJ1Okg3Wbl0Km8xcL6KvS7gfBl6uss+I6RldmQTP0gJKdHSdf/QlJN1FK0b7bLnCB3wHsg==",
+ "cpu": [
+ "x86",
+ "ia32"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-win32-x64": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.57.0.tgz",
+ "integrity": "sha512-QWYV/Y0sbpDSTyA4XQBOTaid4a6H2Iwa1Z8UI+qNxFlk0ADSEgIqo2NrRHDU8iRnghTkecQNX1NTt/7mXN3f/A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/glob": {
+ "version": "9.3.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
+ "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "minimatch": "^8.0.2",
+ "minipass": "^4.2.4",
+ "path-scurry": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": {
+ "version": "0.30.8",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+ "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz",
+ "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/minipass": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz",
+ "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/unplugin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
+ "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.8.1",
+ "chokidar": "^3.5.3",
+ "webpack-sources": "^3.2.3",
+ "webpack-virtual-modules": "^0.5.0"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/webpack-virtual-modules": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
+ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@sentry/cli": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.56.1.tgz",
@@ -12605,6 +12717,58 @@
"node": ">=18"
}
},
+ "node_modules/@sentry/webpack-plugin": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.6.0.tgz",
+ "integrity": "sha512-i9Yy2kXCbFKlRST09fV1HsI0naJAfeXxoiUPyh5iCgSo2w7ZwEUlk0tJhupnHZzfSa3OSg01+vVNeeyLYM4tdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/bundler-plugin-core": "4.6.0",
+ "unplugin": "1.0.1",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "webpack": ">=4.40.0"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin/node_modules/unplugin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
+ "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.8.1",
+ "chokidar": "^3.5.3",
+ "webpack-sources": "^3.2.3",
+ "webpack-virtual-modules": "^0.5.0"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin/node_modules/webpack-virtual-modules": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
+ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@shopify/flash-list": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.0.3.tgz",
@@ -17296,9 +17460,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
- "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==",
+ "version": "2.8.21",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz",
+ "integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -17831,6 +17995,19 @@
"node": ">=8"
}
},
+ "node_modules/builtin-modules": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
+ "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/bundle-name": {
"version": "4.1.0",
"dev": true,
@@ -18097,9 +18274,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001743",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz",
- "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==",
+ "version": "1.0.30001751",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
+ "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
"funding": [
{
"type": "opencollective",
@@ -18175,6 +18352,13 @@
"node": ">=0.8.0"
}
},
+ "node_modules/change-case": {
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
+ "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/char-regex": {
"version": "1.0.2",
"dev": true,
@@ -18332,6 +18516,29 @@
"node": ">=0.10.0"
}
},
+ "node_modules/clean-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
+ "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/clean-regexp/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -19098,16 +19305,51 @@
}
},
"node_modules/core-js-compat": {
- "version": "3.38.1",
+ "version": "3.46.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
+ "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==",
"license": "MIT",
"dependencies": {
- "browserslist": "^4.23.3"
+ "browserslist": "^4.26.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
+ "node_modules/core-js-compat/node_modules/browserslist": {
+ "version": "4.27.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
+ "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.19",
+ "caniuse-lite": "^1.0.30001751",
+ "electron-to-chromium": "^1.5.238",
+ "node-releases": "^2.0.26",
+ "update-browserslist-db": "^1.1.4"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
"node_modules/core-js-pure": {
"version": "3.38.1",
"dev": true,
@@ -20681,9 +20923,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.222",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz",
- "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==",
+ "version": "1.5.243",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.243.tgz",
+ "integrity": "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==",
"license": "ISC"
},
"node_modules/electron-winstaller": {
@@ -21359,9 +21601,9 @@
}
},
"node_modules/eslint-config-expensify": {
- "version": "2.0.91",
- "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.91.tgz",
- "integrity": "sha512-sdVa7o7Cl9JIV2UiENQTue9nlt8sc45B7SyiKdtDizajY7tMK402PEa9ngPIkRt/0xzKOFC+jtPy8pMXg6OSeg==",
+ "version": "2.0.94",
+ "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.94.tgz",
+ "integrity": "sha512-jw+p9geQ8F+xiDnxzN5KtgYs7yGmpw7uLDkMkzMm/YzG/uhvNRShuMxqSMv7SZfLHXgjgF77BmNe+zNKHSTFpA==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -21378,6 +21620,7 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-rulesdir": "^0.2.2",
+ "eslint-plugin-unicorn": "^61.0.2",
"globals": "^15.14.0",
"lodash": "^4.17.21",
"underscore": "^1.13.6"
@@ -21945,6 +22188,123 @@
"eslint": "^8.57.0 || ^9.0.0"
}
},
+ "node_modules/eslint-plugin-unicorn": {
+ "version": "61.0.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-61.0.2.tgz",
+ "integrity": "sha512-zLihukvneYT7f74GNbVJXfWIiNQmkc/a9vYBTE4qPkQZswolWNdu+Wsp9sIXno1JOzdn6OUwLPd19ekXVkahRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@eslint/plugin-kit": "^0.3.3",
+ "change-case": "^5.4.4",
+ "ci-info": "^4.3.0",
+ "clean-regexp": "^1.0.0",
+ "core-js-compat": "^3.44.0",
+ "esquery": "^1.6.0",
+ "find-up-simple": "^1.0.1",
+ "globals": "^16.3.0",
+ "indent-string": "^5.0.0",
+ "is-builtin-module": "^5.0.0",
+ "jsesc": "^3.1.0",
+ "pluralize": "^8.0.0",
+ "regexp-tree": "^0.1.27",
+ "regjsparser": "^0.12.0",
+ "semver": "^7.7.2",
+ "strip-indent": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.10.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
+ },
+ "peerDependencies": {
+ "eslint": ">=9.29.0"
+ }
+ },
+ "node_modules/eslint-plugin-unicorn/node_modules/ci-info": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
+ "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint-plugin-unicorn/node_modules/globals": {
+ "version": "16.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
+ "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-plugin-unicorn/node_modules/indent-string": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+ "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-plugin-unicorn/node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/eslint-plugin-unicorn/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-plugin-unicorn/node_modules/strip-indent": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz",
+ "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/eslint-plugin-you-dont-need-lodash-underscore": {
"version": "6.14.0",
"dev": true,
@@ -22405,7 +22765,9 @@
}
},
"node_modules/expo-av": {
- "version": "15.1.5",
+ "version": "15.1.7",
+ "resolved": "https://registry.npmjs.org/expo-av/-/expo-av-15.1.7.tgz",
+ "integrity": "sha512-NC+JR+65sxXfQN1mOHp3QBaXTL2J+BzNwVO27XgUEc5s9NaoBTdHWElYXrfxvik6xwytZ+a7abrqfNNgsbQzsA==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
@@ -23684,6 +24046,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/find-up-simple": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
+ "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/find-up/node_modules/path-exists": {
"version": "4.0.0",
"license": "MIT",
@@ -24133,9 +24508,9 @@
}
},
"node_modules/fs-fingerprint/node_modules/p-limit": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.1.1.tgz",
- "integrity": "sha512-i8PyM2JnsNChVSYWLr2BAjNoLi0BAYC+wecOnZnVV+YSNJkzP7cWmvI34dk0WArWfH9KwBHNoZI3P3MppImlIA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.2.0.tgz",
+ "integrity": "sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -25598,6 +25973,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-builtin-module": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
+ "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "builtin-modules": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-callable": {
"version": "1.2.7",
"license": "MIT",
@@ -30710,9 +31101,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.21",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
- "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
+ "version": "2.0.26",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
+ "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==",
"license": "MIT"
},
"node_modules/node-stream-zip": {
@@ -31946,6 +32337,16 @@
"node": ">=10.4.0"
}
},
+ "node_modules/pluralize": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/pngjs": {
"version": "5.0.0",
"license": "MIT",
@@ -33061,7 +33462,9 @@
"license": "MIT"
},
"node_modules/react-native-is-edge-to-edge": {
- "version": "1.1.7",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
+ "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==",
"license": "MIT",
"peerDependencies": {
"react": "*",
@@ -33100,14 +33503,6 @@
"react-native-reanimated": ">=3.0.0"
}
},
- "node_modules/react-native-keyboard-controller/node_modules/react-native-is-edge-to-edge": {
- "version": "1.2.1",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
"node_modules/react-native-launch-arguments": {
"version": "4.0.2",
"license": "MIT",
@@ -33503,26 +33898,31 @@
}
},
"node_modules/react-native-reanimated": {
- "version": "3.19.1",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.2.tgz",
+ "integrity": "sha512-qzmQiFrvjm62pRBcj97QI9Xckc3EjgHQoY1F2yjktd0kpjhoyePeuTEXjYRCAVIy7IV/1cfeSup34+zFThFoHQ==",
"license": "MIT",
"dependencies": {
- "@babel/plugin-transform-arrow-functions": "^7.0.0-0",
- "@babel/plugin-transform-class-properties": "^7.0.0-0",
- "@babel/plugin-transform-classes": "^7.0.0-0",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0",
- "@babel/plugin-transform-optional-chaining": "^7.0.0-0",
- "@babel/plugin-transform-shorthand-properties": "^7.0.0-0",
- "@babel/plugin-transform-template-literals": "^7.0.0-0",
- "@babel/plugin-transform-unicode-regex": "^7.0.0-0",
- "@babel/preset-typescript": "^7.16.7",
- "convert-source-map": "^2.0.0",
- "invariant": "^2.2.4",
- "react-native-is-edge-to-edge": "1.1.7"
+ "react-native-is-edge-to-edge": "^1.2.1",
+ "semver": "7.7.2"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0",
"react": "*",
- "react-native": "*"
+ "react-native": "*",
+ "react-native-worklets": ">=0.5.0"
+ }
+ },
+ "node_modules/react-native-reanimated/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/react-native-release-profiler": {
@@ -33665,16 +34065,6 @@
"react-native": "*"
}
},
- "node_modules/react-native-screens/node_modules/react-native-is-edge-to-edge": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
- "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
"node_modules/react-native-share": {
"version": "11.0.2",
"license": "MIT",
@@ -33683,10 +34073,13 @@
}
},
"node_modules/react-native-sound": {
- "version": "0.11.2",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.13.0.tgz",
+ "integrity": "sha512-SnREzaV0fmpYNuDV1Y8M7FutmaYei0pKBgpldULKKJMkoA3DBv5ppyRxY+oxRQ7HwEpt6LsonrKgM+13GH/tCw==",
"license": "MIT",
"peerDependencies": {
- "react-native": ">=0.8.0"
+ "react": "*",
+ "react-native": "*"
}
},
"node_modules/react-native-svg": {
@@ -33806,6 +34199,42 @@
"react-native": "*"
}
},
+ "node_modules/react-native-worklets": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.6.0.tgz",
+ "integrity": "sha512-yETMNuCcivdYWteuG4eRqgiAk2DzRCrVAaEBIEWPo4emrf3BNjadFo85L5QvyEusrX9QKE3ZEAx8U5A/nbyFgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-transform-arrow-functions": "^7.0.0-0",
+ "@babel/plugin-transform-class-properties": "^7.0.0-0",
+ "@babel/plugin-transform-classes": "^7.0.0-0",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0",
+ "@babel/plugin-transform-optional-chaining": "^7.0.0-0",
+ "@babel/plugin-transform-shorthand-properties": "^7.0.0-0",
+ "@babel/plugin-transform-template-literals": "^7.0.0-0",
+ "@babel/plugin-transform-unicode-regex": "^7.0.0-0",
+ "@babel/preset-typescript": "^7.16.7",
+ "convert-source-map": "^2.0.0",
+ "semver": "7.7.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-worklets/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-native/node_modules/@react-native/normalize-colors": {
"version": "0.81.4",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.4.tgz",
@@ -34119,6 +34548,16 @@
"@babel/runtime": "^7.8.4"
}
},
+ "node_modules/regexp-tree": {
+ "version": "0.1.27",
+ "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
+ "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "regexp-tree": "bin/regexp-tree"
+ }
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -34150,6 +34589,32 @@
"url": "https://github.com/sponsors/mysticatea"
}
},
+ "node_modules/regjsparser": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
+ "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~3.0.2"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/regjsparser/node_modules/jsesc": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/relateurl": {
"version": "0.2.7",
"dev": true,
@@ -34454,15 +34919,15 @@
"license": "BSD-3-Clause"
},
"node_modules/rock": {
- "version": "0.11.5",
- "resolved": "https://registry.npmjs.org/rock/-/rock-0.11.5.tgz",
- "integrity": "sha512-VXNPnEOQam1K2zVoKyL0vpPXFIU1jCi7vWVRDHz6DjV9g8/5dlJoWX5/lyOI5r5295Z1R86tZTu7104huIFClw==",
+ "version": "0.11.9",
+ "resolved": "https://registry.npmjs.org/rock/-/rock-0.11.9.tgz",
+ "integrity": "sha512-unehLkQIzyK/Py7ZaCuFUpx4sIbbFcnzfbLh0R50VVauCvnmMLy3brEm42ezrY2O6W2v5kzvxHO/dHdCZmtWxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config": "^20.0.0",
- "@rock-js/config": "^0.11.5",
- "@rock-js/tools": "^0.11.5",
+ "@rock-js/config": "^0.11.9",
+ "@rock-js/tools": "^0.11.9",
"adm-zip": "^0.5.16",
"commander": "^12.1.0",
"tar": "^7.5.1",
@@ -37607,9 +38072,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
- "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
"funding": [
{
"type": "opencollective",
diff --git a/package.json b/package.json
index 02ce3999d504b..ebada6a8dc352 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.2.42-0",
+ "version": "9.2.46-0",
"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.",
@@ -47,7 +47,7 @@
"test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand",
"perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure",
"typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc",
- "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=154 --cache --cache-location=node_modules/.cache/eslint",
+ "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=128 --cache --cache-location=node_modules/.cache/eslint",
"lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh",
"lint-watch": "npx eslint-watch --watch --changed",
"shellcheck": "./scripts/shellCheck.sh",
@@ -88,7 +88,7 @@
"@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils",
"@expensify/react-native-background-task": "file:./modules/background-task",
"@expensify/react-native-hybrid-app": "file:./modules/hybrid-app",
- "@expensify/react-native-live-markdown": "0.1.308",
+ "@expensify/react-native-live-markdown": "0.1.310",
"@expensify/react-native-wallet": "0.1.11",
"@expo/metro-runtime": "^6.0.2",
"@firebase/app": "^0.13.2",
@@ -137,7 +137,7 @@
"expensify-common": "2.0.162",
"expo": "54.0.10",
"expo-asset": "12.0.8",
- "expo-av": "^15.1.5",
+ "expo-av": "15.1.7",
"expo-font": "14.0.8",
"expo-image": "3.0.8",
"expo-image-manipulator": "^13.1.5",
@@ -196,13 +196,13 @@
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806",
"react-native-plaid-link-sdk": "12.5.3",
"react-native-qrcode-svg": "6.3.14",
- "react-native-reanimated": "3.19.1",
+ "react-native-reanimated": "4.1.2",
"react-native-release-profiler": "0.4.2",
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "4.15.4",
"react-native-share": "11.0.2",
- "react-native-sound": "^0.11.2",
+ "react-native-sound": "^0.13.0",
"react-native-svg": "15.12.1",
"react-native-tab-view": "^4.1.0",
"react-native-url-polyfill": "^2.0.0",
@@ -210,6 +210,7 @@
"react-native-vision-camera": "^4.7.2",
"react-native-web": "0.21.2",
"react-native-webview": "13.16.0",
+ "react-native-worklets": "0.6.0",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
"react-webcam": "^7.1.1",
@@ -253,10 +254,11 @@
"@react-native/babel-preset": "0.81.4",
"@react-native/metro-config": "0.81.4",
"@react-navigation/devtools": "^6.0.10",
- "@rock-js/platform-android": "0.11.5",
- "@rock-js/platform-ios": "0.11.5",
- "@rock-js/plugin-metro": "0.11.5",
- "@rock-js/provider-github": "0.11.5",
+ "@rock-js/platform-android": "0.11.9",
+ "@rock-js/platform-ios": "0.11.9",
+ "@rock-js/plugin-metro": "0.11.9",
+ "@rock-js/provider-github": "0.11.9",
+ "@sentry/webpack-plugin": "4.6.0",
"@storybook/addon-a11y": "^8.6.9",
"@storybook/addon-essentials": "^8.6.9",
"@storybook/addon-webpack5-compiler-babel": "^3.0.5",
@@ -312,7 +314,7 @@
"electron-builder": "26.0.19",
"eslint": "^9.36.0",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-config-expensify": "2.0.91",
+ "eslint-config-expensify": "2.0.94",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^60.7.0",
@@ -348,7 +350,7 @@
"react-refresh": "^0.14.2",
"react-test-renderer": "19.1.0",
"reassure": "^1.0.0-rc.4",
- "rock": "0.11.5",
+ "rock": "0.11.9",
"semver": "7.5.2",
"setimmediate": "^1.0.5",
"shellcheck": "^1.1.0",
diff --git a/patches/@rock-js/platform-apple-helpers/@rock-js+platform-apple-helpers+0.11.6+001+fix-ios-remote-builds.patch b/patches/@rock-js/platform-apple-helpers/@rock-js+platform-apple-helpers+0.11.6+001+fix-ios-remote-builds.patch
deleted file mode 100644
index e8fd8e5ca8607..0000000000000
--- a/patches/@rock-js/platform-apple-helpers/@rock-js+platform-apple-helpers+0.11.6+001+fix-ios-remote-builds.patch
+++ /dev/null
@@ -1,17 +0,0 @@
-diff --git a/node_modules/@rock-js/platform-apple-helpers/dist/src/lib/utils/pods.js b/node_modules/@rock-js/platform-apple-helpers/dist/src/lib/utils/pods.js
-index d08747d..f3ed060 100644
---- a/node_modules/@rock-js/platform-apple-helpers/dist/src/lib/utils/pods.js
-+++ b/node_modules/@rock-js/platform-apple-helpers/dist/src/lib/utils/pods.js
-@@ -81,10 +81,8 @@ async function runPodInstall(options) {
- env: {
- RCT_NEW_ARCH_ENABLED: options.newArch ? '1' : '0',
- RCT_IGNORE_PODS_DEPRECATION: '1',
-- RCT_USE_RN_DEP: process.env['RCT_USE_RN_DEP'] || usePrebuiltReactNative ? '1' : '0',
-- RCT_USE_PREBUILT_RNCORE: process.env['RCT_USE_PREBUILT_RNCORE'] || usePrebuiltReactNative
-- ? '1'
-- : '0',
-+ RCT_USE_RN_DEP: '0',
-+ RCT_USE_PREBUILT_RNCORE: '0',
- ...(options.brownfield && { USE_FRAMEWORKS: 'static' }),
- ...(process.env['USE_THIRD_PARTY_JSC'] && {
- USE_THIRD_PARTY_JSC: process.env['USE_THIRD_PARTY_JSC'],
diff --git a/patches/@rock-js/platform-apple-helpers/details.md b/patches/@rock-js/platform-apple-helpers/details.md
deleted file mode 100644
index 041422feece84..0000000000000
--- a/patches/@rock-js/platform-apple-helpers/details.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# `@rock-js/platform-apple-helpers` patches
-
-### [@rock-js+platform-apple-helpers+0.11.6+001+fix-ios-remote-builds.patch](@rock-js+platform-apple-helpers+0.11.6+001+fix-ios-remote-builds.patch)
-
-- Reason:
-
- ```
- Remote builds are failing due to bug in setting env variables in rock - https://github.com/callstackincubator/rock/blob/2274e6bf895a4bf456a43e795af4bd5166c09463/packages/platform-apple-helpers/src/lib/utils/pods.ts#L136
- This patch temporary sets `RCT_USE_RN_DEP` and `RCT_USE_PREBUILT_RNCORE` to `0` until real fix is applied in the upstream repo
- ```
-
-- Upstream PR/issue: N/A I will create an upstream PR once those changes are merged
-- E/App issue: N/A patch will be removed very soon
-- PR introducing patch: https://github.com/Expensify/App/pull/73829
diff --git a/patches/expo-av/details.md b/patches/expo-av/details.md
index 76ed78d13050f..e24511e138a1d 100644
--- a/patches/expo-av/details.md
+++ b/patches/expo-av/details.md
@@ -1,6 +1,6 @@
# `expo-av` patches
-### [expo-av+15.1.5+001+fix-blank-screen-android.patch](expo-av+15.1.5+001+fix-blank-screen-android.patch)
+### [expo-av+15.1.7+001+fix-blank-screen-android.patch](expo-av+15.1.7+001+fix-blank-screen-android.patch)
- Reason:
@@ -13,7 +13,7 @@
- PR introducing patch: https://github.com/Expensify/App/pull/56302
-### [expo-av+15.1.5+002+handle-unsupported-videos-ios.patch](expo-av+15.1.5+002+handle-unsupported-videos-ios.patch)
+### [expo-av+15.1.7+002+handle-unsupported-videos-ios.patch](expo-av+15.1.7+002+handle-unsupported-videos-ios.patch)
- Reason:
diff --git a/patches/expo-av/expo-av+15.1.5+001+fix-blank-screen-android.patch b/patches/expo-av/expo-av+15.1.7+001+fix-blank-screen-android.patch
similarity index 100%
rename from patches/expo-av/expo-av+15.1.5+001+fix-blank-screen-android.patch
rename to patches/expo-av/expo-av+15.1.7+001+fix-blank-screen-android.patch
diff --git a/patches/expo-av/expo-av+15.1.5+002+handle-unsupported-videos-ios.patch b/patches/expo-av/expo-av+15.1.7+002+handle-unsupported-videos-ios.patch
similarity index 100%
rename from patches/expo-av/expo-av+15.1.5+002+handle-unsupported-videos-ios.patch
rename to patches/expo-av/expo-av+15.1.7+002+handle-unsupported-videos-ios.patch
diff --git a/patches/react-native-reanimated/details.md b/patches/react-native-reanimated/details.md
index fb80b770dd636..5762a8e682823 100644
--- a/patches/react-native-reanimated/details.md
+++ b/patches/react-native-reanimated/details.md
@@ -1,37 +1,9 @@
# `react-native-reanimated` patches
-### [react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch](react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch)
+### [react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch](react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch)
- Reason: Reanimated wasn't able to catch an exception here, so the catch clause was broadened.
- Upstream PR/issue: đ
- E/App issue: đ
-- PR Introducing Patch: [Upgrade to React Native 0.76](https://github.com/Expensify/App/pull/51475)
-
-### [react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch](react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch)
-
-- Reason: In Expensify `text` prop in a JS prop and not in native code. Recheck if this is still needed when migrating to v4.
-- Upstream PR/issue: đ
-- E/App issue: đ
-- PR Introducing Patch: [NR 0.75 upgrade](https://github.com/Expensify/App/pull/45289)
-
-### [react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch](react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch)
-
-- Reason: The Easing.bezier animation doesn't work on web
-- Upstream PR/issue: https://github.com/software-mansion/react-native-reanimated/pull/8049
-- E/App issue: https://github.com/Expensify/App/pull/63623
-- PR Introducing Patch: đ
-
-### [react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch](react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch)
-
-- Reason: The layout animation callbacks were not called when Reduce Motion accessibility setting was enabled on mobile devices (on native apps). This caused the app to be unresponsive after opening a modal.
-- Upstream PR/issue: https://github.com/software-mansion/react-native-reanimated/pull/8142
-- E/App issue: https://github.com/Expensify/App/issues/69190
-- PR Introducing Patch: https://github.com/Expensify/App/pull/69444
-
-### [react-native-reanimated+3.19.1+005+fix-broken-slideInUp-animation.patch](react-native-reanimated+3.19.1+005+fix-broken-slideInUp-animation.patch)
-
-- Reason: `SlideInUp` animation is not working correctly with React Native 0.81. The fix is already present in reanimated v4, but has not been backported yet
-- Upstream PR/issue: https://github.com/software-mansion/react-native-reanimated/pull/8089
-- E/App issue:
-- PR Introducing Patch: https://github.com/Expensify/App/pull/69535
+- PR Introducing Patch: [Upgrade to React Native 0.76](https://github.com/Expensify/App/pull/51475)
\ No newline at end of file
diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch b/patches/react-native-reanimated/react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch
deleted file mode 100644
index 583cc7015ee44..0000000000000
--- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch
+++ /dev/null
@@ -1,12 +0,0 @@
-diff --git a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
-index d4b31f2..ced6561 100644
---- a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
-+++ b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
-@@ -46,7 +46,6 @@ function createCircularDoublesBuffer(size: number) {
- }
-
- const DEFAULT_BUFFER_SIZE = 20;
--addWhitelistedNativeProps({ text: true });
- const AnimatedTextInput = createAnimatedComponent(TextInput);
-
- function loopAnimationFrame(fn: (lastTime: number, time: number) => void) {
diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch b/patches/react-native-reanimated/react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch
deleted file mode 100644
index b623fc7f758e9..0000000000000
--- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch
+++ /dev/null
@@ -1,62 +0,0 @@
-diff --git a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js
-index ed0f9d3..6baf136 100644
---- a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js
-+++ b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js
-@@ -14,4 +14,18 @@ export const WebEasings = {
- export function getEasingByName(easingName) {
- return `cubic-bezier(${WebEasings[easingName].toString()})`;
- }
--//# sourceMappingURL=Easing.web.js.map
-\ No newline at end of file
-+export function maybeGetBezierEasing(easing) {
-+ if (!('factory' in easing)) {
-+ return null;
-+ }
-+ const easingFactory = easing.factory;
-+ if (!('__closure' in easingFactory)) {
-+ return null;
-+ }
-+ const closure = easingFactory.__closure;
-+ if (!('Bezier' in closure)) {
-+ return null;
-+ }
-+ return `cubic-bezier(${closure.x1}, ${closure.y1}, ${closure.x2}, ${closure.y2})`;
-+}
-+
-diff --git a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js
-index 7f724c4..ad53a74 100644
---- a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js
-+++ b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js
-@@ -10,18 +10,28 @@ import { setElementPosition, snapshots } from "./componentStyle.js";
- import { Animations, TransitionType } from "./config.js";
- import { TransitionGenerator } from "./createAnimation.js";
- import { scheduleAnimationCleanup } from "./domUtils.js";
--import { getEasingByName, WebEasings } from "./Easing.web.js";
-+import {
-+ getEasingByName,
-+ maybeGetBezierEasing,
-+ WebEasings
-+} from "./Easing.web.js";
- import { prepareCurvedTransition } from "./transition/Curved.web.js";
- function getEasingFromConfig(config) {
- if (!config.easingV) {
- return getEasingByName('linear');
- }
- const easingName = config.easingV[EasingNameSymbol];
-- if (!(easingName in WebEasings)) {
-- logger.warn(`Selected easing is not currently supported on web.`);
-+ if (easingName in WebEasings) {
-+ return getEasingByName(easingName);
-+ }
-+ const bezierEasing = maybeGetBezierEasing(config.easingV);
-+ if (!bezierEasing) {
-+ logger.warn(
-+ `Selected easing is not currently supported on web. Using linear easing instead.`
-+ );
- return getEasingByName('linear');
- }
-- return getEasingByName(easingName);
-+ return bezierEasing;
- }
- function getRandomDelay(maxDelay = 1000) {
- return Math.floor(Math.random() * (maxDelay + 1)) / 1000;
diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch b/patches/react-native-reanimated/react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch
deleted file mode 100644
index 03310811d5df8..0000000000000
--- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch
+++ /dev/null
@@ -1,33 +0,0 @@
-diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
-index a2f6cf1..93a37f5 100644
---- a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
-+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
-@@ -503,13 +503,6 @@ export function createAnimatedComponent(
- return;
- }
-
-- if (this._isReducedMotion(currentConfig)) {
-- if (!previousConfig) {
-- return;
-- }
-- currentConfig = undefined;
-- }
--
- updateLayoutAnimations(
- isFabric() && type === LayoutAnimationType.ENTERING
- ? this.reanimatedID
-@@ -608,14 +601,6 @@ export function createAnimatedComponent(
- },
- });
-
-- _isReducedMotion(config?: LayoutAnimationOrBuilder): boolean {
-- return config &&
-- 'getReduceMotion' in config &&
-- typeof config.getReduceMotion === 'function'
-- ? getReduceMotionFromConfig(config.getReduceMotion())
-- : getReduceMotionFromConfig();
-- }
--
- // This is a component lifecycle method from React, therefore we are not calling it directly.
- // It is called before the component gets rerendered. This way we can access components' position before it changed
- // and later on, in componentDidUpdate, calculate translation for layout transition.
diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+005+fix-broken-slideInUp-animation.patch b/patches/react-native-reanimated/react-native-reanimated+3.19.1+005+fix-broken-slideInUp-animation.patch
deleted file mode 100644
index 2e4e06749b85a..0000000000000
--- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+005+fix-broken-slideInUp-animation.patch
+++ /dev/null
@@ -1,11 +0,0 @@
-diff --git a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/CMakeLists.txt b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/CMakeLists.txt
-index c61b03a..a3a3428 100644
---- a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/CMakeLists.txt
-+++ b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/CMakeLists.txt
-@@ -36,3 +36,6 @@ if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)
- else()
- target_link_libraries(reanimated ReactAndroid::react_nativemodule_core)
- endif()
-+
-+include("${REACT_NATIVE_DIR}/ReactCommon/cmake-utils/react-native-flags.cmake")
-+target_compile_reactnative_options(reanimated PUBLIC)
diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch b/patches/react-native-reanimated/react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch
similarity index 87%
rename from patches/react-native-reanimated/react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch
rename to patches/react-native-reanimated/react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch
index 2fdf25db7f640..4e168c22a926b 100644
--- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch
+++ b/patches/react-native-reanimated/react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch
@@ -1,8 +1,8 @@
diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp
-index 8102462..f2738d2 100644
+index 6574d2d..8cb9b2f 100644
--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp
+++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp
-@@ -853,7 +853,7 @@ void LayoutAnimationsProxy::transferConfigFromNativeID(
+@@ -805,7 +805,7 @@ void LayoutAnimationsProxy::transferConfigFromNativeID(
auto nativeId = stoi(nativeIdString);
layoutAnimationsManager_->transferConfigFromNativeID(nativeId, tag);
} catch (std::invalid_argument) {
@@ -10,4 +10,4 @@ index 8102462..f2738d2 100644
+ } catch (...) {
}
}
-
+
diff --git a/patches/react-native-sound/details.md b/patches/react-native-sound/details.md
deleted file mode 100644
index a740ef9c0b0d8..0000000000000
--- a/patches/react-native-sound/details.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# `react-native-sound` patches
-
-### [react-native-sound+0.11.2+001+app-sounds.patch](react-native-sound+0.11.2+001+app-sounds.patch)
-
-- Reason:
-
- ```
- This patch makes react-native-web-sound lib build with App.
- ```
-
-- Upstream PR/issue: đ, commented in the App PR https://github.com/Expensify/App/pull/31055#issuecomment-3346258817
-- E/App issue: https://github.com/Expensify/App/issues/29835
-- PR introducing patch: https://github.com/Expensify/App/pull/31055
\ No newline at end of file
diff --git a/patches/react-native-sound/react-native-sound+0.11.2+001+app-sounds.patch b/patches/react-native-sound/react-native-sound+0.11.2+001+app-sounds.patch
deleted file mode 100644
index 661e39263c430..0000000000000
--- a/patches/react-native-sound/react-native-sound+0.11.2+001+app-sounds.patch
+++ /dev/null
@@ -1,38 +0,0 @@
-diff --git a/node_modules/react-native-sound/RNSound/RNSound.h b/node_modules/react-native-sound/RNSound/RNSound.h
-index 7f5b97b..1a3c840 100644
---- a/node_modules/react-native-sound/RNSound/RNSound.h
-+++ b/node_modules/react-native-sound/RNSound/RNSound.h
-@@ -1,17 +1,7 @@
--#if __has_include()
- #import
--#else
--#import "RCTBridgeModule.h"
--#endif
--
- #import
--
--#if __has_include()
- #import
--#else
--#import "RCTEventEmitter.h"
--#endif
-
- @interface RNSound : RCTEventEmitter
--@property (nonatomic, weak) NSNumber *_key;
-+@property(nonatomic, weak) NSNumber *_key;
- @end
-diff --git a/node_modules/react-native-sound/RNSound/RNSound.m b/node_modules/react-native-sound/RNSound/RNSound.m
-index df3784e..d34ac01 100644
---- a/node_modules/react-native-sound/RNSound/RNSound.m
-+++ b/node_modules/react-native-sound/RNSound/RNSound.m
-@@ -1,10 +1,6 @@
- #import "RNSound.h"
-
--#if __has_include("RCTUtils.h")
--#import "RCTUtils.h"
--#else
- #import
--#endif
-
- @implementation RNSound {
- NSMutableDictionary *_playerPool;
diff --git a/scripts/check-elf-alignment.sh b/scripts/check-elf-alignment.sh
new file mode 100755
index 0000000000000..92faf3941ccd0
--- /dev/null
+++ b/scripts/check-elf-alignment.sh
@@ -0,0 +1,114 @@
+#!/bin/bash
+set -o pipefail
+
+progname="${0##*/}"
+progname="${progname%.sh}"
+
+# usage: check_elf_alignment.sh [path to *.so files|path to *.apk]
+cleanup_trap() {
+ if [ -n "$tmp" ] && [ -d "$tmp" ]; then
+ rm -rf "$tmp"
+ fi
+ exit "${1:-0}"
+}
+
+# shellcheck disable=SC1090
+source "$(dirname "$0")/shellUtils.sh"
+
+usage() {
+ echo "Host side script to check the ELF alignment of shared libraries."
+ echo "Shared libraries are reported ALIGNED when their ELF regions are"
+ echo "16 KB or 64 KB aligned. Otherwise they are reported as UNALIGNED."
+ echo
+ echo "Usage: ${progname} [input-path|input-APK|input-APEX]"
+}
+
+if [ "$#" -ne 1 ]; then
+ usage
+ exit 1
+fi
+
+case "$1" in
+ --help | -h | -\?)
+ usage
+ exit 0
+ ;;
+ *)
+ dir="$1"
+ ;;
+esac
+
+if ! [ -f "$dir" ] && ! [ -d "$dir" ]; then
+ error "Invalid file: $dir"
+ exit 1
+fi
+
+if [[ "$dir" == *.apk ]]; then
+ trap 'cleanup_trap' EXIT
+
+ echo
+ title "Recursively analyzing $dir"
+ echo
+
+ if zipalign --help 2>&1 | grep -q "\-P "; then
+ title "APK zip-alignment"
+ zipalign -v -c -P 16 4 "$dir" | grep -E 'lib/arm64-v8a|lib/x86_64|Verification'
+ echo "========================="
+ else
+ info "NOTICE: Zip alignment check requires build-tools version 35.0.0-rc3 or higher."
+ info " You can install the latest build-tools by running the below command"
+ info " and updating your \$PATH:"
+ echo
+ info ' sdkmanager "build-tools;35.0.0-rc3"'
+ fi
+
+ dir_filename=$(basename "$dir")
+ tmp=$(mktemp -d -t "${dir_filename%.apk}_out_XXXXX")
+ unzip "$dir" lib/* -d "$tmp" >/dev/null 2>&1
+ dir="$tmp"
+fi
+
+if [[ "$dir" == *.apex ]]; then
+ trap 'cleanup_trap' EXIT
+
+ echo
+ title "Recursively analyzing $dir"
+ echo
+
+ dir_filename=$(basename "$dir")
+ tmp=$(mktemp -d -t "${dir_filename%.apex}_out_XXXXX")
+ if ! deapexer extract "$dir" "$tmp"; then
+ error "Failed to deapex."
+ exit 1
+ fi
+ dir="$tmp"
+fi
+
+unaligned_libs=()
+
+echo
+title "ELF alignment"
+
+matches="$(find "$dir" -type f)"
+IFS=$'\n'
+for match in $matches; do
+ [[ "$match" == *".apk" ]] && warn "doesn't recursively inspect .apk file: $match"
+ [[ "$match" == *".apex" ]] && warn "doesn't recursively inspect .apex file: $match"
+
+ [[ $(file "$match") == *"ELF"* ]] || continue
+
+ res="$(objdump -p "$match" | grep LOAD | awk '{ print $NF }' | head -1)"
+ if [[ $res =~ 2\*\*(1[4-9]|[2-9][0-9]|[1-9][0-9]{2,}) ]]; then
+ printf "%s: %sALIGNED%s (%s)\n" "$match" "$GREEN" "$RESET" "$res"
+ else
+ printf "%s: %sUNALIGNED%s (%s)\n" "$match" "$RED" "$RESET" "$res"
+ unaligned_libs+=("$match")
+ fi
+done
+
+if [ "${#unaligned_libs[@]}" -gt 0 ]; then
+ printf "%sFound %d unaligned libs (only arm64-v8a/x86_64 libs need to be aligned).%s\n" "$RED" "${#unaligned_libs[@]}" "$RESET"
+elif [ -n "${dir_filename:-}" ]; then
+ success "ELF Verification Successful"
+fi
+echo "====================="
\ No newline at end of file
diff --git a/scripts/lintChanged.sh b/scripts/lintChanged.sh
index 56f33828b4201..7a6ab783a9bde 100755
--- a/scripts/lintChanged.sh
+++ b/scripts/lintChanged.sh
@@ -36,7 +36,7 @@ fi
# Run eslint on the changed files
if [[ -n "$GIT_DIFF_OUTPUT" ]] ; then
# shellcheck disable=SC2086 # For multiple files in variable
- eslint --max-warnings=241 --config ./eslint.changed.config.mjs $GIT_DIFF_OUTPUT
+ eslint --max-warnings=133 --config ./eslint.changed.config.mjs $GIT_DIFF_OUTPUT
else
info "No TypeScript files changed"
fi
diff --git a/scripts/run-build.sh b/scripts/run-build.sh
index 18e92e089563e..f6bc1c7f8837d 100755
--- a/scripts/run-build.sh
+++ b/scripts/run-build.sh
@@ -35,7 +35,7 @@ IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh)
# See if we should force standalone NewDot build
NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}"
- if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then
+if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then
# Set HybridApp-specific arguments
IOS_MODE="Debug"
ANDROID_MODE="Debug"
@@ -57,16 +57,16 @@ fi
# Check if the argument is one of the desired values
case "$BUILD" in
--ios)
- npx rock run:ios --configuration $IOS_MODE --scheme "$SCHEME"
+ RCT_USE_RN_DEP=0 RCT_USE_PREBUILT_RNCORE=0 npx rock run:ios --configuration $IOS_MODE --scheme "$SCHEME" --dev-server
;;
--ipad)
- npx rock run:ios --simulator "iPad Pro (12.9-inch) (6th generation)" --configuration $IOS_MODE --scheme "$SCHEME"
+ RCT_USE_RN_DEP=0 RCT_USE_PREBUILT_RNCORE=0 npx rock run:ios --simulator "iPad Pro (12.9-inch) (6th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --dev-server
;;
--ipad-sm)
- npx rock run:ios --simulator "iPad Pro (11-inch) (4th generation)" --configuration $IOS_MODE --scheme "$SCHEME"
+ RCT_USE_RN_DEP=0 RCT_USE_PREBUILT_RNCORE=0 npx rock run:ios --simulator "iPad Pro (11-inch) (4th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --dev-server
;;
--android)
- npx rock run:android --variant $ANDROID_MODE --app-id $APP_ID --active-arch-only --verbose
+ npx rock run:android --variant $ANDROID_MODE --app-id $APP_ID --active-arch-only --verbose --dev-server
;;
*)
print_error_and_exit
diff --git a/scripts/stubReactNative.js b/scripts/stubReactNative.js
index 0275fd501202b..c68631ee344a4 100644
--- a/scripts/stubReactNative.js
+++ b/scripts/stubReactNative.js
@@ -4,6 +4,7 @@ const Module = require('module');
const originalRequire = Module.prototype.require;
// List of modules to stub (we don't need these in scripts)
+// eslint-disable-next-line unicorn/prefer-set-has
const MODULES_TO_STUB = [
'react-native',
'react-native-config',
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 8e67394a65ab7..5b64e0a6f3116 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -427,8 +427,6 @@ const CONST = {
NEW_EXPENSIFY_URL: ACTIVE_EXPENSIFY_URL,
UBER_CONNECT_URL,
- FREE_TRIAL_MARKDOWN:
- "# Your free trial has started! Let's get you set up.\nđ Hey there, I'm your Expensify setup specialist. I've already created a workspace to help manage your team's receipts and expenses. To make the most of your 30-day free trial, just follow the remaining setup steps below!",
APP_DOWNLOAD_LINKS: {
ANDROID: `https://play.google.com/store/apps/details?id=${ANDROID_PACKAGE_NAME}`,
IOS: 'https://apps.apple.com/us/app/expensify-travel-expense/id471713959',
@@ -700,11 +698,9 @@ const CONST = {
NEWDOT_MANAGER_MCTEST: 'newDotManagerMcTest',
NEWDOT_REJECT: 'newDotReject',
CUSTOM_RULES: 'customRules',
- CUSTOM_AVATARS: 'customAvatars',
GLOBAL_REIMBURSEMENTS_ON_ND: 'globalReimbursementsOnND',
IS_TRAVEL_VERIFIED: 'isTravelVerified',
PLAID_COMPANY_CARDS: 'plaidCompanyCards',
- NEWDOT_REVERT_SPLITS: 'newDotRevertSplits',
EXPENSIFY_CARD_EU_UK: 'expensifyCardEuUk',
EUR_BILLING: 'eurBilling',
NO_OPTIMISTIC_TRANSACTION_THREADS: 'noOptimisticTransactionThreads',
@@ -1027,6 +1023,7 @@ const CONST = {
PLAN_TYPES_AND_PRICING_HELP_URL: 'https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing',
MERGE_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/new-expensify/settings/Merge-Accounts',
CONNECT_A_BUSINESS_BANK_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account',
+ DOMAIN_VERIFICATION_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain',
REGISTER_FOR_WEBINAR_URL: 'https://events.zoom.us/eo/Aif1I8qCi1GZ7KnLnd1vwGPmeukSRoPjFpyFAZ2udQWn0-B86e1Z~AggLXsr32QYFjq8BlYLZ5I06Dg',
TEST_RECEIPT_URL: `${CLOUDFRONT_URL}/images/fake-receipt__tacotodds.png`,
// Use Environment.getEnvironmentURL to get the complete URL with port number
@@ -1312,6 +1309,7 @@ const CONST = {
UPDATE_MANUAL_APPROVAL_THRESHOLD: 'POLICYCHANGELOG_UPDATE_MANUAL_APPROVAL_THRESHOLD',
UPDATE_MAX_EXPENSE_AMOUNT: 'POLICYCHANGELOG_UPDATE_MAX_EXPENSE_AMOUNT',
UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT: 'POLICYCHANGELOG_UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT',
+ UPDATE_MULTIPLE_TAGS_APPROVER_RULES: 'POLICYCHANGELOG_UPDATE_MULTIPLE_TAGS_APPROVER_RULES',
UPDATE_NAME: 'POLICYCHANGELOG_UPDATE_NAME',
UPDATE_DESCRIPTION: 'POLICYCHANGELOG_UPDATE_DESCRIPTION',
UPDATE_OWNERSHIP: 'POLICYCHANGELOG_UPDATE_OWNERSHIP',
@@ -1615,6 +1613,8 @@ const CONST = {
},
TELEMETRY: {
CONTEXT_FULLSTORY: 'Fullstory',
+ CONTEXT_POLICIES: 'Policies',
+ TAG_ACTIVE_POLICY: 'active_policy_id',
},
PRIORITY_MODE: {
GSD: 'gsd',
@@ -3261,6 +3261,7 @@ const CONST = {
ACTIVITY_INDICATOR_SIZE: {
LARGE: 'large',
+ SMALL: 'small',
},
QR_CODE_SIZE: {
@@ -7026,6 +7027,14 @@ const CONST = {
description: 'workspace.upgrade.distanceRates.description' as const,
icon: 'CarIce',
},
+ auditor: {
+ id: 'auditor' as const,
+ alias: 'auditor',
+ name: 'Auditor',
+ title: 'workspace.upgrade.auditor.title' as const,
+ description: 'workspace.upgrade.auditor.description' as const,
+ icon: 'BlueShield',
+ },
reports: {
id: 'reports' as const,
alias: 'reports',
@@ -7373,6 +7382,7 @@ const FRAUD_PROTECTION_EVENT = {
VIEW_VIRTUAL_CARD_PAN: 'ViewVirtualCardPAN',
BUSINESS_BANK_ACCOUNT_SETUP: 'BusinessBankAccountSetup',
PERSONAL_BANK_ACCOUNT_SETUP: 'PersonalBankAccountSetup',
+ NEW_EMAILS_INVITED: 'NewEmailsInvited',
};
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 85c6e51515ee3..f31f85ab68ad3 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -1,4 +1,5 @@
import HybridAppModule from '@expensify/react-native-hybrid-app';
+import * as Sentry from '@sentry/react-native';
import {Audio} from 'expo-av';
import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import type {NativeEventSubscription} from 'react-native';
@@ -23,6 +24,7 @@ import usePriorityMode from './hooks/usePriorityChange';
import {updateLastRoute} from './libs/actions/App';
import {disconnect} from './libs/actions/Delegate';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
+import {openReportFromDeepLink} from './libs/actions/Link';
import * as Report from './libs/actions/Report';
import {hasAuthToken} from './libs/actions/Session';
import * as User from './libs/actions/User';
@@ -186,6 +188,12 @@ function Expensify() {
useEffect(() => {
// Initialize Fullstory lib
FS.init(userMetadata);
+ FS.getSessionId().then((sessionId) => {
+ if (!sessionId) {
+ return;
+ }
+ Sentry.setContext(CONST.TELEMETRY.CONTEXT_FULLSTORY, {sessionId});
+ });
}, [userMetadata]);
// Log the platform and config to debug .env issues
@@ -236,7 +244,7 @@ function Expensify() {
Linking.getInitialURL().then((url) => {
setInitialUrl(url as Route);
if (url) {
- Report.openReportFromDeepLink(url, currentOnboardingPurposeSelected, currentOnboardingCompanySize, onboardingInitialPath, allReports, isAuthenticated);
+ openReportFromDeepLink(url, currentOnboardingPurposeSelected, currentOnboardingCompanySize, onboardingInitialPath, allReports, isAuthenticated);
} else {
Report.doneCheckingPublicRoom();
}
@@ -245,7 +253,7 @@ function Expensify() {
// Open chat report from a deep link (only mobile native)
linkingChangeListener.current = Linking.addEventListener('url', (state) => {
const isCurrentlyAuthenticated = hasAuthToken();
- Report.openReportFromDeepLink(state.url, currentOnboardingPurposeSelected, currentOnboardingCompanySize, onboardingInitialPath, allReports, isCurrentlyAuthenticated);
+ openReportFromDeepLink(state.url, currentOnboardingPurposeSelected, currentOnboardingCompanySize, onboardingInitialPath, allReports, isCurrentlyAuthenticated);
});
if (CONFIG.IS_HYBRID_APP) {
HybridAppModule.onURLListenerAdded();
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 596e4a1a09de9..c3c1c5ef94f3f 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -1192,7 +1192,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount;
[ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount;
[ONYXKEYS.REIMBURSEMENT_ACCOUNT_OPTION_PRESSED]: ValueOf;
- [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: number;
+ [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number;
[ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[];
[ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID]: string;
[ONYXKEYS.IS_LOADING_PAYMENT_METHODS]: boolean;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index bad346a8f0d03..3b192bd39c728 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -60,6 +60,7 @@ const ROUTES = {
return `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const;
},
},
+ SEARCH_ROOT_VERIFY_ACCOUNT: `search/${VERIFY_ACCOUNT}`,
SEARCH_SAVED_SEARCH_RENAME: {
route: 'search/saved-search/rename',
getRoute: ({name, jsonQuery}: {name: string; jsonQuery: SearchQueryString}) => `search/saved-search/rename?name=${name}&q=${jsonQuery}` as const,
@@ -83,6 +84,10 @@ const ROUTES = {
return getUrlWithBackToParam(baseRoute, backTo);
},
},
+ SEARCH_REPORT_VERIFY_ACCOUNT: {
+ route: `search/view/:reportID/${VERIFY_ACCOUNT}`,
+ getRoute: (reportID: string) => `search/view/${reportID}/${VERIFY_ACCOUNT}` as const,
+ },
SEARCH_MONEY_REQUEST_REPORT: {
route: 'search/r/:reportID',
getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => {
@@ -92,6 +97,10 @@ const ROUTES = {
return getUrlWithBackToParam(baseRoute, backTo);
},
},
+ SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT: {
+ route: `search/r/:reportID/${VERIFY_ACCOUNT}`,
+ getRoute: (reportID: string) => `search/r/${reportID}/${VERIFY_ACCOUNT}` as const,
+ },
SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS: {
route: 'search/r/:reportID/hold',
getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => {
@@ -268,6 +277,14 @@ const ROUTES = {
route: 'settings/card/:cardID?',
getRoute: (cardID: string) => `settings/card/${cardID}` as const,
},
+ SETTINGS_DOMAIN_CARD_UPDATE_ADDRESS: {
+ route: 'settings/card/:cardID/update-address',
+ getRoute: (cardID: string) => `settings/card/${cardID}/update-address` as const,
+ },
+ SETTINGS_DOMAIN_CARD_CONFIRM_MAGIC_CODE: {
+ route: 'settings/card/:cardID/confirm-magic-code',
+ getRoute: (cardID: string) => `settings/card/${cardID}/confirm-magic-code` as const,
+ },
SETTINGS_REPORT_FRAUD: {
route: 'settings/wallet/card/:cardID/report-virtual-fraud',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-virtual-fraud` as const,
@@ -457,10 +474,6 @@ const ROUTES = {
return getUrlWithBackToParam(baseRoute, backTo);
},
},
- SET_DEFAULT_WORKSPACE: {
- route: 'set-default-workspace',
- getRoute: (navigateTo?: string) => (navigateTo ? (`set-default-workspace?navigateTo=${encodeURIComponent(navigateTo)}` as const) : ('set-default-workspace' as const)),
- },
REPORT: 'r',
REPORT_WITH_ID: {
route: 'r/:reportID?/:reportActionID?',
@@ -529,6 +542,10 @@ const ROUTES = {
return getUrlWithBackToParam(`r/${reportID}/details/shareCode` as const, backTo);
},
},
+ REPORT_VERIFY_ACCOUNT: {
+ route: `r/:reportID/${VERIFY_ACCOUNT}`,
+ getRoute: (reportID: string) => `r/${reportID}/${VERIFY_ACCOUNT}` as const,
+ },
REPORT_PARTICIPANTS: {
route: 'r/:reportID/participants',
@@ -727,8 +744,12 @@ const ROUTES = {
},
MONEY_REQUEST_CREATE: {
route: ':action/:iouType/start/:transactionID/:reportID/:backToReport?',
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) =>
- `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${backToReport ?? ''}` as const,
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => {
+ if (backToReport) {
+ return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${backToReport}` as const;
+ }
+ return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}` as const;
+ },
},
MONEY_REQUEST_STEP_SEND_FROM: {
route: 'create/:iouType/from/:transactionID/:reportID',
@@ -744,12 +765,22 @@ const ROUTES = {
},
MONEY_REQUEST_STEP_CONFIRMATION: {
route: ':action/:iouType/confirmation/:transactionID/:reportID/:backToReport?',
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backToReport?: string, participantsAutoAssigned?: boolean, backTo?: string) =>
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backToReport?: string, participantsAutoAssigned?: boolean, backTo?: string) => {
+ let optionalRoutePart = '';
+ if (backToReport !== undefined) {
+ optionalRoutePart += `/${backToReport}`;
+ }
+ if (participantsAutoAssigned !== undefined) {
+ optionalRoutePart += '?participantsAutoAssigned=true';
+ }
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
- getUrlWithBackToParam(
- `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}/${backToReport ?? ''}${participantsAutoAssigned ? '?participantsAutoAssigned=true' : ''}`,
- backTo,
- ),
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}${optionalRoutePart}` as const, backTo);
+ },
+ },
+ MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT: {
+ route: `:action/:iouType/confirmation/:transactionID/:reportID/${VERIFY_ACCOUNT}`,
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) =>
+ `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}/${VERIFY_ACCOUNT}` as const,
},
MONEY_REQUEST_STEP_AMOUNT: {
route: ':action/:iouType/amount/:transactionID/:reportID/:reportActionID?/:pageIndex?/:backToReport?',
@@ -898,6 +929,10 @@ const ROUTES = {
return getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${reportID}/edit${shouldTurnOffSelectionMode ? '?shouldTurnOffSelectionMode=true' : ''}`, backTo);
},
},
+ SET_DEFAULT_WORKSPACE: {
+ route: 'set-default-workspace',
+ getRoute: (navigateTo?: string) => (navigateTo ? (`set-default-workspace?navigateTo=${encodeURIComponent(navigateTo)}` as const) : ('set-default-workspace' as const)),
+ },
SETTINGS_TAGS_ROOT: {
route: 'settings/:policyID/tags',
getRoute: (policyID: string | undefined, backTo = '') => {
@@ -2062,12 +2097,6 @@ const ROUTES = {
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getRoute: (policyID: string, feed: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${feed}/assign-card`, backTo),
},
- WORKSPACE_COMPANY_CARDS_TRANSACTION_START_DATE: {
- route: 'workspaces/:policyID/company-cards/:feed/assign-card/transaction-start-date',
-
- // eslint-disable-next-line no-restricted-syntax -- Legacy route generation
- getRoute: (policyID: string, feed: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${feed}/assign-card/transaction-start-date`, backTo),
- },
WORKSPACE_COMPANY_CARD_DETAILS: {
route: 'workspaces/:policyID/company-cards/:bank/:cardID',
@@ -3302,6 +3331,14 @@ const ROUTES = {
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getRoute: (backTo?: string) => getUrlWithBackToParam('test-tools' as const, backTo),
},
+ WORKSPACES_VERIFY_DOMAIN: {
+ route: 'workspaces/verify-domain/:accountID',
+ getRoute: (accountID: number) => `workspaces/verify-domain/${accountID}` as const,
+ },
+ WORKSPACES_DOMAIN_VERIFIED: {
+ route: 'workspaces/domain-verified/:accountID',
+ getRoute: (accountID: number) => `workspaces/domain-verified/${accountID}` as const,
+ },
} as const;
/**
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index cab8d814b911d..acbdd26627cba 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -41,9 +41,12 @@ const SCREENS = {
},
SEARCH: {
ROOT: 'Search_Root',
+ ROOT_VERIFY_ACCOUNT: 'Search_Root_Verify_Account',
MONEY_REQUEST_REPORT: 'Search_Money_Request_Report',
+ MONEY_REQUEST_REPORT_VERIFY_ACCOUNT: 'Search_Money_Request_Report_Verify_Account',
MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS: 'Search_Money_Request_Report_Hold_Transactions',
REPORT_RHP: 'Search_Report_RHP',
+ REPORT_VERIFY_ACCOUNT: 'Search_Report_Verify_Account',
ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP',
ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP',
ADVANCED_FILTERS_GROUP_BY_RHP: 'Search_Advanced_Filters_GroupBy_RHP',
@@ -253,7 +256,9 @@ const SCREENS = {
ADD_UNREPORTED_EXPENSE: 'AddUnreportedExpense',
SCHEDULE_CALL: 'ScheduleCall',
REPORT_CHANGE_APPROVER: 'Report_Change_Approver',
+ REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account',
MERGE_TRANSACTION: 'MergeTransaction',
+ DOMAIN: 'Domain',
},
PUBLIC_CONSOLE_DEBUG: 'Console_Debug',
SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop',
@@ -268,6 +273,7 @@ const SCREENS = {
HOLD: 'Money_Request_Hold_Reason',
REJECT: 'Money_Request_Reject_Reason',
STEP_CONFIRMATION: 'Money_Request_Step_Confirmation',
+ STEP_CONFIRMATION_VERIFY_ACCOUNT: 'Money_Request_Step_Confirmation_Verify_Account',
START: 'Money_Request_Start',
STEP_UPGRADE: 'Money_Request_Step_Upgrade',
STEP_AMOUNT: 'Money_Request_Step_Amount',
@@ -355,6 +361,8 @@ const SCREENS = {
DOMAIN_CARD: {
DOMAIN_CARD_DETAIL: 'Domain_Card_Detail',
DOMAIN_CARD_REPORT_FRAUD: 'Domain_Card_Report_Fraud',
+ DOMAIN_CARD_UPDATE_ADDRESS: 'Domain_Card_Update_Address',
+ DOMAIN_CARD_CONFIRM_MAGIC_CODE: 'Domain_Card_Confirm_Magic_Code',
},
SETTINGS_TAGS: {
@@ -402,9 +410,7 @@ const SCREENS = {
ROOT: 'NewReportWorkspaceSelection_Root',
},
- SET_DEFAULT_WORKSPACE: {
- ROOT: 'SetDefaultWorkspace_Root',
- },
+ SET_DEFAULT_WORKSPACE: 'SetDefaultWorkspace',
REPORT_DETAILS: {
ROOT: 'Report_Details_Root',
@@ -556,7 +562,6 @@ const SCREENS = {
PROFILE: 'Workspace_Overview',
COMPANY_CARDS: 'Workspace_CompanyCards',
COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard',
- COMPANY_CARDS_TRANSACTION_START_DATE: 'Workspace_CompanyCards_TransactionStartDate',
COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed',
COMPANY_CARDS_BANK_CONNECTION: 'Workspace_CompanyCards_BankConnection',
COMPANY_CARDS_ADD_NEW: 'Workspace_CompanyCards_New',
@@ -781,6 +786,7 @@ const SCREENS = {
REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount',
REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO: 'Reimbursement_Account_Signer_Info',
REFERRAL_DETAILS: 'Referral_Details',
+ REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account',
KEYBOARD_SHORTCUTS: 'KeyboardShortcuts',
SHARE: {
ROOT: 'Share_Root',
@@ -813,6 +819,8 @@ const SCREENS = {
TEST_TOOLS_MODAL: {
ROOT: 'TestToolsModal_Root',
},
+ WORKSPACES_VERIFY_DOMAIN: 'Workspaces_Verify_Domain',
+ WORKSPACES_DOMAIN_VERIFIED: 'Workspaces_Domain_Verified',
} as const;
type Screen = DeepValueOf;
diff --git a/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx
index c0d2fe0e3f9cf..7bd63b8e8eefb 100644
--- a/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx
+++ b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx
@@ -5,8 +5,8 @@ import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import SelectionList from '@components/SelectionListWithSections';
-import RadioListItem from '@components/SelectionListWithSections/RadioListItem';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/ListItem/RadioListItem';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
@@ -56,19 +56,14 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq
});
}, [isBetaEnabled]);
- const {sections} = useMemo(
- () => ({
- sections: [
- {
- data: availableCurrencies.map((currencyItem) => ({
- text: currencyItem,
- value: currencyItem,
- keyForList: currencyItem,
- isSelected: currencyItem === currency,
- })),
- },
- ],
- }),
+ const currencyOptions = useMemo(
+ () =>
+ availableCurrencies.map((currencyItem) => ({
+ text: currencyItem,
+ value: currencyItem,
+ keyForList: currencyItem,
+ isSelected: currencyItem === currency,
+ })),
[availableCurrencies, currency],
);
@@ -135,17 +130,15 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq
return (
}
- initiallyFocusedOptionKey={currency}
- containerStyle={[styles.mhn5]}
- sections={sections}
+ data={currencyOptions}
+ ListItem={RadioListItem}
onSelectRow={(option) => {
selectCurrency(option.value);
}}
- showScrollIndicator
+ style={{containerStyle: styles.mhn5}}
+ initiallyFocusedItemKey={currency}
+ customListHeader={}
shouldStopPropagation
- shouldUseDynamicMaxToRenderPerBatch
- ListItem={RadioListItem}
/>
);
diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx
index fddbcd2f0838b..b58ddc34ce740 100644
--- a/src/components/AddPaymentCard/PaymentCardForm.tsx
+++ b/src/components/AddPaymentCard/PaymentCardForm.tsx
@@ -9,11 +9,10 @@ import CurrencySelector from '@components/CurrencySelector';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import RenderHTML from '@components/RenderHTML';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
import StateSelector from '@components/StateSelector';
-import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -46,13 +45,7 @@ type PaymentCardFormProps = {
function IAcceptTheLabel() {
const {translate} = useLocalize();
- return (
-
- {`${translate('common.iAcceptThe')}`}
- {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`}
- {` ${translate('common.privacyPolicy')} `}
-
- );
+ return ;
}
const REQUIRED_FIELDS = [
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index 49a52a05774f4..063dece529858 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -42,6 +42,26 @@ type Item = {
pickAttachment: () => Promise;
};
+/**
+ * Ensures asset has proper fileName and type properties
+ */
+const processAssetWithFallbacks = (asset: Asset): Asset => {
+ // Generate fallback name: extract from URI if available, otherwise use timestamped default
+ const fallbackName = asset.uri
+ ? asset.uri
+ .substring(asset.uri.lastIndexOf('/') + 1)
+ .split('?')
+ .at(0)
+ : `image_${Date.now()}.jpeg`;
+ const fileName = asset.fileName ?? fallbackName;
+ return {
+ ...asset,
+ fileName,
+ // Default to JPEG if no type specified
+ type: asset.type ?? 'image/jpeg',
+ };
+};
+
/**
* Return imagePickerOptions based on the type
*/
@@ -202,7 +222,9 @@ function AttachmentPicker({
checkAllProcessed();
});
} else {
- processedAssets.push(asset);
+ // Ensure the asset has proper fileName and type for non-HEIC images
+ const processedAsset = processAssetWithFallbacks(asset);
+ processedAssets.push(processedAsset);
checkAllProcessed();
}
})
@@ -211,7 +233,9 @@ function AttachmentPicker({
checkAllProcessed();
});
} else {
- processedAssets.push(asset);
+ // Ensure the asset has proper fileName and type
+ const processedAsset = processAssetWithFallbacks(asset);
+ processedAssets.push(processedAsset);
checkAllProcessed();
}
});
diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index b307d7da0435c..3ff23c88a9850 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -273,7 +273,7 @@ function AvatarWithDisplayName({
)}
-
+
{getCustomDisplayName(
shouldUseCustomSearchTitleName,
report,
diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx
index 7013e287e229b..52add8391ef3b 100644
--- a/src/components/CategoryPicker.tsx
+++ b/src/components/CategoryPicker.tsx
@@ -60,6 +60,7 @@ function CategoryPicker({selectedCategory, policyID, onSubmit, addBottomSafeArea
categories,
localeCompare,
recentlyUsedCategories: validPolicyRecentlyUsedCategories,
+ translate,
});
const categoryData = categoryOptions?.at(0)?.data ?? [];
@@ -69,7 +70,7 @@ function CategoryPicker({selectedCategory, policyID, onSubmit, addBottomSafeArea
const showInput = !isCategoriesCountBelowThreshold;
return [categoryOptions, header, showInput];
- }, [policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, policyCategories, policyCategoriesDraft, localeCompare]);
+ }, [policyCategories, policyCategoriesDraft, policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, localeCompare, translate]);
const selectedOptionKey = useMemo(() => (sections?.at(0)?.data ?? []).filter((category) => category.searchText === selectedCategory).at(0)?.keyForList, [sections, selectedCategory]);
diff --git a/src/components/DestinationPicker.tsx b/src/components/DestinationPicker.tsx
index ab788df47282d..5d29d8f3658da 100644
--- a/src/components/DestinationPicker.tsx
+++ b/src/components/DestinationPicker.tsx
@@ -54,6 +54,7 @@ function DestinationPicker({selectedDestination, policyID, onSubmit}: Destinatio
selectedOptions,
destinations: Object.values(customUnit?.rates ?? {}),
recentlyUsedDestinations: policyRecentlyUsedDestinations,
+ translate,
});
const destinationData = destinationOptions?.at(0)?.data ?? [];
@@ -63,7 +64,7 @@ function DestinationPicker({selectedDestination, policyID, onSubmit}: Destinatio
const showInput = !isDestinationsCountBelowThreshold;
return [destinationOptions, header, showInput];
- }, [debouncedSearchValue, selectedOptions, customUnit?.rates, policyRecentlyUsedDestinations]);
+ }, [debouncedSearchValue, selectedOptions, customUnit?.rates, policyRecentlyUsedDestinations, translate]);
const selectedOptionKey = useMemo(
() => (sections?.at(0)?.data ?? []).filter((destination) => destination.keyForList === selectedDestination).at(0)?.keyForList,
diff --git a/src/components/Domain/CopyableTextField.tsx b/src/components/Domain/CopyableTextField.tsx
new file mode 100644
index 0000000000000..8310b74bb5e13
--- /dev/null
+++ b/src/components/Domain/CopyableTextField.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import {View} from 'react-native';
+import ActivityIndicator from '@components/ActivityIndicator';
+import CopyTextToClipboard from '@components/CopyTextToClipboard';
+import Text from '@components/Text';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+
+type CopyableTextFieldProps = {
+ /** Text to display and to copy */
+ value?: string;
+
+ /** Should an activity indicator be shown instead of the text and button */
+ isLoading?: boolean;
+};
+
+function CopyableTextField({value, isLoading = false}: CopyableTextFieldProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ return (
+
+ {isLoading ? (
+
+ ) : (
+ <>
+ {value ?? ''}
+
+
+
+ >
+ )}
+
+ );
+}
+
+CopyableTextField.displayName = 'CopyableTextField';
+export default CopyableTextField;
diff --git a/src/components/Domain/DomainMenuItem.tsx b/src/components/Domain/DomainMenuItem.tsx
new file mode 100644
index 0000000000000..7bdef86bbe058
--- /dev/null
+++ b/src/components/Domain/DomainMenuItem.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import * as Expensicons from '@components/Icon/Expensicons';
+import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
+import {PressableWithoutFeedback} from '@components/Pressable';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import DomainsListRow from './DomainsListRow';
+
+type DomainMenuItemProps = {
+ /** Domain menu item data */
+ item: DomainItem;
+
+ /** Row index in the menu */
+ index: number;
+};
+
+type DomainItem = {
+ /** Type of menu item row in the list of workspaces and domains */
+ listItemType: 'domain';
+
+ /** Main text to show in the row */
+ title: string;
+
+ /** Function to run after clicking on the row */
+ action: () => void;
+
+ /** ID of the row's domain */
+ accountID: number;
+
+ /** Whether the user is an admin of the row's domain */
+ isAdmin: boolean;
+
+ /** Whether the row's domain is validated (aka verified) */
+ isValidated: boolean;
+} & Pick;
+
+function DomainMenuItem({item, index}: DomainMenuItemProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {isAdmin, isValidated} = item;
+
+ const threeDotsMenuItems: PopoverMenuItem[] | undefined =
+ !isValidated && isAdmin
+ ? [
+ {
+ icon: Expensicons.Globe,
+ text: translate('domain.verifyDomain.title'),
+ onSelected: () => Navigation.navigate(ROUTES.WORKSPACES_VERIFY_DOMAIN.getRoute(item.accountID)),
+ },
+ ]
+ : undefined;
+
+ return (
+
+
+ {({hovered}) => (
+
+ )}
+
+
+ );
+}
+
+DomainMenuItem.displayName = 'DomainMenuItem';
+
+export type {DomainItem};
+export default DomainMenuItem;
diff --git a/src/components/Domain/DomainsListRow.tsx b/src/components/Domain/DomainsListRow.tsx
index 79cecbc180f9c..adfe7d9b2377f 100644
--- a/src/components/Domain/DomainsListRow.tsx
+++ b/src/components/Domain/DomainsListRow.tsx
@@ -1,10 +1,15 @@
import React from 'react';
import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import Badge from '@components/Badge';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
import TextWithTooltip from '@components/TextWithTooltip';
+import ThreeDotsMenu from '@components/ThreeDotsMenu';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
type DomainsListRowProps = {
/** Name of the domain */
@@ -13,17 +18,23 @@ type DomainsListRowProps = {
/** Whether the row is hovered, so we can modify its style */
isHovered: boolean;
- /** Whether the icon at the end of the row should be displayed */
- shouldShowRightIcon: boolean;
+ /** The text to display inside a badge next to the title */
+ badgeText?: string;
+
+ /** Items for the three dots menu */
+ menuItems?: PopoverMenuItem[];
+
+ /** The type of brick road indicator to show. */
+ brickRoadIndicator?: ValueOf;
};
-function DomainsListRow({title, isHovered, shouldShowRightIcon}: DomainsListRowProps) {
+function DomainsListRow({title, isHovered, badgeText, brickRoadIndicator, menuItems}: DomainsListRowProps) {
const styles = useThemeStyles();
const theme = useTheme();
return (
-
-
+
+
+
+ {!!badgeText && (
+
+
+
+ )}
- {shouldShowRightIcon && (
+
+
+
+
+ {!!brickRoadIndicator && (
+
+ )}
+
+ {!!menuItems?.length && (
+
+ )}
+
+
- )}
+
);
}
diff --git a/src/components/EReceiptWithSizeCalculation.tsx b/src/components/EReceiptWithSizeCalculation.tsx
index ed513ee029784..6856610133bbf 100644
--- a/src/components/EReceiptWithSizeCalculation.tsx
+++ b/src/components/EReceiptWithSizeCalculation.tsx
@@ -5,6 +5,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import type {Transaction} from '@src/types/onyx';
import EReceipt from './EReceipt';
+import PerDiemEReceipt from './PerDiemEReceipt';
import type {TransactionListItemType} from './SelectionListWithSections/types';
type EReceiptWithSizeCalculationProps = {
@@ -19,6 +20,9 @@ type EReceiptWithSizeCalculationProps = {
/** Callback to be called when the image loads */
onLoad?: () => void;
+
+ /** Determines which receipt component to render */
+ receiptType?: 'default' | 'perDiem';
};
const eReceiptAspectRatio = variables.eReceiptBGHWidth / variables.eReceiptBGHeight;
@@ -32,6 +36,10 @@ function EReceiptWithSizeCalculation(props: EReceiptWithSizeCalculationProps) {
setScaleFactor(width / variables.eReceiptBGHWidth);
};
+ if (props.receiptType === 'perDiem' && !props.transactionID) {
+ return null;
+ }
+
return scaleFactor ? (
-
+ {props.receiptType === 'perDiem' && props.transactionID ? (
+
+ ) : (
+
+ )}
) : (
diff --git a/src/components/EmojiPicker/EmojiPicker.tsx b/src/components/EmojiPicker/EmojiPicker.tsx
index 552ab1b2ec8ff..4e56ce64daab8 100644
--- a/src/components/EmojiPicker/EmojiPicker.tsx
+++ b/src/components/EmojiPicker/EmojiPicker.tsx
@@ -183,7 +183,7 @@ function EmojiPicker({viewportOffsetTop, ref}: EmojiPickerProps) {
/**
* Callback for the emoji picker to add whatever emoji is chosen into the main input
*/
- const selectEmoji = (emoji: string, emojiObject: Emoji, preferredSkinTone: number) => {
+ const selectEmoji = (emoji: string, emojiObject: Emoji) => {
// Prevent fast click / multiple emoji selection;
// The first click will hide the emoji picker by calling the hideEmojiPicker() function
if (!isEmojiPickerVisible) {
@@ -192,7 +192,7 @@ function EmojiPicker({viewportOffsetTop, ref}: EmojiPickerProps) {
hideEmojiPicker(false);
if (typeof onEmojiSelected.current === 'function') {
- onEmojiSelected.current(emoji, emojiObject, preferredSkinTone);
+ onEmojiSelected.current(emoji, emojiObject);
}
};
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx b/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx
index 92fb036ce7a07..9f71caa5032a6 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx
@@ -107,7 +107,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji, ref}: EmojiPickerMenuPro
if (!('name' in item)) {
return;
}
- onEmojiSelected(emoji, item, preferredSkinTone);
+ onEmojiSelected(emoji, item);
})}
emoji={emojiCode ?? ''}
isHighlighted={shouldEmojiBeHighlighted}
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.tsx b/src/components/EmojiPicker/EmojiPickerMenu/index.tsx
index d04822b42eadf..542f615e686b2 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.tsx
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.tsx
@@ -174,7 +174,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji, ref}: EmojiPickerMenuPro
}
if ('types' in item || 'name' in item) {
const emoji = typeof preferredSkinTone === 'number' && preferredSkinTone !== -1 && item?.types?.at(preferredSkinTone) ? item.types.at(preferredSkinTone) : item.code;
- onEmojiSelected(emoji ?? '', item, preferredSkinTone);
+ onEmojiSelected(emoji ?? '', item);
// On web, avoid this Enter default input action; otherwise, it will add a new line in the subsequently focused composer.
keyBoardEvent.preventDefault();
// On mWeb, avoid propagating this Enter keystroke to Pressable child component; otherwise, it will trigger the onEmojiSelected callback again.
@@ -282,7 +282,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji, ref}: EmojiPickerMenuPro
if (!('name' in item)) {
return;
}
- onEmojiSelected(emoji, item, preferredSkinTone);
+ onEmojiSelected(emoji, item);
})}
onHoverIn={() => {
setHighlightEmoji(false);
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/types.ts b/src/components/EmojiPicker/EmojiPickerMenu/types.ts
index 5f150c4541d8d..7db5821d2e12f 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/types.ts
+++ b/src/components/EmojiPicker/EmojiPickerMenu/types.ts
@@ -4,7 +4,7 @@ import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
type EmojiPickerMenuProps = {
/** Function to add the selected emoji to the main compose text input */
- onEmojiSelected: (emoji: string, emojiObject: Emoji, preferredSkinTone: number) => void;
+ onEmojiSelected: (emoji: string, emojiObject: Emoji) => void;
activeEmoji?: string;
diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx
index a1969e987444f..63fddb1881c37 100644
--- a/src/components/FlatList/index.tsx
+++ b/src/components/FlatList/index.tsx
@@ -46,7 +46,7 @@ type CustomFlatListProps = FlatListProps & {
shouldDisableVisibleContentPosition?: boolean;
};
-function MVCPFlatList({maintainVisibleContentPosition, horizontal = false, onScroll, ref, ...props}: CustomFlatListProps) {
+function MVCPFlatList({maintainVisibleContentPosition, horizontal = false, onScroll, initialNumToRender, ref, ...props}: CustomFlatListProps) {
const {minIndexForVisible: mvcpMinIndexForVisible, autoscrollToTopThreshold: mvcpAutoscrollToTopThreshold} = maintainVisibleContentPosition ?? {};
const scrollRef = useRef(null);
const prevFirstVisibleOffsetRef = useRef(0);
@@ -244,6 +244,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
onScroll={onScrollInternal}
scrollEventThrottle={1}
ref={onRef}
+ initialNumToRender={Math.max(0, initialNumToRender ?? 0) || undefined}
onLayout={(e) => {
isListRenderedRef.current = true;
if (!mutationObserverRef.current) {
diff --git a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx
index 2c624c243dab2..804f9fa3cb696 100644
--- a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx
+++ b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx
@@ -60,7 +60,7 @@ function BaseFloatingCameraButton({icon}: BaseFloatingCameraButtonProps) {
const quickActionReportID = policyChatForActivePolicy?.reportID ?? reportID;
Tab.setSelectedTab(CONST.TAB.IOU_REQUEST_TYPE, CONST.IOU.REQUEST_TYPE.SCAN);
- startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatForActivePolicy?.reportID, undefined, allTransactionDrafts);
+ startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatForActivePolicy?.reportID, undefined, allTransactionDrafts);
});
};
diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
index fb58356e7717a..2cf82c6bb63af 100644
--- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
+++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
@@ -32,7 +32,7 @@ function FocusTrapForScreen({children, focusTrapSettings}: FocusTrapProps) {
if (WIDE_LAYOUT_INACTIVE_SCREENS.includes(route.name) && !shouldUseNarrowLayout) {
return false;
}
- return true;
+ return isFocused;
}, [isFocused, shouldUseNarrowLayout, route.name, focusTrapSettings?.active]);
return (
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index 4c92ae1b64645..177128b7cfa19 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -5,6 +5,7 @@ import React, {createRef, useCallback, useEffect, useImperativeHandle, useMemo,
import {InteractionManager} from 'react-native';
import type {StyleProp, TextInputSubmitEditingEvent, ViewStyle} from 'react-native';
import {useInputBlurContext} from '@components/InputBlurContext';
+import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import useDebounceNonReactive from '@hooks/useDebounceNonReactive';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
@@ -52,7 +53,7 @@ type FormProviderProps = FormProps}) => ReactNode) | ReactNode;
/** Callback to validate the form */
- validate?: (values: FormOnyxValues) => FormInputErrors;
+ validate?: (values: FormOnyxValues, translate: LocalizedTranslate) => FormInputErrors;
/** Should validate function be called when input loose focus */
shouldValidateOnBlur?: boolean;
@@ -149,7 +150,7 @@ function FormProvider({
}
clearErrorFields(formID);
- const validateErrors: GenericFormInputErrors = validate?.(trimmedStringValues) ?? {};
+ const validateErrors: GenericFormInputErrors = validate?.(trimmedStringValues, translate) ?? {};
if (!allowHTML) {
// Validate the input for html tags. It should supersede any other error
diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx
index ed1a4b2901cb3..0db5e1a11e98e 100644
--- a/src/components/Form/InputWrapper.tsx
+++ b/src/components/Form/InputWrapper.tsx
@@ -12,6 +12,7 @@ import type {InputComponentBaseProps, InputComponentValueProps, ValidInputs, Val
type TextInputBasedComponents = [ComponentType, ComponentType];
+// eslint-disable-next-line unicorn/prefer-set-has
const textInputBasedComponents: TextInputBasedComponents = [TextInput, RoomNameInput];
type ComputedComponentSpecificRegistrationParams = {
diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx
index e94cf16a93b8a..44dc1d87eb2ad 100644
--- a/src/components/FullscreenLoadingIndicator.tsx
+++ b/src/components/FullscreenLoadingIndicator.tsx
@@ -19,6 +19,9 @@ type FullScreenLoadingIndicatorProps = {
/** Size of the icon */
iconSize?: FullScreenLoadingIndicatorIconSize;
+ /** Whether the "Go Back" button appears after a timeout. */
+ shouldUseGoBackButton?: boolean;
+
/** The ID of the test to be used for testing */
testID?: string;
@@ -26,17 +29,27 @@ type FullScreenLoadingIndicatorProps = {
extraLoadingContext?: ExtraLoadingContext;
};
-function FullScreenLoadingIndicator({style, iconSize = CONST.ACTIVITY_INDICATOR_SIZE.LARGE, testID = '', extraLoadingContext}: FullScreenLoadingIndicatorProps) {
+function FullScreenLoadingIndicator({
+ style,
+ iconSize = CONST.ACTIVITY_INDICATOR_SIZE.LARGE,
+ shouldUseGoBackButton = false,
+ testID = '',
+ extraLoadingContext,
+}: FullScreenLoadingIndicatorProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [showGoBackButton, setShowGoBackButton] = useState(false);
useEffect(() => {
+ if (!shouldUseGoBackButton) {
+ return;
+ }
+
const timeoutId = setTimeout(() => {
setShowGoBackButton(true);
}, CONST.TIMING.ACTIVITY_INDICATOR_TIMEOUT);
return () => clearTimeout(timeoutId);
- }, []);
+ }, [shouldUseGoBackButton]);
return (
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index a91b8df2a2edf..405cec53fd244 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -56,6 +56,7 @@ import Abacus from '@assets/images/simple-illustrations/simple-illustration__aba
import Alert from '@assets/images/simple-illustrations/simple-illustration__alert.svg';
import Approval from '@assets/images/simple-illustrations/simple-illustration__approval.svg';
import Binoculars from '@assets/images/simple-illustrations/simple-illustration__binoculars.svg';
+import BlueShield from '@assets/images/simple-illustrations/simple-illustration__blueshield.svg';
import Buildings from '@assets/images/simple-illustrations/simple-illustration__buildings.svg';
import CarIce from '@assets/images/simple-illustrations/simple-illustration__car-ice.svg';
import Car from '@assets/images/simple-illustrations/simple-illustration__car.svg';
@@ -224,4 +225,5 @@ export {
PaperAirplane,
CardReplacementSuccess,
EmptyShelves,
+ BlueShield,
};
diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts
index 35cd16d944f05..1f2059398fc8b 100644
--- a/src/components/Icon/chunks/illustrations.chunk.ts
+++ b/src/components/Icon/chunks/illustrations.chunk.ts
@@ -12,6 +12,7 @@ import LaptopWithSecondScreenX from '@assets/images/laptop-with-second-screen-x.
import TeleScope from '@assets/images/product-illustrations/telescope.svg';
// Simple Illustrations - Core ones that are actually used
import Accounting from '@assets/images/simple-illustrations/simple-illustration__accounting.svg';
+import BlueShield from '@assets/images/simple-illustrations/simple-illustration__blueshield.svg';
import Building from '@assets/images/simple-illustrations/simple-illustration__building.svg';
import CarIce from '@assets/images/simple-illustrations/simple-illustration__car-ice.svg';
import Coins from '@assets/images/simple-illustrations/simple-illustration__coins.svg';
@@ -68,6 +69,7 @@ const Illustrations = {
CompanyCard,
Workflows,
CarIce,
+ BlueShield,
Pencil,
// Legacy aliases for compatibility
Car: CompanyCard, // Fallback for Car illustration requests
@@ -126,6 +128,8 @@ function getIllustration(illustrationName: string): unknown {
return CompanyCard;
case 'CarIce':
return CarIce;
+ case 'BlueShield':
+ return BlueShield;
case 'Pencil':
return Pencil;
default:
diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx
index a0a62727f3a6e..f078e06ef33f5 100644
--- a/src/components/MapView/MapView.tsx
+++ b/src/components/MapView/MapView.tsx
@@ -173,6 +173,13 @@ function MapView({
return unsubscribe;
}, [navigation]);
+ useEffect(() => {
+ if (!isOffline) {
+ return;
+ }
+ setIsIdle(false);
+ }, [isOffline]);
+
useEffect(() => {
setAccessToken(accessToken).then((token) => {
if (!token) {
diff --git a/src/components/MigratedUserWelcomeModal.tsx b/src/components/MigratedUserWelcomeModal.tsx
index e658f6f6136f9..5943df77e296b 100644
--- a/src/components/MigratedUserWelcomeModal.tsx
+++ b/src/components/MigratedUserWelcomeModal.tsx
@@ -1,4 +1,5 @@
import {useRoute} from '@react-navigation/native';
+import {tryNewDotOnyxSelector} from '@selectors/Onboarding';
import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
@@ -13,7 +14,6 @@ import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {MigratedUserModalNavigatorParamList} from '@libs/Navigation/types';
-import {tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 4cd55943dc1f9..7221c6aa8c7ff 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -296,13 +296,10 @@ function MoneyReportHeader({
const transactionIDs = useMemo(() => transactions?.map((t) => t.transactionID) ?? [], [transactions]);
const messagePDF = useMemo(() => {
- if (!reportPDFFilename) {
- return translate('reportDetailsPage.waitForPDF');
- }
if (reportPDFFilename === CONST.REPORT_DETAILS_MENU_ITEM.ERROR) {
return translate('reportDetailsPage.errorPDF');
}
- return translate('reportDetailsPage.generatedPDF');
+ return translate('reportDetailsPage.waitForPDF');
}, [reportPDFFilename, translate]);
// Check if there is pending rter violation in all transactionViolations with given transactionIDs.
@@ -486,15 +483,6 @@ function MoneyReportHeader({
} else {
startApprovedAnimation();
approveMoneyRequest(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, true);
- if (currentSearchQueryJSON) {
- search({
- searchKey: currentSearchKey,
- shouldCalculateTotals,
- offset: 0,
- queryJSON: currentSearchQueryJSON,
- isOffline,
- });
- }
}
};
@@ -1133,6 +1121,14 @@ function MoneyReportHeader({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactionThreadReportID]);
+ useEffect(() => {
+ if (!isPDFModalVisible || !reportPDFFilename || reportPDFFilename === CONST.REPORT_DETAILS_MENU_ITEM.ERROR || isDownloadingPDF) {
+ return;
+ }
+ downloadReportPDF(reportPDFFilename, moneyRequestReport?.reportName ?? '');
+ setIsPDFModalVisible(false);
+ }, [isPDFModalVisible, reportPDFFilename, isDownloadingPDF, moneyRequestReport?.reportName]);
+
const shouldShowBackButton = shouldDisplayBackButton || shouldUseNarrowLayout;
const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', {connectionName: connectedIntegration}) : '';
@@ -1154,7 +1150,7 @@ function MoneyReportHeader({
};
}, []);
- if (isMobileSelectionModeEnabled) {
+ if (isMobileSelectionModeEnabled && shouldUseNarrowLayout) {
// If mobile selection mode is enabled but only one or no transactions remain, turn it off
const visibleTransactions = transactions.filter((t) => t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline);
if (visibleTransactions.length <= 1) {
@@ -1472,37 +1468,26 @@ function MoneyReportHeader({
innerContainerStyle={styles.pv0}
>
-
-
-
+
+
+
+
+
+ {messagePDF}
-
- {messagePDF}
- {!reportPDFFilename && (
-
- )}
+
+
- {!!reportPDFFilename && reportPDFFilename !== 'error' && (
-
diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx
index f0855a85fd537..9d20f6c248432 100644
--- a/src/components/MoneyReportHeaderStatusBar.tsx
+++ b/src/components/MoneyReportHeaderStatusBar.tsx
@@ -1,6 +1,7 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {parseMessage} from '@libs/NextStepUtils';
@@ -27,11 +28,12 @@ const iconMap: IconMap = {
function MoneyReportHeaderStatusBar({nextStep}: MoneyReportHeaderStatusBarProps) {
const styles = useThemeStyles();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const theme = useTheme();
const messageContent = useMemo(() => {
const messageArray = nextStep?.message;
- return parseMessage(messageArray);
- }, [nextStep?.message]);
+ return parseMessage(messageArray, currentUserPersonalDetails.email ?? '');
+ }, [nextStep?.message, currentUserPersonalDetails.email]);
return (
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index a58b779bea9a3..ba085062945ce 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -29,6 +29,7 @@ import {
setMoneyRequestTaxRate,
setSplitShares,
} from '@libs/actions/IOU';
+import {isCategoryDescriptionRequired} from '@libs/CategoryUtils';
import {convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {calculateAmount, insertTagIntoTransactionTagsString, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil} from '@libs/IOUUtils';
@@ -277,7 +278,9 @@ function MoneyRequestConfirmationList({
isTestDriveReceipt || isManagerMcTestReceipt,
);
- const policy = policyReal ?? policyDraft;
+ const {policyForMovingExpenses} = usePolicyForMovingExpenses();
+ const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK;
+ const policy = isTrackExpense ? policyForMovingExpenses : (policyReal ?? policyDraft);
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;
const defaultMileageRate = defaultMileageRateDraft ?? defaultMileageRateReal;
@@ -324,7 +327,6 @@ function MoneyRequestConfirmationList({
const prevCurrency = usePrevious(currency);
const prevSubRates = usePrevious(subRates);
- const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK;
const {shouldSelectPolicy} = usePolicyForMovingExpenses();
// A flag for showing the categories field
@@ -406,6 +408,11 @@ function MoneyRequestConfirmationList({
const isCategoryRequired = !!policy?.requiresCategory && !isTypeInvoice;
+ const isDescriptionRequired = useMemo(
+ () => isCategoryDescriptionRequired(policyCategories, iouCategory, policy?.areRulesEnabled),
+ [iouCategory, policyCategories, policy?.areRulesEnabled],
+ );
+
useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
@@ -1131,6 +1138,7 @@ function MoneyRequestConfirmationList({
iouIsReimbursable={iouIsReimbursable}
onToggleReimbursable={onToggleReimbursable}
isReceiptEditable={isReceiptEditable}
+ isDescriptionRequired={isDescriptionRequired}
/>
);
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index c1943a492fb4e..fac7f948ad84f 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -205,6 +205,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** Flag indicating if the IOU is reimbursable */
iouIsReimbursable: boolean;
+
+ /** Flag indicating if the description is required */
+ isDescriptionRequired: boolean;
};
function MoneyRequestConfirmationListFooter({
@@ -258,6 +261,7 @@ function MoneyRequestConfirmationListFooter({
iouIsReimbursable,
onToggleReimbursable,
isReceiptEditable = false,
+ isDescriptionRequired = false,
}: MoneyRequestConfirmationListFooterProps) {
const styles = useThemeStyles();
const {translate, toLocaleDigit, localeCompare} = useLocalize();
@@ -455,6 +459,7 @@ function MoneyRequestConfirmationListFooter({
disabled={didConfirm}
interactive={!isReadOnly}
numberOfLinesTitle={2}
+ rightLabel={isDescriptionRequired ? translate('common.required') : ''}
/>
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index 5f62518741eec..49dd550bd6314 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -400,7 +400,12 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
)}
)}
- {shouldDisplayTransactionNavigation && !!transaction && }
+ {shouldDisplayTransactionNavigation && !!transaction && (
+
+ )}
{!shouldDisplayNarrowMoreButton && (
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx
index eddf35edd28d7..f4a3a38847a93 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx
@@ -59,6 +59,9 @@ type MoneyRequestReportTransactionItemProps = ForwardedFSClassProps & {
/** Callback function that scrolls to this transaction in case it is newly added */
scrollToNewTransaction?: (offset: number) => void;
+
+ /** Callback function that navigates to the transaction thread */
+ onArrowRightPress?: (transactionID: string) => void;
};
const expenseHeaders = getExpenseHeaders();
@@ -78,6 +81,7 @@ function MoneyRequestReportTransactionItem({
taxAmountColumnSize,
scrollToNewTransaction,
forwardedFSClass,
+ onArrowRightPress,
}: MoneyRequestReportTransactionItemProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -154,6 +158,7 @@ function MoneyRequestReportTransactionItem({
onButtonPress={() => {
handleOnPress(transaction.transactionID);
}}
+ onArrowRightPress={() => onArrowRightPress?.(transaction.transactionID)}
/>
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index c801dbdf40b35..bb7a83ca47fb7 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -10,13 +10,13 @@ import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
-import openSearchReport from '@components/Search/openSearchReport';
import {useSearchContext} from '@components/Search/SearchContext';
import type {SearchColumnType, SortOrder} from '@components/Search/types';
import Text from '@components/Text';
import {WideRHPContext} from '@components/WideRHPContextProvider';
import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useHandleSelectionMode from '@hooks/useHandleSelectionMode';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useReportIsArchived from '@hooks/useReportIsArchived';
@@ -50,6 +50,7 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader';
@@ -163,6 +164,7 @@ function MoneyRequestReportTransactionList({
}, [hasPendingDeletionTransaction, transactions]);
const {selectedTransactionIDs, setSelectedTransactions, clearSelectedTransactions} = useSearchContext();
+ useHandleSelectionMode(selectedTransactionIDs);
const isMobileSelectionModeEnabled = useMobileSelectionMode();
const personalDetailsList = usePersonalDetails();
@@ -266,7 +268,7 @@ function MoneyRequestReportTransactionList({
if (reportIDToNavigate) {
markReportIDAsExpense(reportIDToNavigate);
}
- openSearchReport(routeParams.reportID, backTo);
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(routeParams));
});
},
[report, reportActions, sortedTransactions, markReportIDAsExpense],
@@ -312,6 +314,13 @@ function MoneyRequestReportTransactionList({
[isMobileSelectionModeEnabled, toggleTransaction, navigateToTransaction],
);
+ const handleArrowRightPress = useCallback(
+ (transactionID: string) => {
+ navigateToTransaction(transactionID);
+ },
+ [navigateToTransaction],
+ );
+
const listHorizontalPadding = styles.ph5;
const transactionItemFSClass = FS.getChatFSClass(personalDetailsList, report);
@@ -392,6 +401,7 @@ function MoneyRequestReportTransactionList({
// if we add few new transactions, then we need to scroll to the first one
scrollToNewTransaction={transaction.transactionID === newTransactions?.at(0)?.transactionID ? scrollToNewTransaction : undefined}
forwardedFSClass={transactionItemFSClass}
+ onArrowRightPress={handleArrowRightPress}
/>
);
})}
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
index 2168b7c8b502d..0e2762262159c 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
@@ -1,12 +1,13 @@
import {findFocusedRoute} from '@react-navigation/native';
-import React, {useContext, useEffect, useMemo} from 'react';
+import React, {useCallback, useContext, useEffect, useMemo} from 'react';
import type {GestureResponderEvent} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import PrevNextButtons from '@components/PrevNextButtons';
import {WideRHPContext} from '@components/WideRHPContextProvider';
import useOnyx from '@hooks/useOnyx';
import {createTransactionThreadReport, setOptimisticTransactionThread} from '@libs/actions/Report';
import {clearActiveTransactionIDs} from '@libs/actions/TransactionThreadNavigation';
+import type {SearchReportParamList} from '@libs/Navigation/types';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import Navigation from '@navigation/Navigation';
import navigationRef from '@navigation/navigationRef';
@@ -18,6 +19,7 @@ import getEmptyArray from '@src/types/utils/getEmptyArray';
type MoneyRequestReportRHPNavigationButtonsProps = {
currentTransactionID: string;
+ isFromReviewDuplicates?: boolean;
};
const parentReportActionIDsSelector = (reportActions: OnyxEntry) => {
@@ -32,24 +34,16 @@ const parentReportActionIDsSelector = (reportActions: OnyxEntry()] = useOnyx(ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS, {
canBeMissing: true,
});
- const [currentTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${currentTransactionID}`, {
- canBeMissing: true,
- });
- const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentTransaction?.reportID}`, {canBeMissing: true});
const {markReportIDAsExpense} = useContext(WideRHPContext);
- const [parentReportActions = new Map()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`, {
- canBeMissing: true,
- selector: parentReportActionIDsSelector,
- });
- const {prevTransactionID, prevParentReportAction, nextTransactionID, nextParentReportAction} = useMemo(() => {
+ const {prevTransactionID, nextTransactionID} = useMemo(() => {
if (!transactionIDsList || transactionIDsList.length < 2) {
- return {prevTransactionID: undefined, prevParentReportAction: undefined, nextTransactionID: undefined, nextParentReportAction: undefined};
+ return {prevTransactionID: undefined, nextTransactionID: undefined};
}
const currentTransactionIndex = transactionIDsList.findIndex((id) => id === currentTransactionID);
@@ -60,11 +54,48 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID}: MoneyR
return {
prevTransactionID: prevID,
nextTransactionID: nextID,
- prevParentReportAction: prevID ? parentReportActions.get(prevID) : undefined,
- nextParentReportAction: nextID ? parentReportActions.get(nextID) : undefined,
};
- }, [currentTransactionID, parentReportActions, transactionIDsList]);
+ }, [currentTransactionID, transactionIDsList]);
+
+ const prevNextTransactionsSelector = useCallback(
+ (allTransactions: OnyxCollection) =>
+ [currentTransactionID, prevTransactionID, nextTransactionID].map((transactionID) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]),
+ [currentTransactionID, nextTransactionID, prevTransactionID],
+ );
+
+ const [[currentTransaction, prevTransaction, nextTransaction] = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
+ canBeMissing: true,
+ selector: prevNextTransactionsSelector,
+ });
+
+ const parentReportActionsSelector = useCallback(
+ (allReportActions: OnyxCollection) => {
+ let reportActions = {};
+ for (const transaction of [currentTransaction, prevTransaction, nextTransaction]) {
+ reportActions = {...reportActions, ...allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction?.reportID}`]};
+ }
+ return parentReportActionIDsSelector(reportActions);
+ },
+ [currentTransaction, nextTransaction, prevTransaction],
+ );
+ const [parentReportActions = new Map()] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {
+ canBeMissing: true,
+ selector: parentReportActionsSelector,
+ });
+
+ const {prevParentReportAction, nextParentReportAction} = useMemo(() => {
+ if (!transactionIDsList || transactionIDsList.length < 2) {
+ return {prevParentReportAction: undefined, nextParentReportAction: undefined};
+ }
+
+ return {
+ prevParentReportAction: prevTransactionID ? parentReportActions.get(prevTransactionID) : undefined,
+ nextParentReportAction: nextTransactionID ? parentReportActions.get(nextTransactionID) : undefined,
+ };
+ }, [nextTransactionID, parentReportActions, prevTransactionID, transactionIDsList]);
+
+ const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentTransaction?.reportID}`, {canBeMissing: true});
const [prevThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${prevParentReportAction?.childReportID}`, {canBeMissing: true});
const [nextThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${nextParentReportAction?.childReportID}`, {canBeMissing: true});
@@ -75,7 +106,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID}: MoneyR
useEffect(() => {
return () => {
const focusedRoute = findFocusedRoute(navigationRef.getRootState());
- if (focusedRoute?.name === SCREENS.SEARCH.REPORT_RHP) {
+ if (focusedRoute?.name === SCREENS.SEARCH.REPORT_RHP || focusedRoute?.name === SCREENS.TRANSACTION_DUPLICATE.REVIEW) {
return;
}
clearActiveTransactionIDs();
@@ -89,7 +120,12 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID}: MoneyR
const onNext = (e: GestureResponderEvent | KeyboardEvent | undefined) => {
e?.preventDefault();
- const backTo = Navigation.getActiveRoute();
+ let backTo = Navigation.getActiveRoute();
+ if (isFromReviewDuplicates) {
+ const currentRoute = navigationRef.getCurrentRoute();
+ const params = currentRoute?.params as SearchReportParamList[typeof SCREENS.SEARCH.REPORT_RHP] | undefined;
+ backTo = params?.backTo ?? backTo;
+ }
const nextThreadReportID = nextParentReportAction?.childReportID;
const navigationParams = {reportID: nextThreadReportID, backTo};
@@ -112,7 +148,12 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID}: MoneyR
const onPrevious = (e: GestureResponderEvent | KeyboardEvent | undefined) => {
e?.preventDefault();
- const backTo = Navigation.getActiveRoute();
+ let backTo = Navigation.getActiveRoute();
+ if (isFromReviewDuplicates) {
+ const currentRoute = navigationRef.getCurrentRoute();
+ const params = currentRoute?.params as SearchReportParamList[typeof SCREENS.SEARCH.REPORT_RHP] | undefined;
+ backTo = params?.backTo ?? backTo;
+ }
const prevThreadReportID = prevParentReportAction?.childReportID;
const navigationParams = {reportID: prevThreadReportID, backTo};
diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx
index ae8c66057f6c9..9e638426cabbc 100644
--- a/src/components/Navigation/NavigationTabBar/index.tsx
+++ b/src/components/Navigation/NavigationTabBar/index.tsx
@@ -2,9 +2,12 @@ import {findFocusedRoute, StackActions, useNavigationState} from '@react-navigat
import reportsSelector from '@selectors/Attributes';
import React, {memo, useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
+import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
+import FloatingCameraButton from '@components/FloatingCameraButton';
import HeaderGap from '@components/HeaderGap';
import Icon from '@components/Icon';
+// import * as Expensicons from '@components/Icon/Expensicons';
import ImageSVG from '@components/ImageSVG';
import DebugTabView from '@components/Navigation/DebugTabView';
import {PressableWithFeedback} from '@components/Pressable';
@@ -41,14 +44,16 @@ import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
+import type {Policy} from '@src/types/onyx';
import NAVIGATION_TABS from './NAVIGATION_TABS';
type NavigationTabBarProps = {
selectedTab: ValueOf;
isTopLevelBar?: boolean;
+ shouldShowFloatingCameraButton?: boolean;
};
-function NavigationTabBar({selectedTab, isTopLevelBar = false}: NavigationTabBarProps) {
+function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatingCameraButton = true}: NavigationTabBarProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -78,17 +83,22 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false}: NavigationTabBar
const subscriptionPlan = useSubscriptionPlan();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Inbox', 'MoneySearch', 'Buildings'] as const);
+ const lastViewedPolicySelector = useCallback(
+ (policies: OnyxCollection) => {
+ if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !params?.policyID) {
+ return undefined;
+ }
+
+ return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${params.policyID}`];
+ },
+ [params?.policyID, lastWorkspacesTabNavigatorRoute],
+ );
+
const [lastViewedPolicy] = useOnyx(
ONYXKEYS.COLLECTION.POLICY,
{
canBeMissing: true,
- selector: (val) => {
- if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !params?.policyID) {
- return undefined;
- }
-
- return val?.[`${ONYXKEYS.COLLECTION.POLICY}${params.policyID}`];
- },
+ selector: lastViewedPolicySelector,
},
[navigationState],
);
@@ -121,13 +131,8 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false}: NavigationTabBar
return;
}
- if (!shouldUseNarrowLayout && isRoutePreloaded(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR)) {
- navigationRef.dispatch(StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR));
- return;
- }
-
Navigation.navigate(ROUTES.HOME);
- }, [selectedTab, shouldUseNarrowLayout]);
+ }, [selectedTab]);
const navigateToSearch = useCallback(() => {
if (selectedTab === NAVIGATION_TABS.SEARCH) {
@@ -448,6 +453,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false}: NavigationTabBar
onPress={navigateToSettings}
/>
+ {shouldShowFloatingCameraButton && }
>
);
}
diff --git a/src/components/Navigation/TopBar.tsx b/src/components/Navigation/TopBar.tsx
index fa8118f494f3b..84eb0a5a8c6eb 100644
--- a/src/components/Navigation/TopBar.tsx
+++ b/src/components/Navigation/TopBar.tsx
@@ -1,4 +1,4 @@
-import React, {useContext} from 'react';
+import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import LoadingBar from '@components/LoadingBar';
@@ -6,7 +6,6 @@ import {PressableWithoutFeedback} from '@components/Pressable';
import SearchButton from '@components/Search/SearchRouter/SearchButton';
import HelpButton from '@components/SidePanel/HelpComponents/HelpButton';
import Text from '@components/Text';
-import {WideRHPContext} from '@components/WideRHPContextProvider';
import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
@@ -33,8 +32,6 @@ function TopBar({breadcrumbLabel, shouldDisplaySearch = true, shouldDisplayHelpB
const [session] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector, canBeMissing: true});
const shouldShowLoadingBarForReports = useLoadingBarVisibility();
const isAnonymousUser = isAnonymousUserUtil(session);
- const {wideRHPRouteKeys} = useContext(WideRHPContext);
- const isWideRhpVisible = !!wideRHPRouteKeys.length;
const displaySignIn = isAnonymousUser;
const displaySearch = !isAnonymousUser && shouldDisplaySearch;
@@ -71,7 +68,7 @@ function TopBar({breadcrumbLabel, shouldDisplaySearch = true, shouldDisplayHelpB
{shouldDisplayHelpButton && }
{displaySearch && }
-
+
);
}
diff --git a/src/components/Navigation/TopLevelNavigationTabBar/index.tsx b/src/components/Navigation/TopLevelNavigationTabBar/index.tsx
index cd6a12f6b0f93..6712967b36cba 100644
--- a/src/components/Navigation/TopLevelNavigationTabBar/index.tsx
+++ b/src/components/Navigation/TopLevelNavigationTabBar/index.tsx
@@ -1,7 +1,6 @@
import type {ParamListBase} from '@react-navigation/native';
import React, {useContext, useEffect, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
-import FloatingCameraButton from '@components/FloatingCameraButton';
import {FullScreenBlockingViewContext} from '@components/FullScreenBlockingViewContextProvider';
import NavigationTabBar from '@components/Navigation/NavigationTabBar';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -75,7 +74,6 @@ function TopLevelNavigationTabBar({state}: TopLevelNavigationTabBarProps) {
selectedTab={selectedTab}
isTopLevelBar
/>
-
);
}
diff --git a/src/components/NumberWithSymbolForm.tsx b/src/components/NumberWithSymbolForm.tsx
index 17f8bb69ec0e0..bac31aad06aec 100644
--- a/src/components/NumberWithSymbolForm.tsx
+++ b/src/components/NumberWithSymbolForm.tsx
@@ -475,7 +475,7 @@ function NumberWithSymbolForm({
)}
{!!errorText && (
@@ -486,31 +486,29 @@ function NumberWithSymbolForm({
textInputComponent
)}
-
-
- {isSymbolPressable && canUseTouchScreen && (
-
- )}
- {allowFlippingAmount && canUseTouchScreen && (
-
- )}
-
+
+ {isSymbolPressable && canUseTouchScreen && (
+
+ )}
+ {allowFlippingAmount && canUseTouchScreen && (
+
+ )}
{shouldShowBigNumberPad || !!footer ? (
diff --git a/src/components/OnyxListItemProvider.tsx b/src/components/OnyxListItemProvider.tsx
index 50fe162791270..21104bfda7895 100644
--- a/src/components/OnyxListItemProvider.tsx
+++ b/src/components/OnyxListItemProvider.tsx
@@ -18,6 +18,7 @@ const [PolicyTagsProvider, , usePolicyTags] = createOnyxContext(ONYXKEYS.COLLECT
const [ReportTransactionsAndViolationsProvider, , useAllReportsTransactionsAndViolations] = createOnyxContext(ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS);
const [CardListProvider, , useCardList] = createOnyxContext(ONYXKEYS.CARD_LIST);
const [WorkspaceCardListProvider, , useWorkspaceCardList] = createOnyxContext(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST);
+const [OnboardingValuesProvider, , useOnboardingValues] = createOnyxContext(ONYXKEYS.NVP_ONBOARDING);
type OnyxListItemProviderProps = {
/** Rendered child component */
@@ -38,6 +39,7 @@ function OnyxListItemProvider(props: OnyxListItemProviderProps) {
ReportTransactionsAndViolationsProvider,
CardListProvider,
WorkspaceCardListProvider,
+ OnboardingValuesProvider,
]}
>
{props.children}
@@ -63,4 +65,5 @@ export {
useAllReportsTransactionsAndViolations,
useCardList,
useWorkspaceCardList,
+ useOnboardingValues,
};
diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx
index 4d1de8cf8fec1..8df332f00d7b6 100644
--- a/src/components/ParentNavigationSubtitle.tsx
+++ b/src/components/ParentNavigationSubtitle.tsx
@@ -125,12 +125,11 @@ function ParentNavigationSubtitle({
};
return (
-
+
{!!statusText && (
)}
{!!reportName && (
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index a3644c0299ccd..dfb6200b79329 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -509,6 +509,32 @@ function BasePopoverMenu({
...restMenuContainerStyle
} = StyleSheet.flatten(menuContainerStyle) ?? {};
+ const {
+ paddingVertical: containerPaddingVertical,
+ paddingTop: containerPaddingTop,
+ paddingBottom: containerPaddingBottom,
+ ...restContainerStyles
+ } = StyleSheet.flatten(containerStyles) ?? {};
+
+ const scrollViewPaddingStyles = useMemo(
+ () => ({
+ paddingTop: paddingTop ?? containerPaddingTop ?? menuContainerPaddingTop,
+ paddingBottom: paddingBottom ?? containerPaddingBottom ?? menuContainerPaddingBottom,
+ paddingVertical: paddingVertical ?? containerPaddingVertical ?? menuContainerPaddingVertical ?? 0,
+ }),
+ [
+ paddingTop,
+ containerPaddingTop,
+ menuContainerPaddingTop,
+ paddingBottom,
+ containerPaddingBottom,
+ menuContainerPaddingBottom,
+ paddingVertical,
+ containerPaddingVertical,
+ menuContainerPaddingVertical,
+ ],
+ );
+
return (
{renderWithConditionalWrapper(
shouldUseScrollView,
- [
- {
- paddingTop: menuContainerPaddingTop ?? paddingTop,
- paddingBottom: menuContainerPaddingBottom ?? paddingBottom,
- paddingVertical: paddingVertical ?? menuContainerPaddingVertical ?? 0,
- },
- restScrollContainerStyle,
- ],
+ [scrollViewPaddingStyles, restScrollContainerStyle],
[renderHeaderText(), enteredSubMenuIndexes.length > 0 && renderBackButtonItem(), renderedMenuItems],
)}
diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx
index 4837c881cc43f..c4f79a128d7d1 100644
--- a/src/components/ProductTrainingContext/index.tsx
+++ b/src/components/ProductTrainingContext/index.tsx
@@ -1,4 +1,5 @@
import {isActingAsDelegateSelector} from '@selectors/Account';
+import {hasCompletedGuidedSetupFlowSelector} from '@selectors/Onboarding';
import {emailSelector} from '@selectors/Session';
import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
@@ -13,7 +14,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSidePanel from '@hooks/useSidePanel';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
import {getActiveAdminWorkspaces, getActiveEmployeeWorkspaces, getGroupPaidPoliciesWithExpenseChatEnabled} from '@libs/PolicyUtils';
import isProductTrainingElementDismissed from '@libs/TooltipUtils';
import variables from '@styles/variables';
diff --git a/src/components/Reactions/AddReactionBubble.tsx b/src/components/Reactions/AddReactionBubble.tsx
index d98e2ac9ecf7d..5bfa95ede5d42 100644
--- a/src/components/Reactions/AddReactionBubble.tsx
+++ b/src/components/Reactions/AddReactionBubble.tsx
@@ -38,7 +38,7 @@ type AddReactionBubbleProps = {
/**
* Called when the user selects an emoji.
*/
- onSelectEmoji: (emoji: Emoji, preferredSkinTone: number) => void;
+ onSelectEmoji: (emoji: Emoji) => void;
/**
* ReportAction for EmojiPicker.
@@ -63,8 +63,8 @@ function AddReactionBubble({onSelectEmoji, reportAction, onPressOpenPicker, onWi
onModalHide: () => {
setIsEmojiPickerActive?.(false);
},
- onEmojiSelected: (emojiCode, emojiObject, preferredSkinTone) => {
- onSelectEmoji(emojiObject, preferredSkinTone);
+ onEmojiSelected: (emojiCode, emojiObject) => {
+ onSelectEmoji(emojiObject);
},
emojiPopoverAnchor: refParam ?? ref,
anchorOrigin,
diff --git a/src/components/Reactions/MiniQuickEmojiReactions.tsx b/src/components/Reactions/MiniQuickEmojiReactions.tsx
index bbadbe7d46f90..493a059a33d04 100644
--- a/src/components/Reactions/MiniQuickEmojiReactions.tsx
+++ b/src/components/Reactions/MiniQuickEmojiReactions.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useRef} from 'react';
+import React, {useRef} from 'react';
import {View} from 'react-native';
import type {Emoji} from '@assets/emojis/types';
import BaseMiniContextMenuItem from '@components/BaseMiniContextMenuItem';
@@ -42,19 +42,12 @@ function MiniQuickEmojiReactions({reportAction, reportActionID, onEmojiSelected,
const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true});
const [emojiReactions = getEmptyObject()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, {canBeMissing: true});
- const onEmojiSelectedWithReactions = useCallback(
- (emoji: Emoji, skinTone: number) => {
- onEmojiSelected(emoji, emojiReactions, skinTone);
- },
- [onEmojiSelected, emojiReactions],
- );
-
const openEmojiPicker = () => {
onPressOpenPicker();
showEmojiPicker({
onModalHide: onEmojiPickerClosed,
- onEmojiSelected: (_emojiCode, emojiObject, skinTone) => {
- onEmojiSelectedWithReactions(emojiObject, skinTone);
+ onEmojiSelected: (_emojiCode, emojiObject) => {
+ onEmojiSelected(emojiObject, emojiReactions);
},
emojiPopoverAnchor: ref,
id: reportAction.reportActionID,
@@ -68,7 +61,7 @@ function MiniQuickEmojiReactions({reportAction, reportActionID, onEmojiSelected,
key={emoji.name}
isDelayButtonStateComplete={false}
tooltipText={`:${getLocalizedEmojiName(emoji.name, preferredLocale)}:`}
- onPress={callFunctionIfActionIsAllowed(() => onEmojiSelected(emoji, emojiReactions, preferredSkinTone))}
+ onPress={callFunctionIfActionIsAllowed(() => onEmojiSelected(emoji, emojiReactions))}
>
()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, {canBeMissing: true});
- const onEmojiSelectedWithReactions = useCallback(
- (emoji: Emoji, skinTone: number) => {
- onEmojiSelected(emoji, emojiReactions, skinTone);
- },
- [onEmojiSelected, emojiReactions],
- );
-
return (
{CONST.QUICK_REACTIONS.map((emoji: Emoji) => (
@@ -46,7 +39,7 @@ function BaseQuickEmojiReactions({
onEmojiSelected(emoji, emojiReactions, preferredSkinTone))}
+ onPress={callFunctionIfActionIsAllowed(() => onEmojiSelected(emoji, emojiReactions))}
/>
@@ -55,7 +48,7 @@ function BaseQuickEmojiReactions({
isContextMenu
onPressOpenPicker={onPressOpenPicker}
onWillShowPicker={onWillShowPicker}
- onSelectEmoji={onEmojiSelectedWithReactions}
+ onSelectEmoji={(emoji) => onEmojiSelected(emoji, emojiReactions)}
reportAction={reportAction}
setIsEmojiPickerActive={setIsEmojiPickerActive}
/>
diff --git a/src/components/Reactions/QuickEmojiReactions/types.ts b/src/components/Reactions/QuickEmojiReactions/types.ts
index 7197b317ba034..d3b377099c7c6 100644
--- a/src/components/Reactions/QuickEmojiReactions/types.ts
+++ b/src/components/Reactions/QuickEmojiReactions/types.ts
@@ -13,7 +13,7 @@ type CloseContextMenuCallback = () => void;
type BaseReactionsProps = {
/** Callback to fire when an emoji is selected. */
- onEmojiSelected: (emoji: Emoji, emojiReactions: OnyxEntry, preferredSkinTone: number) => void;
+ onEmojiSelected: (emoji: Emoji, emojiReactions: OnyxEntry) => void;
/**
* Will be called when the emoji picker is about to show.
diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.tsx b/src/components/Reactions/ReportActionItemEmojiReactions.tsx
index 905595277d6dc..930a5ee2e9840 100644
--- a/src/components/Reactions/ReportActionItemEmojiReactions.tsx
+++ b/src/components/Reactions/ReportActionItemEmojiReactions.tsx
@@ -7,13 +7,11 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
-import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import {getEmojiReactionDetails, getLocalizedEmojiName} from '@libs/EmojiUtils';
import {ReactionListContext} from '@pages/home/ReportScreenContext';
import type {ReactionListAnchor, ReactionListEvent} from '@pages/home/ReportScreenContext';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
import type {Locale, ReportAction, ReportActionReactions} from '@src/types/onyx';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import AddReactionBubble from './AddReactionBubble';
@@ -35,7 +33,7 @@ type ReportActionItemEmojiReactionsProps = WithCurrentUserPersonalDetailsProps &
* This can also be an emoji the user already reacted with,
* hence this function asks to toggle the reaction by emoji.
*/
- toggleReaction: (emoji: Emoji, preferredSkinTone: number, ignoreSkinToneOnCompare?: boolean) => void;
+ toggleReaction: (emoji: Emoji, ignoreSkinToneOnCompare?: boolean) => void;
/** We disable reacting with emojis on report actions that have errors */
shouldBlockReactions?: boolean;
@@ -89,7 +87,6 @@ function ReportActionItemEmojiReactions({
const styles = useThemeStyles();
const reactionListRef = useContext(ReactionListContext);
const popoverReactionListAnchors = useRef({});
- const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true});
const reportActionID = reportAction.reportActionID;
@@ -107,7 +104,7 @@ function ReportActionItemEmojiReactions({
}
const onPress = () => {
- toggleReaction(emoji, preferredSkinTone, true);
+ toggleReaction(emoji, true);
};
const onReactionListOpen = (event: ReactionListEvent) => {
diff --git a/src/components/ReceiptImage/index.tsx b/src/components/ReceiptImage/index.tsx
index ccbb6cde4e86a..1453f02212881 100644
--- a/src/components/ReceiptImage/index.tsx
+++ b/src/components/ReceiptImage/index.tsx
@@ -123,6 +123,9 @@ type ReceiptImageProps = (
/** Callback to be called when the image loads */
onLoad?: (event?: {nativeEvent: {width: number; height: number}}) => void;
+
+ /** Callback to be called when the image fails to load */
+ onLoadFailure?: () => void;
};
function ReceiptImage({
@@ -150,6 +153,7 @@ function ReceiptImage({
loadingIndicatorStyles,
thumbnailContainerStyles,
onLoad,
+ onLoadFailure,
}: ReceiptImageProps) {
const styles = useThemeStyles();
const [receiptImageWidth, setReceiptImageWidth] = useState(undefined);
@@ -217,6 +221,7 @@ function ReceiptImage({
fallbackIconBackground={fallbackIconBackground}
objectPosition={shouldUseInitialObjectPosition ? CONST.IMAGE_OBJECT_POSITION.INITIAL : CONST.IMAGE_OBJECT_POSITION.TOP}
onLoad={onLoad}
+ onLoadFailure={onLoadFailure}
/>
);
}
@@ -239,6 +244,7 @@ function ReceiptImage({
onLoad={onLoad}
shouldCalculateAspectRatioForWideImage={shouldUseFullHeight}
imageWidthToCalculateHeight={receiptImageWidth}
+ onError={onLoadFailure}
/>
);
}
diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx
index 870e81cbb2304..3ba9c060d3f97 100644
--- a/src/components/RenderHTML.tsx
+++ b/src/components/RenderHTML.tsx
@@ -1,18 +1,24 @@
import React, {useMemo} from 'react';
-import {RenderHTMLSource} from 'react-native-render-html';
+import {RenderHTMLConfigProvider, RenderHTMLSource} from 'react-native-render-html';
+import type {RenderersProps} from 'react-native-render-html';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Parser from '@libs/Parser';
+type LinkPressHandler = NonNullable['onPress'];
+
type RenderHTMLProps = {
/** HTML string to render */
html: string;
+
+ /** Callback to handle link press */
+ onLinkPress?: LinkPressHandler;
};
// We are using the explicit composite architecture for performance gains.
// Configuration for RenderHTML is handled in a top-level component providing
// context to RenderHTMLSource components. See https://git.io/JRcZb
// The provider is available at src/components/HTMLEngineProvider/
-function RenderHTML({html: htmlParam}: RenderHTMLProps) {
+function RenderHTML({html: htmlParam, onLinkPress}: RenderHTMLProps) {
const {windowWidth} = useWindowDimensions();
const html = useMemo(() => {
return (
@@ -25,12 +31,32 @@ function RenderHTML({html: htmlParam}: RenderHTMLProps) {
.replace(/(<\/emoji[^>]*>)(?:<\/emoji[^>]*>)+/g, '$1')
);
}, [htmlParam]);
- return (
+
+ const renderersProps = useMemo(() => {
+ return {
+ a: {
+ onPress: onLinkPress,
+ },
+ };
+ }, [onLinkPress]);
+
+ const htmlSource = (
);
+
+ return onLinkPress ? (
+
+ {htmlSource}
+
+ ) : (
+ htmlSource
+ );
}
RenderHTML.displayName = 'RenderHTML';
diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx
index ba907472226eb..f959a03eeb849 100644
--- a/src/components/ReportActionItem/MoneyRequestAction.tsx
+++ b/src/components/ReportActionItem/MoneyRequestAction.tsx
@@ -87,7 +87,7 @@ function MoneyRequestAction({
isWhisper = false,
shouldDisplayContextMenu = true,
}: MoneyRequestActionProps) {
- const {shouldOpenReportInRHP} = useContext(ReportActionItemContext);
+ const {shouldOpenReportInRHP, onPreviewPressed} = useContext(ReportActionItemContext);
const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`];
const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`];
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, {canEvict: false, canBeMissing: true});
@@ -108,6 +108,10 @@ function MoneyRequestAction({
const reportPreviewStyles = StyleUtils.getMoneyRequestReportPreviewStyle(shouldUseNarrowLayout, 1, undefined, undefined);
const onMoneyRequestPreviewPressed = () => {
+ if (onPreviewPressed && action?.childReportID) {
+ onPreviewPressed(action?.childReportID);
+ return;
+ }
if (contextMenuRef.current?.isContextMenuOpening) {
return;
}
diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
index 8878fcd57b2bb..fce65f9885736 100644
--- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
@@ -15,7 +15,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionViolations from '@hooks/useTransactionViolations';
-import {isReceiptError} from '@libs/ErrorUtils';
+import {getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from '@libs/ErrorUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
@@ -74,6 +74,7 @@ type MoneyRequestReceiptViewProps = {
isDisplayedInWideRHP?: boolean;
};
+// eslint-disable-next-line unicorn/prefer-set-has
const receiptImageViolationNames: OnyxTypes.ViolationName[] = [
CONST.VIOLATIONS.RECEIPT_REQUIRED,
CONST.VIOLATIONS.RECEIPT_NOT_SMART_SCANNED,
@@ -83,6 +84,7 @@ const receiptImageViolationNames: OnyxTypes.ViolationName[] = [
CONST.VIOLATIONS.RECEIPT_GENERATED_WITH_AI,
];
+// eslint-disable-next-line unicorn/prefer-set-has
const receiptFieldViolationNames: OnyxTypes.ViolationName[] = [CONST.VIOLATIONS.MODIFIED_AMOUNT, CONST.VIOLATIONS.MODIFIED_DATE];
function MoneyRequestReceiptView({
@@ -97,7 +99,7 @@ function MoneyRequestReceiptView({
}: MoneyRequestReceiptViewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout();
const {getReportRHPActiveRoute} = useActiveRoute();
const parentReportID = report?.parentReportID;
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`];
@@ -108,7 +110,6 @@ function MoneyRequestReceiptView({
});
const [isLoading, setIsLoading] = useState(true);
-
const parentReportAction = report?.parentReportActionID ? parentReportActions?.[report.parentReportActionID] : undefined;
const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(parentReportAction);
const isTrackExpense = isTrackExpenseReport(report);
@@ -190,11 +191,15 @@ function MoneyRequestReceiptView({
!isTransactionScanning && (hasReceipt || !!receiptRequiredViolation || !!customRulesViolation) && !!(receiptViolations.length || didReceiptScanSucceed) && isPaidGroupPolicy(report);
const shouldShowReceiptAudit = !isInvoice && (shouldShowReceiptEmptyState || hasReceipt);
- const errors = {
- ...(transaction?.errorFields?.route ?? transaction?.errorFields?.waypoints ?? transaction?.errors),
- ...parentReportAction?.errors,
- };
-
+ const errorsWithoutReportCreation = useMemo(
+ () => ({
+ ...(transaction?.errorFields?.route ?? transaction?.errorFields?.waypoints ?? transaction?.errors),
+ ...parentReportAction?.errors,
+ }),
+ [transaction?.errorFields?.route, transaction?.errorFields?.waypoints, transaction?.errors, parentReportAction?.errors],
+ );
+ const reportCreationError = useMemo(() => (getCreationReportErrors(report) ? getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage') : {}), [report]);
+ const errors = useMemo(() => ({...errorsWithoutReportCreation, ...reportCreationError}), [errorsWithoutReportCreation, reportCreationError]);
const showReceiptErrorWithEmptyState = shouldShowReceiptEmptyState && !hasReceipt && !isEmptyObject(errors);
const [showConfirmDismissReceiptError, setShowConfirmDismissReceiptError] = useState(false);
@@ -221,10 +226,30 @@ function MoneyRequestReceiptView({
clearAllRelatedReportActionErrors(report.reportID, parentReportAction);
return;
}
- revert(transaction, getLastModifiedExpense(report?.reportID));
- clearError(transaction.transactionID);
- clearAllRelatedReportActionErrors(report.reportID, parentReportAction);
- }, [transaction, chatReport, parentReportAction, linkedTransactionID, report?.reportID, iouReport, chatIOUReport, isChatIOUReportArchived]);
+ if (!isEmptyObject(errorsWithoutReportCreation)) {
+ revert(transaction, getLastModifiedExpense(report?.reportID));
+ clearError(transaction.transactionID);
+ clearAllRelatedReportActionErrors(report.reportID, parentReportAction);
+ }
+ if (!isEmptyObject(reportCreationError)) {
+ if (isInNarrowPaneModal) {
+ Navigation.goBack();
+ }
+ navigateToConciergeChatAndDeleteReport(report.reportID, true, true);
+ }
+ }, [
+ transaction,
+ chatReport,
+ parentReportAction,
+ linkedTransactionID,
+ report?.reportID,
+ iouReport,
+ chatIOUReport,
+ isChatIOUReportArchived,
+ errorsWithoutReportCreation,
+ reportCreationError,
+ isInNarrowPaneModal,
+ ]);
let receiptStyle: StyleProp;
@@ -320,10 +345,13 @@ function MoneyRequestReceiptView({
mergeTransactionID={mergeTransactionID}
report={report}
onLoad={() => setIsLoading(false)}
+ onLoadFailure={() => setIsLoading(false)}
/>
)}
- {!!shouldShowAuditMessage && hasReceipt && !isLoading && receiptAuditMessagesRow}
+ {/* For WideRHP (fillSpace is true), we need to wait for the image to load to get the correct size, then display the violation message to avoid the jumping issue.
+ Otherwise (when fillSpace is false), we use a fixed size, so there's no need to wait for the image to load. */}
+ {!!shouldShowAuditMessage && hasReceipt && (!isLoading || !fillSpace) && receiptAuditMessagesRow}
)}
{!shouldShowReceiptEmptyState && !hasReceipt && }
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index e7c04524c1a55..1960ca0d939aa 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -14,9 +14,11 @@ import Text from '@components/Text';
import ViolationMessages from '@components/ViolationMessages';
import {WideRHPContext} from '@components/WideRHPContextProvider';
import useActiveRoute from '@hooks/useActiveRoute';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
+import usePermissions from '@hooks/usePermissions';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
import usePrevious from '@hooks/usePrevious';
import useReportIsArchived from '@hooks/useReportIsArchived';
@@ -32,6 +34,7 @@ import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import {getReportIDForExpense} from '@libs/MergeTransactionUtils';
import {hasEnabledOptions} from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import {getLengthOfTag, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isTaxTrackingEnabled} from '@libs/PolicyUtils';
@@ -43,6 +46,7 @@ import {
canEditMoneyRequest,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
getReportName,
+ getReportOrDraftReport,
getTransactionDetails,
getTripIDFromTransactionParentReportID,
isInvoiceReport,
@@ -276,7 +280,8 @@ function MoneyRequestView({
// transactionTag can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const shouldShowTag = isPolicyExpenseChat && (transactionTag || hasEnabledTags(policyTagLists));
- const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
+ const shouldShowBillable =
+ (isPolicyExpenseChat || isExpenseUnreported) && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
const isCurrentTransactionReimbursableDifferentFromPolicyDefault =
policy?.defaultReimbursable !== undefined && !!(updatedTransaction?.reimbursable ?? transactionReimbursable) !== policy.defaultReimbursable;
const shouldShowReimbursable =
@@ -287,6 +292,9 @@ function MoneyRequestView({
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest);
const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID);
const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID;
+ const {isBetaEnabled} = usePermissions();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
const {getViolationsForField} = useViolations(transactionViolations ?? [], isTransactionScanning || !isPaidGroupPolicy(report));
const hasViolations = useCallback(
@@ -328,9 +336,19 @@ function MoneyRequestView({
if (newBillable === getBillable(transaction) || !transaction?.transactionID || !report?.reportID) {
return;
}
- updateMoneyRequestBillable(transaction.transactionID, report?.reportID, newBillable, policy, policyTagList, policyCategories);
+ updateMoneyRequestBillable(
+ transaction.transactionID,
+ report?.reportID,
+ newBillable,
+ policy,
+ policyTagList,
+ policyCategories,
+ currentUserPersonalDetails.accountID,
+ currentUserPersonalDetails.login ?? '',
+ isASAPSubmitBetaEnabled,
+ );
},
- [transaction, report?.reportID, policy, policyTagList, policyCategories],
+ [transaction, report?.reportID, policy, policyTagList, policyCategories, currentUserPersonalDetails.accountID, currentUserPersonalDetails.login, isASAPSubmitBetaEnabled],
);
const saveReimbursable = useCallback(
@@ -339,9 +357,19 @@ function MoneyRequestView({
if (newReimbursable === getReimbursable(transaction) || !transaction?.transactionID || !report?.reportID) {
return;
}
- updateMoneyRequestReimbursable(transaction.transactionID, report?.reportID, newReimbursable, policy, policyTagList, policyCategories);
+ updateMoneyRequestReimbursable(
+ transaction.transactionID,
+ report?.reportID,
+ newReimbursable,
+ policy,
+ policyTagList,
+ policyCategories,
+ currentUserPersonalDetails.accountID,
+ currentUserPersonalDetails.login ?? '',
+ isASAPSubmitBetaEnabled,
+ );
},
- [transaction, report, policy, policyTagList, policyCategories],
+ [transaction, report, policy, policyTagList, policyCategories, currentUserPersonalDetails.accountID, currentUserPersonalDetails.login, isASAPSubmitBetaEnabled],
);
if (isCardTransaction) {
@@ -412,11 +440,7 @@ function MoneyRequestView({
// Return violations if there are any
if (field !== 'merchant' && hasViolations(field, data, policyHasDependentTags, tagValue)) {
const violations = getViolationsForField(field, data, policyHasDependentTags, tagValue);
- const firstViolation = violations.at(0);
-
- if (firstViolation) {
- return ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit);
- }
+ return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit)).join('. ')}.`;
}
return '';
@@ -548,7 +572,7 @@ function MoneyRequestView({
const getAttendeesTitle = useMemo(() => {
return Array.isArray(actualAttendees) ? actualAttendees.map((item) => item?.displayName ?? item?.login).join(', ') : '';
- }, [transactionAttendees]);
+ }, [actualAttendees]);
const attendeesCopyValue = !canEdit ? getAttendeesTitle : undefined;
const previousTagLength = getLengthOfTag(previousTag ?? '');
@@ -615,9 +639,9 @@ function MoneyRequestView({
);
});
- const reportNameToDisplay = isFromMergeTransaction ? updatedTransaction?.reportName : getReportName(parentReport) || parentReport?.reportName;
- const shouldShowReport = !!parentReportID || (isFromMergeTransaction && !!reportNameToDisplay);
- const reportCopyValue = !canEditReport ? reportNameToDisplay : undefined;
+ const actualParentReport = isFromMergeTransaction ? getReportOrDraftReport(getReportIDForExpense(updatedTransaction)) : parentReport;
+ const shouldShowReport = !!parentReportID || !!actualParentReport;
+ const reportCopyValue = !canEditReport ? getReportName(actualParentReport) || actualParentReport?.reportName : undefined;
// In this case we want to use this value. The shouldUseNarrowLayout will always be true as this case is handled when we display ReportScreen in RHP.
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -914,7 +938,7 @@ function MoneyRequestView({
void;
+
+ /** Callback to be called when the image fails to load */
+ onLoadFailure?: () => void;
};
/**
@@ -106,6 +109,7 @@ function ReportActionItemImage({
report: reportProp,
shouldUseThumbnailImage,
onLoad,
+ onLoadFailure,
}: ReportActionItemImageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -195,6 +199,7 @@ function ReportActionItemImage({
{...propsObj}
onLoad={onLoad}
shouldUseFullHeight={shouldUseFullHeight}
+ onLoadFailure={onLoadFailure}
/>
)}
@@ -208,6 +213,7 @@ function ReportActionItemImage({
shouldUseFullHeight={shouldUseFullHeight}
thumbnailContainerStyles={styles.thumbnailImageContainerHover}
onLoad={onLoad}
+ onLoadFailure={onLoadFailure}
/>
);
}
diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx
index 84cf6f57a4b35..47138ed0f59ee 100644
--- a/src/components/ReportWelcomeText.tsx
+++ b/src/components/ReportWelcomeText.tsx
@@ -25,7 +25,6 @@ import {
temporary_getMoneyRequestOptions,
} from '@libs/ReportUtils';
import SidebarUtils from '@libs/SidebarUtils';
-import TextWithEmojiFragment from '@pages/home/report/comment/TextWithEmojiFragment';
import CONST from '@src/CONST';
import type {IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -152,7 +151,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) {
{isSelfDM && (
{welcomeMessage.messageText}
- {shouldShowUsePlusButtonText && }
+ {shouldShowUsePlusButtonText && {translate('reportActionsView.usePlusButton', {additionalText})}}
)}
{isSystemChat && (
@@ -184,7 +183,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) {
{index < displayNamesWithTooltips.length - 2 && , }
))}
- {shouldShowUsePlusButtonText && }
+ {shouldShowUsePlusButtonText && {translate('reportActionsView.usePlusButton', {additionalText})}}
{isConciergeChatReport(report) && {translate('reportActionsView.askConcierge')}}
)}
diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx
index bc451045ccd25..434acd7387351 100644
--- a/src/components/ScrollOffsetContextProvider.tsx
+++ b/src/components/ScrollOffsetContextProvider.tsx
@@ -101,6 +101,7 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp
const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback(
(state) => {
const sidebarRoutes = state.routes.filter((route) => isSidebarScreenName(route.name));
+ // eslint-disable-next-line unicorn/prefer-set-has
const existingScreenKeys = sidebarRoutes.map(getKey);
const focusedRoute = findFocusedRoute(state);
diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx
index 71382cc0d0996..81596ebf2d892 100644
--- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx
+++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx
@@ -58,6 +58,7 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps)
const [searchTerm, setSearchTerm] = useState('');
const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true});
const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true});
+ const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
const initialSelectedOptions = useMemo(() => {
return value.reduce((acc, id) => {
const participant = personalDetails?.[id];
@@ -89,13 +90,14 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps)
personalDetails: options.personalDetails,
},
draftComments,
+ nvpDismissedProductTraining,
{
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
includeCurrentUser: true,
},
countryCode,
);
- }, [options.reports, options.personalDetails, draftComments, countryCode]);
+ }, [options.reports, options.personalDetails, draftComments, nvpDismissedProductTraining, countryCode]);
const filteredOptions = useMemo(() => {
return filterAndOrderOptions(optionsList, cleanSearchTerm, countryCode, {
diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx
index 010afa6e4e2b4..8c4bcb15ca6ec 100644
--- a/src/components/Search/SearchAutocompleteList.tsx
+++ b/src/components/Search/SearchAutocompleteList.tsx
@@ -181,6 +181,7 @@ function SearchAutocompleteList({
const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true});
const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true});
+ const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES, {canBeMissing: true});
const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false});
const taxRates = getAllTaxRates();
@@ -193,6 +194,7 @@ function SearchAutocompleteList({
return getSearchOptions({
options,
draftComments,
+ nvpDismissedProductTraining,
betas: betas ?? [],
isUsedInChatFinder: true,
includeReadOnly: true,
@@ -204,7 +206,7 @@ function SearchAutocompleteList({
countryCode,
shouldShowGBR: false,
});
- }, [areOptionsInitialized, options, draftComments, betas, autocompleteQueryValue, countryCode]);
+ }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, autocompleteQueryValue, countryCode]);
const [isInitialRender, setIsInitialRender] = useState(true);
const parsedQuery = parseForAutocomplete(autocompleteQueryValue);
@@ -329,6 +331,7 @@ function SearchAutocompleteList({
}
}
+ // eslint-disable-next-line unicorn/prefer-set-has
const alreadyAutocompletedKeys = ranges
.filter((range) => {
return autocompleteKey && range.key === autocompleteKey;
@@ -403,6 +406,7 @@ function SearchAutocompleteList({
const participants = getSearchOptions({
options,
draftComments,
+ nvpDismissedProductTraining,
betas: betas ?? [],
isUsedInChatFinder: true,
includeReadOnly: true,
@@ -426,6 +430,7 @@ function SearchAutocompleteList({
const filteredReports = getSearchOptions({
options,
draftComments,
+ nvpDismissedProductTraining,
betas: betas ?? [],
isUsedInChatFinder: true,
includeReadOnly: true,
@@ -594,6 +599,7 @@ function SearchAutocompleteList({
taxAutocompleteList,
options,
draftComments,
+ nvpDismissedProductTraining,
betas,
countryCode,
currentUserLogin,
@@ -606,8 +612,8 @@ function SearchAutocompleteList({
cardAutocompleteList,
booleanTypes,
workspaceList,
- isAutocompleteList,
hasAutocompleteList,
+ isAutocompleteList,
]);
const sortedRecentSearches = useMemo(() => {
@@ -805,7 +811,14 @@ function SearchAutocompleteList({
const keyIndex = autocompleteQueryValue.toLowerCase().lastIndexOf(fieldPattern.toLowerCase());
if (keyIndex !== -1) {
- trimmedUserSearchQuery = autocompleteQueryValue.substring(0, keyIndex + fieldPattern.length);
+ const afterFieldKey = autocompleteQueryValue.substring(keyIndex + fieldPattern.length);
+ const lastCommaIndex = afterFieldKey.lastIndexOf(',');
+
+ if (lastCommaIndex !== -1) {
+ trimmedUserSearchQuery = autocompleteQueryValue.substring(0, keyIndex + fieldPattern.length + lastCommaIndex + 1);
+ } else {
+ trimmedUserSearchQuery = autocompleteQueryValue.substring(0, keyIndex + fieldPattern.length);
+ }
} else {
trimmedUserSearchQuery = getQueryWithoutAutocompletedPart(autocompleteQueryValue);
}
diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx
index ed69de417fa32..da088e59ba007 100644
--- a/src/components/Search/SearchFiltersChatsSelector.tsx
+++ b/src/components/Search/SearchFiltersChatsSelector.tsx
@@ -56,6 +56,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen
const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]);
const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true});
const archivedReportsIdSet = useArchivedReportsIdSet();
+ const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
const selectedOptions = useMemo(() => {
return selectedReportIDs.map((id) => {
@@ -64,14 +65,14 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen
const alternateText = getAlternateText(report, {}, isReportArchived, {});
return {...report, alternateText};
});
- }, [personalDetails, reportAttributesDerived, reports, selectedReportIDs, archivedReportsIdSet]);
+ }, [archivedReportsIdSet, personalDetails, reportAttributesDerived, reports, selectedReportIDs]);
const defaultOptions = useMemo(() => {
if (!areOptionsInitialized || !isScreenTransitionEnd) {
return defaultListOptions;
}
- return getSearchOptions({options, draftComments, betas: undefined, isUsedInChatFinder: false, countryCode});
- }, [areOptionsInitialized, draftComments, isScreenTransitionEnd, options, countryCode]);
+ return getSearchOptions({options, draftComments, nvpDismissedProductTraining, betas: undefined, isUsedInChatFinder: false, countryCode});
+ }, [areOptionsInitialized, draftComments, nvpDismissedProductTraining, isScreenTransitionEnd, options, countryCode]);
const chatOptions = useMemo(() => {
return filterAndOrderOptions(defaultOptions, cleanSearchTerm, countryCode, {
diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx
index bd9dfe71107c4..5d3242545ad01 100644
--- a/src/components/Search/SearchFiltersParticipantsSelector.tsx
+++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx
@@ -53,7 +53,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
const [searchTerm, setSearchTerm] = useState('');
const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]);
const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true});
-
+ const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
const defaultOptions = useMemo(() => {
if (!areOptionsInitialized) {
return defaultListOptions;
@@ -65,13 +65,14 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
personalDetails: options.personalDetails,
},
draftComments,
+ nvpDismissedProductTraining,
{
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
includeCurrentUser: true,
},
countryCode,
);
- }, [areOptionsInitialized, draftComments, options.personalDetails, options.reports, countryCode]);
+ }, [areOptionsInitialized, draftComments, options.personalDetails, options.reports, nvpDismissedProductTraining, countryCode]);
const unselectedOptions = useMemo(() => {
return filterSelectedOptions(defaultOptions, new Set(selectedOptions.map((option) => option.accountID)));
diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx
index a97d96b2b8ea1..e106c04bcb0d7 100644
--- a/src/components/Search/SearchList/index.tsx
+++ b/src/components/Search/SearchList/index.tsx
@@ -183,15 +183,37 @@ function SearchList({
}
return data;
}, [data, groupBy, type]);
- const flattenedItemsWithoutPendingDelete = useMemo(() => flattenedItems.filter((t) => t?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), [flattenedItems]);
-
- const selectedItemsLength = useMemo(
- () =>
- flattenedItems.reduce((acc, item) => {
- return item?.isSelected ? acc + 1 : acc;
- }, 0),
- [flattenedItems],
- );
+
+ const emptyReports = useMemo(() => {
+ if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
+ return data.filter((item) => item.transactions.length === 0);
+ }
+ return [];
+ }, [data, type]);
+
+ const selectedItemsLength = useMemo(() => {
+ const selectedTransactions = flattenedItems.reduce((acc, item) => {
+ return acc + (item?.isSelected ? 1 : 0);
+ }, 0);
+
+ if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
+ const selectedEmptyReports = emptyReports.reduce((acc, item) => {
+ return acc + (item.isSelected ? 1 : 0);
+ }, 0);
+
+ return selectedEmptyReports + selectedTransactions;
+ }
+
+ return selectedTransactions;
+ }, [flattenedItems, type, data, emptyReports]);
+
+ const totalItems = useMemo(() => {
+ if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
+ return emptyReports.length + flattenedItems.length;
+ }
+
+ return flattenedItems.length;
+ }, [data, type, flattenedItems, emptyReports]);
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -237,10 +259,6 @@ function SearchList({
if (shouldPreventLongPressRow || !isSmallScreenWidth || item?.isDisabled || item?.isDisabledCheckbox) {
return;
}
- // disable long press for empty expense reports
- if ('transactions' in item && item.transactions.length === 0 && !groupBy) {
- return;
- }
if (isMobileSelectionModeEnabled) {
onCheckboxPress(item, itemTransactions);
return;
@@ -249,7 +267,7 @@ function SearchList({
setLongPressedItemTransactions(itemTransactions);
setIsModalVisible(true);
},
- [groupBy, route.key, shouldPreventLongPressRow, isSmallScreenWidth, isMobileSelectionModeEnabled, onCheckboxPress],
+ [route.key, shouldPreventLongPressRow, isSmallScreenWidth, isMobileSelectionModeEnabled, onCheckboxPress],
);
const turnOnSelectionMode = useCallback(() => {
@@ -373,7 +391,7 @@ function SearchList({
const tableHeaderVisible = (canSelectMultiple || !!SearchTableHeader) && (!groupBy || type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT);
const selectAllButtonVisible = canSelectMultiple && !SearchTableHeader;
- const isSelectAllChecked = selectedItemsLength > 0 && selectedItemsLength === flattenedItemsWithoutPendingDelete.length;
+ const isSelectAllChecked = selectedItemsLength > 0 && selectedItemsLength === totalItems;
return (
@@ -383,7 +401,7 @@ function SearchList({
0 && selectedItemsLength !== flattenedItemsWithoutPendingDelete.length}
+ isIndeterminate={selectedItemsLength > 0 && selectedItemsLength !== totalItems}
onPress={() => {
onAllCheckboxPress();
}}
diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
index 4625271a17c3a..eee9c885e1055 100644
--- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
+++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
@@ -1,4 +1,5 @@
import {useIsFocused} from '@react-navigation/native';
+import {isUserValidatedSelector} from '@selectors/Account';
import {emailSelector} from '@selectors/Session';
import {searchResultsErrorSelector} from '@selectors/Snapshot';
import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react';
@@ -41,6 +42,7 @@ import {handleBulkPayItemSelected, updateAdvancedFilters} from '@libs/actions/Se
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
+import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import {getActiveAdminWorkspaces, getAllTaxRates, isPaidGroupPolicy} from '@libs/PolicyUtils';
import {isExpenseReport} from '@libs/ReportUtils';
import {buildFilterFormValuesFromQuery, buildQueryStringFromFilterFormValues, isFilterSupported, isSearchDatePreset} from '@libs/SearchQueryUtils';
@@ -85,7 +87,7 @@ function SearchFiltersBar({
const isFocused = useIsFocused();
const scrollRef = useRef(null);
const currentPolicy = usePolicy(currentSelectedPolicyID);
- const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.validated, canBeMissing: true});
+ const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: true});
// type, groupBy and status values are not guaranteed to respect the ts type as they come from user input
const {hash, type: unsafeType, groupBy: unsafeGroupBy, status: unsafeStatus, flatFilters} = queryJSON;
const [selectedIOUReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentSelectedReportID}`, {canBeMissing: true});
@@ -458,7 +460,7 @@ function SearchFiltersBar({
* filter bar
*/
const filters = useMemo(() => {
- const fromValue = filterFormValues.from?.map((currentAccountID) => personalDetails?.[currentAccountID]?.displayName ?? currentAccountID) ?? [];
+ const fromValue = filterFormValues.from?.map((currentAccountID) => getDisplayNameOrDefault(personalDetails?.[currentAccountID], currentAccountID, false)) ?? [];
const shouldDisplayGroupByFilter = !!groupBy?.value;
const shouldDisplayGroupCurrencyFilter = shouldDisplayGroupByFilter && hasMultipleOutputCurrency;
@@ -616,7 +618,7 @@ function SearchFiltersBar({
const hiddenSelectedFilters = useMemo(() => {
const advancedSearchFiltersKeys = typeFiltersKeys.flat();
-
+ // eslint-disable-next-line unicorn/prefer-set-has
const exposedFiltersKeys = filters.flatMap((filter) => {
const dateFilterKey = DATE_FILTER_KEYS.find((key) => filter.filterKey.startsWith(key));
if (dateFilterKey) {
diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
index 480b0d8d5639f..68ee96a6168af 100644
--- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
+++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
@@ -196,9 +196,9 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
);
const submitSearch = useCallback(
- (queryString: SearchQueryString) => {
+ (queryString: SearchQueryString, shouldSkipAmountConversion = false) => {
const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions);
- const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions);
+ const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, shouldSkipAmountConversion);
if (!updatedQuery) {
return;
@@ -268,7 +268,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
timestamp: endTime,
});
} else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH) {
- submitSearch(item.searchQuery);
+ submitSearch(item.searchQuery, item.keyForList !== 'findItem');
const endTime = Date.now();
Log.info('[CMD_K_DEBUG] Page search submitted', false, {
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index 34ffe10ae1129..f4172bd8b6a0b 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -198,7 +198,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
},
];
},
- [contextualReportID, styles.activeComponentBG, textInputValue, translate, isSearchRouterDisplayed],
+ [contextualReportID, styles.activeComponentBG, textInputValue, translate, isSearchRouterDisplayed, reports, personalDetails],
);
const searchQueryItem = textInputValue
@@ -390,7 +390,14 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
const keyIndex = textInputValue.toLowerCase().lastIndexOf(fieldPattern.toLowerCase());
if (keyIndex !== -1) {
- trimmedUserSearchQuery = textInputValue.substring(0, keyIndex + fieldPattern.length);
+ const afterFieldKey = textInputValue.substring(keyIndex + fieldPattern.length);
+ const lastCommaIndex = afterFieldKey.lastIndexOf(',');
+
+ if (lastCommaIndex !== -1) {
+ trimmedUserSearchQuery = textInputValue.substring(0, keyIndex + fieldPattern.length + lastCommaIndex + 1);
+ } else {
+ trimmedUserSearchQuery = textInputValue.substring(0, keyIndex + fieldPattern.length);
+ }
} else {
trimmedUserSearchQuery = getQueryWithoutAutocompletedPart(textInputValue);
}
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 26a8eda50c607..8fbdab23684a0 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -9,7 +9,14 @@ import FullPageErrorView from '@components/BlockingViews/FullPageErrorView';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import ConfirmModal from '@components/ConfirmModal';
import SearchTableHeader, {getExpenseHeaders} from '@components/SelectionListWithSections/SearchTableHeader';
-import type {ReportActionListItemType, SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionListWithSections/types';
+import type {
+ ReportActionListItemType,
+ SearchListItem,
+ SelectionListHandle,
+ TransactionGroupListItemType,
+ TransactionListItemType,
+ TransactionReportGroupListItemType,
+} from '@components/SelectionListWithSections/types';
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import {WideRHPContext} from '@components/WideRHPContextProvider';
import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
@@ -51,6 +58,7 @@ import {
isTransactionGroupListItemType,
isTransactionListItemType,
isTransactionMemberGroupListItemType,
+ isTransactionReportGroupListItemType,
isTransactionWithdrawalIDGroupListItemType,
shouldShowEmptyState,
shouldShowYear as shouldShowYearUtil,
@@ -72,7 +80,6 @@ import type {SearchTransaction} from '@src/types/onyx/SearchResults';
import type {TransactionViolation} from '@src/types/onyx/TransactionViolation';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import arraysEqual from '@src/utils/arraysEqual';
-import openSearchReport from './openSearchReport';
import {useSearchContext} from './SearchContext';
import SearchList from './SearchList';
import {SearchScopeProvider} from './SearchScopeProvider';
@@ -92,7 +99,11 @@ type SearchProps = {
const expenseHeaders = getExpenseHeaders();
-function mapTransactionItemToSelectedEntry(item: TransactionListItemType, outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue): [string, SelectedTransactionInfo] {
+function mapTransactionItemToSelectedEntry(
+ item: TransactionListItemType,
+ outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue,
+ allowNegativeAmount = true,
+): [string, SelectedTransactionInfo] {
return [
item.keyForList,
{
@@ -114,8 +125,8 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType, outsta
action: item.action,
convertedCurrency: item.convertedCurrency,
reportID: item.reportID,
- policyID: item.policyID,
- amount: item.modifiedAmount ?? item.amount,
+ policyID: item.report?.policyID,
+ amount: allowNegativeAmount ? (item.modifiedAmount ?? item.amount) : Math.abs(item.modifiedAmount ?? item.amount),
convertedAmount: item.convertedAmount,
currency: item.currency,
isFromOneTransactionReport: item.isFromOneTransactionReport,
@@ -124,6 +135,27 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType, outsta
];
}
+function mapEmptyReportToSelectedEntry(item: TransactionReportGroupListItemType): [string, SelectedTransactionInfo] {
+ return [
+ item.keyForList ?? '',
+ {
+ isSelected: true,
+ canDelete: true,
+ canHold: false,
+ isHeld: false,
+ canUnhold: false,
+ canChangeReport: false,
+ action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW,
+ reportID: item.reportID,
+ policyID: item.policyID ?? CONST.POLICY.ID_FAKE,
+ amount: 0,
+ convertedAmount: 0,
+ convertedCurrency: '',
+ currency: '',
+ },
+ ];
+}
+
function mapToTransactionItemWithAdditionalInfo(
item: TransactionListItemType,
selectedTransactions: SelectedTransactions,
@@ -160,47 +192,24 @@ function mapToItemWithAdditionalInfo(item: SearchListItem, selectedTransactions:
mapToTransactionItemWithAdditionalInfo(transaction, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight, hash),
),
isSelected:
- item?.transactions?.length > 0 &&
- item.transactions?.filter((t) => !isTransactionPendingDelete(t)).every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected && canSelectMultiple),
+ item?.transactions?.length > 0
+ ? item.transactions?.filter((t) => !isTransactionPendingDelete(t)).every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected && canSelectMultiple)
+ : !!(item.keyForList && selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple),
hash,
};
}
-function prepareTransactionsList(item: TransactionListItemType, selectedTransactions: SelectedTransactions, outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue) {
+function toggleTransactionInList(item: TransactionListItemType, selectedTransactions: SelectedTransactions, outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue) {
if (selectedTransactions[item.keyForList]?.isSelected) {
const {[item.keyForList]: omittedTransaction, ...transactions} = selectedTransactions;
return transactions;
}
+ const [key, selectedInfo] = mapTransactionItemToSelectedEntry(item, outstandingReportsByPolicyID, false);
return {
...selectedTransactions,
- [item.keyForList]: {
- isSelected: true,
- canDelete: item.canDelete,
- canHold: item.canHold,
- isHeld: isOnHold(item),
- canUnhold: item.canUnhold,
- canChangeReport: canEditFieldOfMoneyRequest(
- item.reportAction,
- CONST.EDIT_REQUEST_FIELD.REPORT,
- undefined,
- undefined,
- outstandingReportsByPolicyID,
- item,
- item.report,
- item.policy,
- ),
- action: item.action,
- reportID: item.reportID,
- policyID: item.policyID,
- amount: Math.abs(item.modifiedAmount || item.amount),
- convertedAmount: item.convertedAmount,
- convertedCurrency: item.convertedCurrency,
- currency: item.currency,
- isFromOneTransactionReport: item.isFromOneTransactionReport,
- ownerAccountID: item.report?.ownerAccountID ?? item.accountID,
- },
+ [key]: selectedInfo,
};
}
@@ -370,7 +379,7 @@ function Search({
openSearch();
}, []);
- const {newSearchResultKey, handleSelectionListScroll, newTransactions} = useSearchHighlightAndScroll({
+ const {newSearchResultKeys, handleSelectionListScroll, newTransactions} = useSearchHighlightAndScroll({
searchResults,
transactions,
previousTransactions,
@@ -448,6 +457,22 @@ function Search({
if (!Object.hasOwn(transactionGroup, 'transactions') || !('transactions' in transactionGroup)) {
return;
}
+
+ if (transactionGroup.transactions.length === 0 && isTransactionReportGroupListItemType(transactionGroup)) {
+ const reportKey = transactionGroup.keyForList;
+ if (transactionGroup.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return;
+ }
+ if (reportKey && (reportKey in selectedTransactions || areAllMatchingItemsSelected)) {
+ const [, emptyReportSelection] = mapEmptyReportToSelectedEntry(transactionGroup);
+ newTransactionList[reportKey] = {
+ ...emptyReportSelection,
+ isSelected: areAllMatchingItemsSelected || selectedTransactions[reportKey]?.isSelected,
+ };
+ }
+ return;
+ }
+
transactionGroup.transactions.forEach((transactionItem) => {
if (!(transactionItem.transactionID in selectedTransactions) && !areAllMatchingItemsSelected) {
return;
@@ -472,7 +497,7 @@ function Search({
isSelected: areAllMatchingItemsSelected || selectedTransactions[transactionItem.transactionID].isSelected,
canDelete: transactionItem.canDelete,
reportID: transactionItem.reportID,
- policyID: transactionItem.policyID,
+ policyID: transactionItem.report?.policyID,
amount: transactionItem.modifiedAmount ?? transactionItem.amount,
convertedAmount: transactionItem.convertedAmount,
convertedCurrency: transactionItem.convertedCurrency,
@@ -509,7 +534,7 @@ function Search({
isSelected: areAllMatchingItemsSelected || selectedTransactions[transactionItem.transactionID].isSelected,
canDelete: transactionItem.canDelete,
reportID: transactionItem.reportID,
- policyID: transactionItem.policyID,
+ policyID: transactionItem.report?.policyID,
amount: transactionItem.modifiedAmount ?? transactionItem.amount,
convertedAmount: transactionItem.convertedAmount,
convertedCurrency: transactionItem.convertedCurrency,
@@ -551,25 +576,36 @@ function Search({
isRefreshingSelection.current = false;
}, [selectedTransactions]);
- useEffect(() => {
- if (!isFocused) {
- return;
- }
-
- if (!data.length || isRefreshingSelection.current) {
- return;
- }
- const areItemsGrouped = !!validGroupBy || type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
- const flattenedItems = areItemsGrouped ? (data as TransactionGroupListItemType[]).flatMap((item) => item.transactions) : data;
- const areAllItemsSelected = flattenedItems.length === Object.keys(selectedTransactions).length;
-
- // If the user has selected all the expenses in their view but there are more expenses matched by the search
- // give them the option to select all matching expenses
- shouldShowSelectAllMatchingItems(!!(areAllItemsSelected && searchResults?.search?.hasMoreResults));
- if (!areAllItemsSelected) {
- selectAllMatchingItems(false);
- }
- }, [isFocused, data, searchResults?.search?.hasMoreResults, selectedTransactions, selectAllMatchingItems, shouldShowSelectAllMatchingItems, validGroupBy, type]);
+ const updateSelectAllMatchingItemsState = useCallback(
+ (updatedSelectedTransactions: SelectedTransactions) => {
+ if (!data.length || isRefreshingSelection.current) {
+ return;
+ }
+ const areItemsGrouped = !!validGroupBy || type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
+ const totalSelectableItemsCount = areItemsGrouped
+ ? (data as TransactionGroupListItemType[]).reduce((count, item) => {
+ // For empty reports, count the report itself as a selectable item
+ if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item)) {
+ if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return count;
+ }
+ return count + 1;
+ }
+ // For regular reports, count all transactions
+ return count + item.transactions.length;
+ }, 0)
+ : data.length;
+ const areAllItemsSelected = totalSelectableItemsCount === Object.keys(updatedSelectedTransactions).length;
+
+ // If the user has selected all the expenses in their view but there are more expenses matched by the search
+ // give them the option to select all matching expenses
+ shouldShowSelectAllMatchingItems(!!(areAllItemsSelected && searchResults?.search?.hasMoreResults));
+ if (!areAllItemsSelected) {
+ selectAllMatchingItems(false);
+ }
+ },
+ [data, validGroupBy, type, searchResults?.search?.hasMoreResults, shouldShowSelectAllMatchingItems, selectAllMatchingItems],
+ );
const toggleTransaction = useCallback(
(item: SearchListItem, itemTransactions?: TransactionListItemType[]) => {
@@ -586,11 +622,44 @@ function Search({
if (isTransactionPendingDelete(item)) {
return;
}
- setSelectedTransactions(prepareTransactionsList(item, selectedTransactions, outstandingReportsByPolicyID), data);
+ const updatedTransactions = toggleTransactionInList(item, selectedTransactions, outstandingReportsByPolicyID);
+ setSelectedTransactions(updatedTransactions, data);
+ updateSelectAllMatchingItemsState(updatedTransactions);
return;
}
const currentTransactions = itemTransactions ?? item.transactions;
+
+ // Handle empty reports - treat the report itself as selectable
+ if (currentTransactions.length === 0 && isTransactionReportGroupListItemType(item)) {
+ const reportKey = item.keyForList;
+ if (!reportKey) {
+ return;
+ }
+
+ if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return;
+ }
+
+ if (selectedTransactions[reportKey]?.isSelected) {
+ // Deselect the empty report
+ const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions};
+ delete reducedSelectedTransactions[reportKey];
+ setSelectedTransactions(reducedSelectedTransactions, data);
+ updateSelectAllMatchingItemsState(reducedSelectedTransactions);
+ return;
+ }
+
+ const [, emptyReportSelection] = mapEmptyReportToSelectedEntry(item);
+ const updatedTransactions = {
+ ...selectedTransactions,
+ [reportKey]: emptyReportSelection,
+ };
+ setSelectedTransactions(updatedTransactions, data);
+ updateSelectAllMatchingItemsState(updatedTransactions);
+ return;
+ }
+
if (currentTransactions.some((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)) {
const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions};
@@ -599,22 +668,22 @@ function Search({
});
setSelectedTransactions(reducedSelectedTransactions, data);
+ updateSelectAllMatchingItemsState(reducedSelectedTransactions);
return;
}
- setSelectedTransactions(
- {
- ...selectedTransactions,
- ...Object.fromEntries(
- currentTransactions
- .filter((t) => !isTransactionPendingDelete(t))
- .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, outstandingReportsByPolicyID)),
- ),
- },
- data,
- );
+ const updatedTransactions = {
+ ...selectedTransactions,
+ ...Object.fromEntries(
+ currentTransactions
+ .filter((t) => !isTransactionPendingDelete(t))
+ .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, outstandingReportsByPolicyID)),
+ ),
+ };
+ setSelectedTransactions(updatedTransactions, data);
+ updateSelectAllMatchingItemsState(updatedTransactions);
},
- [data, selectedTransactions, outstandingReportsByPolicyID, setSelectedTransactions],
+ [data, selectedTransactions, outstandingReportsByPolicyID, setSelectedTransactions, updateSelectAllMatchingItemsState],
);
const onSelectRow = useCallback(
@@ -711,9 +780,9 @@ function Search({
setOptimisticDataForTransactionThreadPreview(item, transactionPreviewData);
}
- openSearchReport(reportID, backTo);
+ requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo})));
},
- [isMobileSelectionModeEnabled, type, toggleTransaction, hash, queryJSON, handleSearch, searchKey, markReportIDAsExpense],
+ [isMobileSelectionModeEnabled, toggleTransaction, hash, queryJSON, handleSearch, searchKey, markReportIDAsExpense],
);
const currentColumns = useMemo(() => {
@@ -763,14 +832,14 @@ function Search({
: `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`;
// Check if the base key matches the newSearchResultKey (TransactionListItemType)
- const isBaseKeyMatch = baseKey === newSearchResultKey;
+ const isBaseKeyMatch = !!newSearchResultKeys?.has(baseKey);
// Check if any transaction within the transactions array (TransactionGroupListItemType) matches the newSearchResultKey
const isAnyTransactionMatch =
!isChat &&
(item as TransactionGroupListItemType)?.transactions?.some((transaction) => {
const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`;
- return transactionKey === newSearchResultKey;
+ return !!newSearchResultKeys?.has(transactionKey);
});
// Determine if either the base key or any transaction key matches
@@ -778,7 +847,7 @@ function Search({
return mapToItemWithAdditionalInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight, hash);
}),
- [type, status, data, sortBy, sortOrder, validGroupBy, isChat, newSearchResultKey, selectedTransactions, canSelectMultiple, localeCompare, hash],
+ [type, status, data, sortBy, sortOrder, validGroupBy, isChat, newSearchResultKeys, selectedTransactions, canSelectMultiple, localeCompare, hash],
);
useEffect(() => {
@@ -809,33 +878,38 @@ function Search({
if (totalSelected > 0) {
clearSelectedTransactions();
+ updateSelectAllMatchingItemsState({});
return;
}
+ let updatedTransactions: SelectedTransactions;
if (areItemsGrouped) {
- setSelectedTransactions(
- Object.fromEntries(
- (data as TransactionGroupListItemType[]).flatMap((item) =>
- item.transactions
- .filter((t) => !isTransactionPendingDelete(t))
- .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, outstandingReportsByPolicyID)),
- ),
+ const allSelections: Array<[string, SelectedTransactionInfo]> = (data as TransactionGroupListItemType[]).flatMap((item) => {
+ if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item) && item.keyForList) {
+ if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return [];
+ }
+ return [mapEmptyReportToSelectedEntry(item)];
+ }
+
+ return item.transactions
+ .filter((t) => !isTransactionPendingDelete(t))
+ .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, outstandingReportsByPolicyID));
+ });
+ updatedTransactions = Object.fromEntries(allSelections);
+ } else {
+ updatedTransactions = Object.fromEntries(
+ (data as TransactionGroupListItemType[]).flatMap((item) =>
+ item.transactions
+ .filter((t) => !isTransactionPendingDelete(t))
+ .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, outstandingReportsByPolicyID)),
),
- data,
);
-
- return;
}
- setSelectedTransactions(
- Object.fromEntries(
- (data as TransactionListItemType[])
- .filter((t) => !isTransactionPendingDelete(t))
- .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, outstandingReportsByPolicyID)),
- ),
- data,
- );
- }, [clearSelectedTransactions, data, validGroupBy, selectedTransactions, setSelectedTransactions, outstandingReportsByPolicyID, isExpenseReportType]);
+ setSelectedTransactions(updatedTransactions, data);
+ updateSelectAllMatchingItemsState(updatedTransactions);
+ }, [clearSelectedTransactions, data, validGroupBy, selectedTransactions, setSelectedTransactions, outstandingReportsByPolicyID, isExpenseReportType, updateSelectAllMatchingItemsState]);
const onLayout = useCallback(() => handleSelectionListScroll(sortedSelectedData, searchListRef.current), [handleSelectionListScroll, sortedSelectedData]);
@@ -892,6 +966,7 @@ function Search({
}
const onSortPress = (column: SearchColumnType, order: SortOrder) => {
+ clearSelectedTransactions();
const newQuery = buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order});
onSortPressedCallback?.();
navigation.setParams({q: newQuery});
@@ -919,7 +994,7 @@ function Search({
onDEWModalOpen={handleDEWModalOpen}
SearchTableHeader={
!shouldShowTableHeader ? undefined : (
-
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo})));
-}
diff --git a/src/components/Search/openSearchReport/index.ts b/src/components/Search/openSearchReport/index.ts
deleted file mode 100644
index 3b6cf01799775..0000000000000
--- a/src/components/Search/openSearchReport/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import {CommonActions} from '@react-navigation/native';
-import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
-import NAVIGATORS from '@src/NAVIGATORS';
-import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
-
-// The search report screen is preloaded before navigating to avoid lag when opening the page. If the content is rendered in the background and then navigated, the opening experience is smoother.
-export default function openSearchReport(reportID: string | undefined, backTo: string) {
- navigationRef.dispatch({
- ...CommonActions.preload(NAVIGATORS.RIGHT_MODAL_NAVIGATOR, {
- name: SCREENS.RIGHT_MODAL.SEARCH_REPORT,
- params: {reportID, backTo},
- }),
- target: navigationRef.getRootState().key,
- });
- requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo})));
-}
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 95bf13722c3e0..feabcad37fff6 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -33,7 +33,7 @@ type SelectedTransactionInfo = {
reportID: string;
/** The policyID tied to the report the transaction is reported on */
- policyID: string;
+ policyID: string | undefined;
/** The transaction amount */
amount: number;
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index d0c7fdc5ece6c..aea235857ee2a 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -61,7 +61,7 @@ function BaseSelectionList({
isRowMultilineSupported = false,
addBottomSafeAreaPadding,
includeSafeAreaPaddingBottom = true,
- showListEmptyContent,
+ showListEmptyContent = true,
showLoadingPlaceholder,
showScrollIndicator = true,
canSelectMultiple = false,
@@ -76,6 +76,7 @@ function BaseSelectionList({
shouldSingleExecuteRowSelect = false,
shouldPreventDefaultFocusOnSelectRow = false,
shouldShowTextInput = !!textInputOptions?.label,
+ shouldHighlightSelectedItem = true,
}: SelectionListProps) {
const styles = useThemeStyles();
const isFocused = useIsFocused();
@@ -324,6 +325,7 @@ function BaseSelectionList({
wrapperStyle={style?.listItemWrapperStyle}
titleStyles={style?.listItemTitleStyles}
singleExecution={singleExecution}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
shouldSyncFocus={!isTextInputFocusedRef.current && hasKeyBeenPressed.current}
/>
);
@@ -360,6 +362,19 @@ function BaseSelectionList({
[data, itemsToHighlight, scrollToIndex],
);
+ const updateFocusedIndex = useCallback(
+ (newFocusedIndex: number, shouldScroll = false) => {
+ if (newFocusedIndex < 0 || newFocusedIndex >= data.length) {
+ return;
+ }
+ setFocusedIndex(newFocusedIndex);
+ if (shouldScroll) {
+ scrollToIndex(newFocusedIndex);
+ }
+ },
+ [data.length, scrollToIndex, setFocusedIndex],
+ );
+
useEffect(() => {
if (!itemFocusTimeoutRef.current) {
return;
@@ -374,7 +389,7 @@ function BaseSelectionList({
}
}, [onSelectAll, shouldShowTextInput, shouldPreventDefaultFocusOnSelectRow]);
- useImperativeHandle(ref, () => ({scrollAndHighlightItem, scrollToIndex}), [scrollAndHighlightItem, scrollToIndex]);
+ useImperativeHandle(ref, () => ({scrollAndHighlightItem, scrollToIndex, updateFocusedIndex}), [scrollAndHighlightItem, scrollToIndex, updateFocusedIndex]);
return (
{textInputComponent({shouldBeInsideList: false})}
diff --git a/src/components/SelectionList/ListItem/BaseListItem.tsx b/src/components/SelectionList/ListItem/BaseListItem.tsx
index 256d9d701e405..dff7151d4175e 100644
--- a/src/components/SelectionList/ListItem/BaseListItem.tsx
+++ b/src/components/SelectionList/ListItem/BaseListItem.tsx
@@ -41,6 +41,7 @@ function BaseListItem({
onLongPressRow,
testID,
shouldUseDefaultRightHandSideCheckmark = true,
+ shouldHighlightSelectedItem = true,
}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -108,7 +109,9 @@ function BaseListItem({
id={keyForList ?? ''}
style={[
pressableStyle,
- isFocused && StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
+ isFocused &&
+ shouldHighlightSelectedItem &&
+ StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
]}
onFocus={onFocus}
onMouseLeave={handleMouseLeave}
@@ -121,7 +124,9 @@ function BaseListItem({
accessibilityState={{selected: !!isFocused}}
style={[
wrapperStyle,
- isFocused && StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
+ isFocused &&
+ shouldHighlightSelectedItem &&
+ StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
]}
>
{typeof children === 'function' ? children(hovered) : children}
diff --git a/src/components/SelectionList/ListItem/InviteMemberListItem.tsx b/src/components/SelectionList/ListItem/InviteMemberListItem.tsx
index 2ab88b6e1e6a4..47901002b493e 100644
--- a/src/components/SelectionList/ListItem/InviteMemberListItem.tsx
+++ b/src/components/SelectionList/ListItem/InviteMemberListItem.tsx
@@ -9,6 +9,7 @@ import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
@@ -17,6 +18,7 @@ import {getIsUserSubmittedExpenseOrScannedReceipt} from '@libs/OptionsListUtils'
import {isSelectedManagerMcTest} from '@libs/ReportUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import BaseListItem from './BaseListItem';
import type {InviteMemberListItemProps, ListItem} from './types';
@@ -42,11 +44,12 @@ function InviteMemberListItem({
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {isBetaEnabled} = usePermissions();
+ const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip} = useProductTrainingContext(
CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP_MANAGER,
canShowProductTrainingTooltip &&
- !getIsUserSubmittedExpenseOrScannedReceipt() &&
+ !getIsUserSubmittedExpenseOrScannedReceipt(nvpDismissedProductTraining) &&
isBetaEnabled(CONST.BETAS.NEWDOT_MANAGER_MCTEST) &&
isSelectedManagerMcTest(item.login) &&
!item.isSelected,
diff --git a/src/components/SelectionList/ListItem/ListItemRenderer.tsx b/src/components/SelectionList/ListItem/ListItemRenderer.tsx
index 3f9ed30427ff6..45aa83229cf69 100644
--- a/src/components/SelectionList/ListItem/ListItemRenderer.tsx
+++ b/src/components/SelectionList/ListItem/ListItemRenderer.tsx
@@ -16,6 +16,7 @@ type ListItemRendererProps = Omit['singleExecution'];
titleStyles?: StyleProp;
titleContainerStyles?: StyleProp;
+ shouldHighlightSelectedItem: boolean;
};
function ListItemRenderer({
@@ -45,6 +46,7 @@ function ListItemRenderer({
singleExecution,
titleContainerStyles,
shouldUseDefaultRightHandSideCheckmark,
+ shouldHighlightSelectedItem,
}: ListItemRendererProps) {
const handleOnCheckboxPress = () => {
if (isTransactionGroupListItemType(item)) {
@@ -95,6 +97,7 @@ function ListItemRenderer({
titleStyles={titleStyles}
titleContainerStyles={titleContainerStyles}
shouldUseDefaultRightHandSideCheckmark={shouldUseDefaultRightHandSideCheckmark}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
/>
{item.footerContent && item.footerContent}
>
diff --git a/src/components/SelectionList/ListItem/RadioListItem.tsx b/src/components/SelectionList/ListItem/RadioListItem.tsx
index 256c3b0a876fe..91f4433730628 100644
--- a/src/components/SelectionList/ListItem/RadioListItem.tsx
+++ b/src/components/SelectionList/ListItem/RadioListItem.tsx
@@ -23,6 +23,7 @@ function RadioListItem({
shouldSyncFocus,
wrapperStyle,
titleStyles,
+ shouldHighlightSelectedItem = true,
}: RadioListItemProps) {
const styles = useThemeStyles();
const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text;
@@ -45,6 +46,7 @@ function RadioListItem({
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
pendingAction={item.pendingAction}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
>
<>
{!!item.leftElement && item.leftElement}
diff --git a/src/components/SelectionList/ListItem/SingleSelectListItem.tsx b/src/components/SelectionList/ListItem/SingleSelectListItem.tsx
index 06706771ecb85..671a2a4a51ae7 100644
--- a/src/components/SelectionList/ListItem/SingleSelectListItem.tsx
+++ b/src/components/SelectionList/ListItem/SingleSelectListItem.tsx
@@ -23,6 +23,7 @@ function SingleSelectListItem({
shouldSyncFocus,
wrapperStyle,
titleStyles,
+ shouldHighlightSelectedItem = true,
}: SingleSelectListItemProps) {
const styles = useThemeStyles();
@@ -56,6 +57,7 @@ function SingleSelectListItem({
shouldSyncFocus={shouldSyncFocus}
wrapperStyle={[wrapperStyle, styles.optionRowCompact]}
titleStyles={titleStyles}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
/>
);
}
diff --git a/src/components/SelectionList/ListItem/UserListItem.tsx b/src/components/SelectionList/ListItem/UserListItem.tsx
index 2bba44bcfaf35..a07ea858ebb39 100644
--- a/src/components/SelectionList/ListItem/UserListItem.tsx
+++ b/src/components/SelectionList/ListItem/UserListItem.tsx
@@ -1,6 +1,7 @@
import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
@@ -15,9 +16,12 @@ import useThemeStyles from '@hooks/useThemeStyles';
import getButtonState from '@libs/getButtonState';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report} from '@src/types/onyx';
import BaseListItem from './BaseListItem';
import type {ListItem, UserListItemProps} from './types';
+const reportExistsSelector = (report: OnyxEntry) => !!report;
+
function UserListItem({
item,
isFocused,
@@ -56,7 +60,7 @@ function UserListItem({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [isReportInOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, {
canBeMissing: true,
- selector: (report) => !!report,
+ selector: reportExistsSelector,
});
const reportExists = isReportInOnyx && !!item.reportID;
diff --git a/src/components/SelectionList/ListItem/types.ts b/src/components/SelectionList/ListItem/types.ts
index b10e7a0fef156..53fc743f14251 100644
--- a/src/components/SelectionList/ListItem/types.ts
+++ b/src/components/SelectionList/ListItem/types.ts
@@ -233,6 +233,9 @@ type ListItemProps = CommonListItemProps & {
/** Whether to show the default right hand side checkmark */
shouldUseDefaultRightHandSideCheckmark?: boolean;
+
+ /** Whether to highlight the selected item */
+ shouldHighlightSelectedItem?: boolean;
};
type ValidListItem =
@@ -263,6 +266,8 @@ type BaseListItemProps = CommonListItemProps & {
shouldUseDefaultRightHandSideCheckmark?: boolean;
/** Whether to show the right caret icon */
shouldShowRightCaret?: boolean;
+ /** Whether to highlight the selected item */
+ shouldHighlightSelectedItem?: boolean;
};
type RadioListItemProps = ListItemProps;
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index dd9ca1033ed3f..a8d3d5101c101 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -143,6 +143,9 @@ type SelectionListProps = {
/** Whether to show the text input */
shouldShowTextInput?: boolean;
+
+ /** Whether to highlight the selected item */
+ shouldHighlightSelectedItem?: boolean;
};
type TextInputOptions = {
@@ -205,6 +208,9 @@ type SelectionListHandle = {
/** Scrolls to the item at the specified index */
scrollToIndex: (index: number) => void;
+
+ /** Updates the focused index and optionally scrolls to it */
+ updateFocusedIndex: (newFocusedIndex: number, shouldScroll?: boolean) => void;
};
type DataDetailsType = {
diff --git a/src/components/SelectionListWithModal/index.tsx b/src/components/SelectionListWithModal/index.tsx
index d75e314449805..a6f006728b673 100644
--- a/src/components/SelectionListWithModal/index.tsx
+++ b/src/components/SelectionListWithModal/index.tsx
@@ -1,21 +1,21 @@
import {useIsFocused} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
-import React, {forwardRef, useEffect, useRef, useState} from 'react';
+import React, {forwardRef, useMemo, useState} from 'react';
import {CheckSquare} from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
import SelectionList from '@components/SelectionListWithSections';
import type {ListItem, SelectionListHandle, SelectionListProps} from '@components/SelectionListWithSections/types';
+import useHandleSelectionMode from '@hooks/useHandleSelectionMode';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
+import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import CONST from '@src/CONST';
type SelectionListWithModalProps = SelectionListProps & {
turnOnSelectionModeOnLongPress?: boolean;
onTurnOnSelectionMode?: (item: TItem | null) => void;
- isSelected?: (item: TItem) => boolean;
isScreenFocused?: boolean;
};
@@ -42,53 +42,23 @@ function SelectionListWithModal(
const isFocused = useIsFocused();
const isMobileSelectionModeEnabled = useMobileSelectionMode();
- // Check if selection should be on when the modal is opened
- const wasSelectionOnRef = useRef(false);
- // Keep track of the number of selected items to determine if we should turn off selection mode
- const selectionRef = useRef(0);
- useEffect(() => {
- // We can access 0 index safely as we are not displaying multiple sections in table view
- const selectedItems =
+ const sectionData = sections[0]?.data;
+ const selectedItems = useMemo(
+ () =>
selectedItemsProp ??
- sections[0].data.filter((item) => {
+ sectionData?.filter((item) => {
if (isSelected) {
return isSelected(item);
}
return !!item.isSelected;
- });
- selectionRef.current = selectedItems.length;
-
- if (!isSmallScreenWidth) {
- if (selectedItems.length === 0 && isMobileSelectionModeEnabled) {
- turnOffMobileSelectionMode();
- }
- return;
- }
- if (!isFocused) {
- return;
- }
- if (!wasSelectionOnRef.current && selectedItems.length > 0) {
- wasSelectionOnRef.current = true;
- }
- if (selectedItems.length > 0 && !isMobileSelectionModeEnabled) {
- turnOnMobileSelectionMode();
- } else if (selectedItems.length === 0 && isMobileSelectionModeEnabled && !wasSelectionOnRef.current) {
- turnOffMobileSelectionMode();
- }
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [sections, selectedItemsProp, isMobileSelectionModeEnabled, isSmallScreenWidth, isSelected, isFocused]);
-
- useEffect(
- () => () => {
- if (selectionRef.current !== 0) {
- return;
- }
- turnOffMobileSelectionMode();
- },
- [],
+ }) ??
+ [],
+ [isSelected, sectionData, selectedItemsProp],
);
+ useHandleSelectionMode(selectedItems);
+
const handleLongPressRow = (item: TItem) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (!turnOnSelectionModeOnLongPress || !isSmallScreenWidth || item?.isDisabled || item?.isDisabledCheckbox || (!isFocused && !isScreenFocused)) {
diff --git a/src/components/SelectionListWithSections/BaseListItem.tsx b/src/components/SelectionListWithSections/BaseListItem.tsx
index 3c106b93e7fe1..1a9a15e1ce54a 100644
--- a/src/components/SelectionListWithSections/BaseListItem.tsx
+++ b/src/components/SelectionListWithSections/BaseListItem.tsx
@@ -43,6 +43,7 @@ function BaseListItem({
shouldUseDefaultRightHandSideCheckmark = true,
forwardedFSClass,
shouldShowRightCaret = false,
+ shouldHighlightSelectedItem = true,
}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -110,7 +111,9 @@ function BaseListItem({
id={keyForList ?? ''}
style={[
pressableStyle,
- isFocused && StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
+ isFocused &&
+ shouldHighlightSelectedItem &&
+ StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
]}
onFocus={onFocus}
onMouseLeave={handleMouseLeave}
@@ -123,7 +126,9 @@ function BaseListItem({
accessibilityState={{selected: !!isFocused}}
style={[
wrapperStyle,
- isFocused && StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
+ isFocused &&
+ shouldHighlightSelectedItem &&
+ StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
]}
fsClass={forwardedFSClass}
>
diff --git a/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx b/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx
index e61d61450a821..dd30c5fed2d93 100644
--- a/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx
+++ b/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx
@@ -58,6 +58,7 @@ function BaseSelectionListItemRenderer({
personalDetails,
userBillingFundID,
shouldShowRightCaret,
+ shouldHighlightSelectedItem = true,
}: BaseSelectionListItemRendererProps) {
const handleOnCheckboxPress = () => {
if (isTransactionGroupListItemType(item)) {
@@ -115,6 +116,7 @@ function BaseSelectionListItemRenderer({
userBillingFundID={userBillingFundID}
index={index}
shouldShowRightCaret={shouldShowRightCaret}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
sectionIndex={sectionIndex}
/>
{item.footerContent && item.footerContent}
diff --git a/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
index b68ac6c9f0e61..88d22cb0f4504 100644
--- a/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
+++ b/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
@@ -142,6 +142,7 @@ function BaseSelectionListWithSections({
canShowProductTrainingTooltip,
renderScrollComponent,
shouldShowRightCaret,
+ shouldHighlightSelectedItem = true,
ref,
}: SelectionListProps) {
const styles = useThemeStyles();
@@ -649,6 +650,7 @@ function BaseSelectionListWithSections({
isSelected: selected,
...item,
}}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
shouldUseDefaultRightHandSideCheckmark={shouldUseDefaultRightHandSideCheckmark}
index={index}
sectionIndex={section?.indexOffset}
diff --git a/src/components/SelectionListWithSections/InviteMemberListItem.tsx b/src/components/SelectionListWithSections/InviteMemberListItem.tsx
index 2ab88b6e1e6a4..47901002b493e 100644
--- a/src/components/SelectionListWithSections/InviteMemberListItem.tsx
+++ b/src/components/SelectionListWithSections/InviteMemberListItem.tsx
@@ -9,6 +9,7 @@ import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
@@ -17,6 +18,7 @@ import {getIsUserSubmittedExpenseOrScannedReceipt} from '@libs/OptionsListUtils'
import {isSelectedManagerMcTest} from '@libs/ReportUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import BaseListItem from './BaseListItem';
import type {InviteMemberListItemProps, ListItem} from './types';
@@ -42,11 +44,12 @@ function InviteMemberListItem({
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {isBetaEnabled} = usePermissions();
+ const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip} = useProductTrainingContext(
CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP_MANAGER,
canShowProductTrainingTooltip &&
- !getIsUserSubmittedExpenseOrScannedReceipt() &&
+ !getIsUserSubmittedExpenseOrScannedReceipt(nvpDismissedProductTraining) &&
isBetaEnabled(CONST.BETAS.NEWDOT_MANAGER_MCTEST) &&
isSelectedManagerMcTest(item.login) &&
!item.isSelected,
diff --git a/src/components/SelectionListWithSections/RadioListItem.tsx b/src/components/SelectionListWithSections/RadioListItem.tsx
index 256c3b0a876fe..91f4433730628 100644
--- a/src/components/SelectionListWithSections/RadioListItem.tsx
+++ b/src/components/SelectionListWithSections/RadioListItem.tsx
@@ -23,6 +23,7 @@ function RadioListItem({
shouldSyncFocus,
wrapperStyle,
titleStyles,
+ shouldHighlightSelectedItem = true,
}: RadioListItemProps) {
const styles = useThemeStyles();
const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text;
@@ -45,6 +46,7 @@ function RadioListItem({
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
pendingAction={item.pendingAction}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
>
<>
{!!item.leftElement && item.leftElement}
diff --git a/src/components/SelectionListWithSections/Search/ActionCell.tsx b/src/components/SelectionListWithSections/Search/ActionCell.tsx
index e53a40dd7d888..7d54d6bd291ab 100644
--- a/src/components/SelectionListWithSections/Search/ActionCell.tsx
+++ b/src/components/SelectionListWithSections/Search/ActionCell.tsx
@@ -5,6 +5,7 @@ import Badge from '@components/Badge';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import type {PaymentMethod} from '@components/KYCWall/types';
+import {SearchScopeProvider} from '@components/Search/SearchScopeProvider';
import type {PaymentData} from '@components/Search/types';
import SettlementButton from '@components/SettlementButton';
import useLocalize from '@hooks/useLocalize';
@@ -150,24 +151,26 @@ function ActionCell({
if (action === CONST.SEARCH.ACTION_TYPES.PAY) {
return (
- confirmPayment(type as ValueOf, payAsBusiness, methodID, paymentMethod)}
- style={[styles.w100]}
- wrapperStyle={[styles.w100]}
- shouldShowPersonalBankAccountOption={!policyID && !iouReport?.policyID}
- isDisabled={isOffline}
- isLoading={isLoading}
- onlyShowPayElsewhere={shouldOnlyShowElsewhere}
- />
+
+ confirmPayment(type as ValueOf, payAsBusiness, methodID, paymentMethod)}
+ style={[styles.w100]}
+ wrapperStyle={[styles.w100]}
+ shouldShowPersonalBankAccountOption={!policyID && !iouReport?.policyID}
+ isDisabled={isOffline}
+ isLoading={isLoading}
+ onlyShowPayElsewhere={shouldOnlyShowElsewhere}
+ />
+
);
}
diff --git a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx
index 12a04595e54da..b59158413b9b2 100644
--- a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx
+++ b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx
@@ -16,7 +16,8 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {handleActionButtonPress} from '@userActions/Search';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults';
+import type {Policy} from '@src/types/onyx';
+import type {SearchReport} from '@src/types/onyx/SearchResults';
import ActionCell from './ActionCell';
import TotalCell from './TotalCell';
import UserInfoAndActionButtonRow from './UserInfoAndActionButtonRow';
@@ -216,10 +217,11 @@ function ReportListItemHeader({
const showUserInfo = (reportItem.type === CONST.REPORT.TYPE.IOU && thereIsFromAndTo) || (reportItem.type === CONST.REPORT.TYPE.EXPENSE && !!reportItem?.from);
const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true});
const snapshotReport = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as SearchReport;
}, [snapshot, reportItem.reportID]);
const snapshotPolicy = useMemo(() => {
- return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as SearchPolicy;
+ return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
}, [snapshot, reportItem.policyID]);
const avatarBorderColor =
StyleUtils.getItemBackgroundColorStyle(!!reportItem.isSelected, !!isFocused || !!isHovered, !!isDisabled, theme.activeComponentBG, theme.hoverComponentBG)?.backgroundColor ??
diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx
index 86e175db0f145..d78ab2c807622 100644
--- a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx
+++ b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx
@@ -208,6 +208,7 @@ function TransactionGroupListExpanded({
isInSingleTransactionReport={isInSingleTransactionReport}
areAllOptionalColumnsHidden={areAllOptionalColumnsHidden}
shouldShowBottomBorder={shouldShowBottomBorder}
+ onArrowRightPress={() => openReportInRHP(transaction)}
/>
);
diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
index a74a1f396fc2e..94e84997520c4 100644
--- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
+++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
@@ -125,10 +125,13 @@ function TransactionGroupListItem({
return transactions.filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
}, [transactions]);
- const isSelectAllChecked = selectedItemsLength === transactions.length && transactions.length > 0;
+ const isEmpty = transactions.length === 0;
+
+ const isEmptyReportSelected = isEmpty && item?.keyForList && selectedTransactions[item.keyForList]?.isSelected;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const isSelectAllChecked = isEmptyReportSelected || (selectedItemsLength === transactionsWithoutPendingDelete.length && transactionsWithoutPendingDelete.length > 0);
const isIndeterminate = selectedItemsLength > 0 && selectedItemsLength !== transactionsWithoutPendingDelete.length;
- const isEmpty = transactions.length === 0;
// Currently only the transaction report groups have transactions where the empty view makes sense
const shouldDisplayEmptyView = isEmpty && isExpenseReportType;
const isDisabledOrEmpty = isEmpty || isDisabled;
@@ -187,11 +190,8 @@ function TransactionGroupListItem({
}, [isExpenseReportType, transactions.length, onSelectRow, transactionPreviewData, item, handleToggle]);
const onLongPress = useCallback(() => {
- if (isEmpty) {
- return;
- }
onLongPressRow?.(item, isExpenseReportType ? undefined : transactions);
- }, [isEmpty, isExpenseReportType, item, onLongPressRow, transactions]);
+ }, [isExpenseReportType, item, onLongPressRow, transactions]);
const onCheckboxPress = useCallback(
(val: TItem) => {
@@ -257,7 +257,7 @@ function TransactionGroupListItem({
report={groupItem as TransactionReportGroupListItemType}
onSelectRow={(listItem) => onSelectRow(listItem, transactionPreviewData)}
onCheckboxPress={onCheckboxPress}
- isDisabled={isDisabledOrEmpty}
+ isDisabled={isDisabled}
isFocused={isFocused}
canSelectMultiple={canSelectMultiple}
isSelectAllChecked={isSelectAllChecked}
@@ -278,19 +278,20 @@ function TransactionGroupListItem({
},
[
groupItem,
- onSelectRow,
- transactionPreviewData,
onCheckboxPress,
isDisabledOrEmpty,
- isFocused,
canSelectMultiple,
isSelectAllChecked,
isIndeterminate,
- onDEWModalOpen,
- groupBy,
- isExpanded,
onExpandIconPress,
+ isExpanded,
+ isFocused,
searchType,
+ groupBy,
+ isDisabled,
+ onDEWModalOpen,
+ onSelectRow,
+ transactionPreviewData,
],
);
diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx
index 60d53da0d0204..701c7295fa548 100644
--- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx
+++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx
@@ -25,7 +25,7 @@ import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, ReportAction, ReportActions} from '@src/types/onyx';
-import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults';
+import type {SearchReport} from '@src/types/onyx/SearchResults';
import type {TransactionViolation} from '@src/types/onyx/TransactionViolation';
import UserInfoAndActionButtonRow from './UserInfoAndActionButtonRow';
@@ -54,11 +54,12 @@ function TransactionListItem({
const {currentSearchHash, currentSearchKey} = useSearchContext();
const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true});
const snapshotReport = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as SearchReport;
}, [snapshot, transactionItem.reportID]);
const snapshotPolicy = useMemo(() => {
- return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as SearchPolicy;
+ return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy;
}, [snapshot, transactionItem.policyID]);
const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true});
@@ -104,7 +105,7 @@ function TransactionListItem({
return (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter(
(violation: TransactionViolation) =>
!isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '') &&
- shouldShowViolation(snapshotReport, snapshotPolicy as Policy, violation.name, currentUserDetails.email ?? '', false),
+ shouldShowViolation(snapshotReport, snapshotPolicy, violation.name, currentUserDetails.email ?? '', false),
);
}, [snapshotPolicy, snapshotReport, transactionItem, violations, currentUserDetails.email]);
@@ -197,6 +198,7 @@ function TransactionListItem({
style={[styles.p3, styles.pv2, shouldUseNarrowLayout ? styles.pt2 : {}, isLargeScreenWidth && styles.pr0]}
areAllOptionalColumnsHidden={areAllOptionalColumnsHidden}
violations={transactionViolations}
+ onArrowRightPress={onPress}
/>
diff --git a/src/components/SelectionListWithSections/SingleSelectListItem.tsx b/src/components/SelectionListWithSections/SingleSelectListItem.tsx
index ef199924a14e4..56d023e28aa58 100644
--- a/src/components/SelectionListWithSections/SingleSelectListItem.tsx
+++ b/src/components/SelectionListWithSections/SingleSelectListItem.tsx
@@ -23,6 +23,7 @@ function SingleSelectListItem({
shouldSyncFocus,
wrapperStyle,
titleStyles,
+ shouldHighlightSelectedItem = true,
}: SingleSelectListItemProps) {
const styles = useThemeStyles();
const isSelected = item.isSelected;
@@ -56,6 +57,7 @@ function SingleSelectListItem({
shouldSyncFocus={shouldSyncFocus}
wrapperStyle={[wrapperStyle, styles.optionRowCompact]}
titleStyles={titleStyles}
+ shouldHighlightSelectedItem={shouldHighlightSelectedItem}
/>
);
}
diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts
index b45ee62a24caf..1e3db7880ca56 100644
--- a/src/components/SelectionListWithSections/types.ts
+++ b/src/components/SelectionListWithSections/types.ts
@@ -33,11 +33,11 @@ import type {
SearchDataTypes,
SearchMemberGroup,
SearchPersonalDetails,
- SearchPolicy,
SearchReport,
SearchReportAction,
SearchTask,
SearchTransaction,
+ SearchTransactionAction,
SearchWithdrawalIDGroup,
} from '@src/types/onyx/SearchResults';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
@@ -112,6 +112,9 @@ type CommonListItemProps = {
/** Whether to show the right caret */
shouldShowRightCaret?: boolean;
+
+ /** Whether to highlight the selected item */
+ shouldHighlightSelectedItem?: boolean;
} & TRightHandSideComponent;
type ListItemFocusEventHandler = (event: NativeSyntheticEvent) => void;
@@ -245,7 +248,7 @@ type TransactionListItemType = ListItem &
report: Report | undefined;
/** Policy to which the transaction belongs */
- policy: SearchPolicy | undefined;
+ policy: Policy | undefined;
/** Report IOU action to which the transaction belongs */
reportAction: ReportAction | undefined;
@@ -303,6 +306,9 @@ type TransactionListItemType = ListItem &
/** Parent report action id */
moneyRequestReportActionID?: string;
+
+ /** The available actions that can be performed for the transaction */
+ allActions: SearchTransactionAction[];
};
type ReportActionListItemType = ListItem &
@@ -364,12 +370,19 @@ type TransactionGroupListItemType = ListItem & {
transactionsQueryJSON?: SearchQueryJSON;
};
+// eslint-disable-next-line @typescript-eslint/no-deprecated
type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT} & SearchReport & {
/** The personal details of the user requesting money */
from: SearchPersonalDetails;
/** The personal details of the user paying the request */
to: SearchPersonalDetails;
+
+ /** The main action that can be performed for the report */
+ action: SearchTransactionAction | undefined;
+
+ /** The available actions that can be performed for the report */
+ allActions?: SearchTransactionAction[];
};
type TransactionMemberGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.FROM} & SearchPersonalDetails & SearchMemberGroup;
@@ -949,6 +962,9 @@ type SelectionListProps = Partial & {
/** Whether to show the right caret icon */
shouldShowRightCaret?: boolean;
+
+ /** Whether to highlight the selected item */
+ shouldHighlightSelectedItem?: boolean;
} & TRightHandSideComponent;
type SelectionListHandle = {
diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx
index a8f1ff1e0010c..7ee3b21467936 100644
--- a/src/components/SettlementButton/index.tsx
+++ b/src/components/SettlementButton/index.tsx
@@ -34,6 +34,7 @@ import {
isInvoiceReport as isInvoiceReportUtil,
isIOUReport,
} from '@libs/ReportUtils';
+import {getSettlementButtonPaymentMethods, handleUnvalidatedUserNavigation} from '@libs/SettlementButtonUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts';
import {approveMoneyRequest} from '@userActions/IOU';
@@ -132,7 +133,7 @@ function SettlementButton({
// whether the user has single policy and the expense is p2p
const hasSinglePolicy = !isExpenseReport && activeAdminPolicies.length === 1;
const hasMultiplePolicies = !isExpenseReport && activeAdminPolicies.length > 1;
- const formattedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, fundList ?? {}, styles);
+ const formattedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, fundList ?? {}, styles, translate);
const hasIntentToPay = ((formattedPaymentMethods.length === 1 && isIOUReport(iouReport)) || !!policy?.achAccount) && !lastPaymentMethod;
const {isBetaEnabled} = usePermissions();
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
@@ -179,7 +180,7 @@ function SettlementButton({
}
if (!isUserValidated) {
- Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute()));
+ handleUnvalidatedUserNavigation(chatReportID, reportID);
return true;
}
@@ -189,7 +190,7 @@ function SettlementButton({
}
return false;
- }, [policy, isAccountLocked, isUserValidated]);
+ }, [policy, isAccountLocked, isUserValidated, chatReportID, reportID, showLockedAccountModal]);
const getPaymentSubitems = useCallback(
(payAsBusiness: boolean) => {
@@ -225,24 +226,7 @@ function SettlementButton({
const paymentButtonOptions = useMemo(() => {
const buttonOptions = [];
- const paymentMethods = {
- [CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]: {
- text: hasActivatedWallet ? translate('iou.settleWallet', {formattedAmount: ''}) : translate('iou.settlePersonal', {formattedAmount: ''}),
- icon: Expensicons.User,
- value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT,
- },
- [CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]: {
- text: translate('iou.settleBusiness', {formattedAmount: ''}),
- icon: Expensicons.Building,
- value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT,
- },
- [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: {
- text: translate('iou.payElsewhere', {formattedAmount: ''}),
- icon: Expensicons.CheckCircle,
- value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
- shouldUpdateSelectedIndex: false,
- },
- };
+ const paymentMethods = getSettlementButtonPaymentMethods(hasActivatedWallet, translate);
const shortFormPayElsewhereButton = {
text: translate('iou.pay'),
diff --git a/src/components/TagPicker.tsx b/src/components/TagPicker.tsx
index 57653f5cc8c78..9de3e33731b3c 100644
--- a/src/components/TagPicker.tsx
+++ b/src/components/TagPicker.tsx
@@ -99,6 +99,7 @@ function TagPicker({
});
}
+ // eslint-disable-next-line unicorn/prefer-set-has
const selectedNames = selectedOptions.map((s) => s.name);
return [...selectedOptions, ...Object.values(policyTagList.tags).filter((policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))];
@@ -116,6 +117,7 @@ function TagPicker({
tags: enabledTags,
recentlyUsedTags: policyRecentlyUsedTagsList,
localeCompare,
+ translate,
});
return shouldOrderListByTagName
? tagSections.map((option) => ({
@@ -123,7 +125,7 @@ function TagPicker({
data: option.data.sort((a, b) => localeCompare(a.text ?? '', b.text ?? '')),
}))
: tagSections;
- }, [searchValue, selectedOptions, enabledTags, policyRecentlyUsedTagsList, shouldOrderListByTagName, localeCompare]);
+ }, [searchValue, selectedOptions, enabledTags, policyRecentlyUsedTagsList, localeCompare, translate, shouldOrderListByTagName]);
const headerMessage = getHeaderMessageForNonUserList((sections?.at(0)?.data?.length ?? 0) > 0, searchValue);
diff --git a/src/components/TestDrive/TestDriveDemo.tsx b/src/components/TestDrive/TestDriveDemo.tsx
index 81daaf5881a1c..e313044dc6212 100644
--- a/src/components/TestDrive/TestDriveDemo.tsx
+++ b/src/components/TestDrive/TestDriveDemo.tsx
@@ -1,3 +1,4 @@
+import {hasSeenTourSelector} from '@selectors/Onboarding';
import React, {useCallback, useEffect, useState} from 'react';
import {InteractionManager} from 'react-native';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
@@ -5,6 +6,7 @@ import EmbeddedDemo from '@components/EmbeddedDemo';
import Modal from '@components/Modal';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin';
import useOnboardingMessages from '@hooks/useOnboardingMessages';
import useOnboardingTaskInformation from '@hooks/useOnboardingTaskInformation';
import useOnyx from '@hooks/useOnyx';
@@ -12,7 +14,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {completeTestDriveTask} from '@libs/actions/Task';
import Navigation from '@libs/Navigation/Navigation';
-import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils';
import {isAdminRoom} from '@libs/ReportUtils';
import {getTestDriveURL} from '@libs/TourUtils';
import CONST from '@src/CONST';
@@ -34,21 +35,26 @@ function TestDriveDemo() {
} = useOnboardingTaskInformation(CONST.ONBOARDING_TASK_TYPE.VIEW_TOUR);
const {testDrive} = useOnboardingMessages();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const [isCurrentUserPolicyAdmin = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
+ const isCurrentUserPolicyAdmin = useIsPaidPolicyAdmin();
+
+ const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
+ selector: hasSeenTourSelector,
canBeMissing: true,
- selector: (policies) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserPersonalDetails.login)),
});
+ useEffect(() => {
+ if (hasSeenTour || !viewTourTaskReport || viewTourTaskReport.stateNum === CONST.REPORT.STATE_NUM.APPROVED) {
+ return;
+ }
+
+ completeTestDriveTask(viewTourTaskReport, viewTourTaskParentReport, isViewTourTaskParentReportArchived, currentUserPersonalDetails.accountID);
+ }, [hasSeenTour, viewTourTaskReport, viewTourTaskParentReport, isViewTourTaskParentReportArchived, currentUserPersonalDetails.accountID]);
+
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.runAfterInteractions(() => {
setIsVisible(true);
- completeTestDriveTask(viewTourTaskReport, viewTourTaskParentReport, isViewTourTaskParentReportArchived, currentUserPersonalDetails.accountID);
});
-
- // This should fire only during mount.
- // eslint-disable-next-line react-compiler/react-compiler
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const closeModal = useCallback(() => {
diff --git a/src/components/TextPicker/index.tsx b/src/components/TextPicker/index.tsx
index 9d6f748c4b93f..0f253421e89d8 100644
--- a/src/components/TextPicker/index.tsx
+++ b/src/components/TextPicker/index.tsx
@@ -8,7 +8,20 @@ import TextSelectorModal from './TextSelectorModal';
import type {TextPickerProps} from './types';
function TextPicker(
- {value, description, placeholder = '', errorText = '', onInputChange, furtherDetails, rightLabel, disabled = false, interactive = true, required = false, ...rest}: TextPickerProps,
+ {
+ value,
+ description,
+ placeholder = '',
+ errorText = '',
+ onInputChange,
+ onValueCommitted,
+ furtherDetails,
+ rightLabel,
+ disabled = false,
+ interactive = true,
+ required = false,
+ ...rest
+ }: TextPickerProps,
forwardedRef: ForwardedRef,
) {
const styles = useThemeStyles();
@@ -29,6 +42,7 @@ function TextPicker(
if (updatedValue !== value) {
onInputChange?.(updatedValue);
}
+ onValueCommitted?.(updatedValue);
hidePickerModal();
};
diff --git a/src/components/TextPicker/types.ts b/src/components/TextPicker/types.ts
index 38eff512a63cf..f1bb516b82750 100644
--- a/src/components/TextPicker/types.ts
+++ b/src/components/TextPicker/types.ts
@@ -43,6 +43,12 @@ type TextPickerProps = {
/** Callback to call when the input changes */
onInputChange?: (value: string | undefined) => void;
+ /**
+ * Called after the user commits the value (presses Save in the modal),
+ * once the modal has closed, the parent value is updated.
+ */
+ onValueCommitted?: (value: string) => void;
+
/** Text to display under the main menu item */
furtherDetails?: string;
diff --git a/src/components/TransactionItemRow/DataCells/ReceiptCell.tsx b/src/components/TransactionItemRow/DataCells/ReceiptCell.tsx
index 8603d5e6a867c..94b20c0b8a21b 100644
--- a/src/components/TransactionItemRow/DataCells/ReceiptCell.tsx
+++ b/src/components/TransactionItemRow/DataCells/ReceiptCell.tsx
@@ -11,7 +11,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {getFileName} from '@libs/fileDownload/FileUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
-import {hasReceiptSource} from '@libs/TransactionUtils';
+import {hasReceiptSource, isPerDiemRequest} from '@libs/TransactionUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import variables from '@styles/variables';
import type {Transaction} from '@src/types/onyx';
@@ -22,7 +22,9 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra
const StyleUtils = useStyleUtils();
const backgroundStyles = isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border);
const {hovered, bind} = useHover();
- const isEReceipt = transactionItem.hasEReceipt && !hasReceiptSource(transactionItem);
+ const isMissingReceiptSource = !hasReceiptSource(transactionItem);
+ const isEReceipt = transactionItem.hasEReceipt && isMissingReceiptSource;
+ const isPerDiem = isPerDiemRequest(transactionItem) && isMissingReceiptSource;
let source = transactionItem?.receipt?.source ?? '';
let previewSource = transactionItem?.receipt?.source ?? '';
@@ -62,6 +64,7 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra
loadingIndicatorStyles={styles.receiptCellLoadingContainer}
transactionItem={transactionItem}
shouldUseInitialObjectPosition
+ isPerDiemRequest={isPerDiem}
/>
(undefined);
@@ -92,12 +93,12 @@ function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem}:
setShouldShow(hovered);
}, [hovered, setShouldShow]);
- if (shouldUseNarrowLayout || !debounceShouldShow || !shouldShow || (!source && !isEReceipt && !isDistanceEReceipt)) {
+ if (shouldUseNarrowLayout || !debounceShouldShow || !shouldShow || (!source && !isEReceipt && !isDistanceEReceipt && !isPerDiemEReceipt)) {
return null;
}
- const shouldShowImage = source && !(isEReceipt || isDistanceEReceipt);
- const shouldShowDistanceEReceipt = isDistanceEReceipt && !isEReceipt;
+ const shouldShowImage = source && !(isEReceipt || isDistanceEReceipt || isPerDiemEReceipt);
+ const shouldShowDistanceEReceipt = isDistanceEReceipt && !isEReceipt && !isPerDiemEReceipt;
return ReactDOM.createPortal(
) : (
- {shouldShowDistanceEReceipt ? (
+ {shouldShowDistanceEReceipt && (
- ) : (
+ )}
+ {!shouldShowDistanceEReceipt && isPerDiemEReceipt && (
+
+ )}
+ {!shouldShowDistanceEReceipt && !isPerDiemEReceipt && (
void;
};
function getMerchantName(transactionItem: TransactionWithOptionalSearchFields, translate: (key: TranslationPaths) => string) {
@@ -153,6 +154,7 @@ function TransactionItemRow({
areAllOptionalColumnsHidden = false,
violations,
shouldShowBottomBorder,
+ onArrowRightPress,
}: TransactionItemRowProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -554,9 +556,9 @@ function TransactionItemRow({
/>
)}
- {!!isLargeScreenWidth && (
+ {!!isLargeScreenWidth && !!onArrowRightPress && (
onButtonPress()}
+ onPress={() => onArrowRightPress?.()}
style={[styles.p3Half, styles.pl0half, styles.justifyContentCenter, styles.alignItemsEnd]}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={CONST.ROLE.BUTTON}
diff --git a/src/components/UploadFile.tsx b/src/components/UploadFile.tsx
index 1970204e08557..0d4704c0cd8bd 100644
--- a/src/components/UploadFile.tsx
+++ b/src/components/UploadFile.tsx
@@ -85,6 +85,7 @@ function UploadFile({
}
if (acceptedFileTypes.length > 0) {
+ // eslint-disable-next-line unicorn/prefer-set-has
const filesExtensions = files.map((file) => splitExtensionFromFileName(file?.name ?? '').fileExtension.toLowerCase());
if (acceptedFileTypes.every((element) => !filesExtensions.includes(element as string))) {
@@ -93,6 +94,7 @@ function UploadFile({
}
}
+ // eslint-disable-next-line unicorn/prefer-set-has
const uploadedFilesNames = uploadedFiles.map((uploadedFile) => uploadedFile.name);
const newFilesToUpload = files.filter((file) => !uploadedFilesNames.includes(file.name));
diff --git a/src/components/WideRHPContextProvider/index.tsx b/src/components/WideRHPContextProvider/index.tsx
index cde7c8f11c88e..07a327d957190 100644
--- a/src/components/WideRHPContextProvider/index.tsx
+++ b/src/components/WideRHPContextProvider/index.tsx
@@ -61,22 +61,6 @@ function extractNavigationKeys(state: NavigationState | PartialState isFullScreenName(route.name));
- const lastRHPRouteIndex = state.routes.findLastIndex((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
-
- // Both routes have to be present and the RHP have to be after last full screen for it to be visible.
- if (lastFullScreenRouteIndex === -1 || lastRHPRouteIndex === -1 || lastFullScreenRouteIndex > lastRHPRouteIndex) {
- return undefined;
- }
-
- return state?.routes.at(lastRHPRouteIndex)?.key;
-}
-
/**
* Calculates the optimal width for the receipt pane RHP based on window width.
* Ensures the RHP doesn't exceed maximum width and maintains minimum responsive width.
@@ -115,6 +99,23 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
const [expenseReportIDs, setExpenseReportIDs] = useState>(new Set());
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: expenseReportSelector, canBeMissing: true});
+ // Return undefined if RHP is not the last route
+ const lastVisibleRHPRouteKey = useRootNavigationState((state) => {
+ // Safe handling when navigation is not yet initialized
+ if (!state) {
+ return undefined;
+ }
+ const lastFullScreenRouteIndex = state?.routes.findLastIndex((route) => isFullScreenName(route.name));
+ const lastRHPRouteIndex = state?.routes.findLastIndex((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
+
+ // Both routes have to be present and the RHP have to be after last full screen for it to be visible.
+ if (lastFullScreenRouteIndex === -1 || lastRHPRouteIndex === -1 || lastFullScreenRouteIndex > lastRHPRouteIndex) {
+ return undefined;
+ }
+
+ return state?.routes.at(lastRHPRouteIndex)?.key;
+ });
+
const wideRHPRouteKeys = useMemo(() => {
const rootState = navigationRef.getRootState();
@@ -122,7 +123,6 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
return [];
}
- const lastVisibleRHPRouteKey = getLastVisibleRHPRouteKey(rootState);
const lastRHPRoute = rootState.routes.find((route) => route.key === lastVisibleRHPRouteKey);
if (!lastRHPRoute) {
@@ -133,7 +133,7 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
const currentKeys = allWideRHPRouteKeys.filter((key) => lastRHPKeys.has(key));
return currentKeys;
- }, [allWideRHPRouteKeys]);
+ }, [allWideRHPRouteKeys, lastVisibleRHPRouteKey]);
/**
* Determines whether the secondary overlay should be displayed.
diff --git a/src/components/WorkspacesEmptyStateComponent.tsx b/src/components/WorkspacesEmptyStateComponent.tsx
new file mode 100644
index 0000000000000..a20cb77e22717
--- /dev/null
+++ b/src/components/WorkspacesEmptyStateComponent.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import useLocalize from '@hooks/useLocalize';
+import usePreferredPolicy from '@hooks/usePreferredPolicy';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
+import Navigation from '@libs/Navigation/Navigation';
+import colors from '@styles/theme/colors';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import EmptyStateComponent from './EmptyStateComponent';
+import LottieAnimations from './LottieAnimations';
+import WorkspaceRowSkeleton from './Skeletons/WorkspaceRowSkeleton';
+
+function WorkspacesEmptyStateComponent() {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const StyleUtils = useStyleUtils();
+ const {isRestrictedPolicyCreation} = usePreferredPolicy();
+
+ return (
+ interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(ROUTES.WORKSPACES_LIST.route))),
+ buttonText: translate('workspace.new.newWorkspace'),
+ },
+ ]
+ }
+ />
+ );
+}
+
+WorkspacesEmptyStateComponent.displayName = 'WorkspacesEmptyStateComponent';
+export default WorkspacesEmptyStateComponent;
diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts
index 78ebfb8166195..8a9cfd839c1d2 100644
--- a/src/hooks/useAdvancedSearchFilters.ts
+++ b/src/hooks/useAdvancedSearchFilters.ts
@@ -241,6 +241,7 @@ function useAdvancedSearchFilters() {
});
// When looking if a user has any categories to display, we want to ignore the policies that are of type PERSONAL
+ // eslint-disable-next-line unicorn/prefer-set-has
const nonPersonalPolicyCategoryIds = Object.values(policies)
.filter((policy): policy is NonNullable => !!(policy && policy.type !== CONST.POLICY.TYPE.PERSONAL))
.map((policy) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`);
diff --git a/src/hooks/useArchivedReportsIdSet.ts b/src/hooks/useArchivedReportsIdSet.ts
index e5078cf6bee70..a78d05a047859 100644
--- a/src/hooks/useArchivedReportsIdSet.ts
+++ b/src/hooks/useArchivedReportsIdSet.ts
@@ -1,4 +1,4 @@
-import {isArchivedReport} from '@libs/ReportUtils';
+import {archivedReportsIdSetSelector} from '@selectors/ReportNameValuePairs';
import type {ArchivedReportsIDSet} from '@libs/SearchUIUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import useDeepCompareRef from './useDeepCompareRef';
@@ -10,19 +10,7 @@ import useOnyx from './useOnyx';
function useArchivedReportsIdSet(): ArchivedReportsIDSet {
const [archivedReportsIdSet = new Set()] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {
canBeMissing: true,
- selector: (all): ArchivedReportsIDSet => {
- const ids = new Set();
- if (!all) {
- return ids;
- }
-
- for (const [key, value] of Object.entries(all)) {
- if (isArchivedReport(value)) {
- ids.add(key);
- }
- }
- return ids;
- },
+ selector: archivedReportsIdSetSelector,
});
// useDeepCompareRef is used here to prevent unnecessary re-renders by maintaining referential equality
diff --git a/src/hooks/useBulkPayOptions.ts b/src/hooks/useBulkPayOptions.ts
index 11df01d894dea..881d33f18d899 100644
--- a/src/hooks/useBulkPayOptions.ts
+++ b/src/hooks/useBulkPayOptions.ts
@@ -1,11 +1,12 @@
import truncate from 'lodash/truncate';
import {useMemo} from 'react';
-import {Bank, Building, CheckCircle, User, Wallet} from '@components/Icon/Expensicons';
+import {Bank, Building, Wallet} from '@components/Icon/Expensicons';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type {BankAccountMenuItem} from '@components/Search/types';
import {formatPaymentMethods} from '@libs/PaymentUtils';
import {hasRequestFromCurrentAccount} from '@libs/ReportActionsUtils';
import {isExpenseReport as isExpenseReportUtil, isInvoiceReport as isInvoiceReportUtil, isIOUReport as isIOUReportUtil} from '@libs/ReportUtils';
+import {getSettlementButtonPaymentMethods} from '@libs/SettlementButtonUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {AccountData, Policy} from '@src/types/onyx';
@@ -52,7 +53,7 @@ function useBulkPayOptions({selectedPolicyID, selectedReportID, activeAdminPolic
const canUsePersonalBankAccount = isIOUReport;
const isPersonalOnlyOption = canUsePersonalBankAccount && !canUseBusinessBankAccount;
const shouldShowBusinessBankAccountOptions = isExpenseReport && !isPersonalOnlyOption;
- const formattedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, fundList ?? {}, styles);
+ const formattedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, fundList ?? {}, styles, translate);
const canUseWallet = !isExpenseReport && !isInvoiceReport && isCurrencySupportedWallet;
const hasSinglePolicy = !isExpenseReport && activeAdminPolicies.length === 1;
const hasMultiplePolicies = !isExpenseReport && activeAdminPolicies.length > 1;
@@ -81,23 +82,7 @@ function useBulkPayOptions({selectedPolicyID, selectedReportID, activeAdminPolic
const bulkPayButtonOptions = useMemo(() => {
const buttonOptions = [];
- const paymentMethods = {
- [CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]: {
- text: hasActivatedWallet ? translate('iou.settleWallet', {formattedAmount: ''}) : translate('iou.settlePersonal', {formattedAmount: ''}),
- icon: User,
- key: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT,
- },
- [CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]: {
- text: translate('iou.settleBusiness', {formattedAmount: ''}),
- icon: Building,
- key: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT,
- },
- [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: {
- text: translate('iou.payElsewhere', {formattedAmount: ''}),
- icon: CheckCircle,
- key: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
- },
- };
+ const paymentMethods = getSettlementButtonPaymentMethods(hasActivatedWallet, translate);
if (!selectedReportID || !selectedPolicyID) {
return undefined;
diff --git a/src/hooks/useCardFeedsForDisplay.ts b/src/hooks/useCardFeedsForDisplay.ts
index 9bc679c7fc71e..ba68a47d0dd4d 100644
--- a/src/hooks/useCardFeedsForDisplay.ts
+++ b/src/hooks/useCardFeedsForDisplay.ts
@@ -1,25 +1,29 @@
import {useMemo} from 'react';
+import type {OnyxCollection} from 'react-native-onyx';
import {getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils';
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {isPaidGroupPolicy} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy} from '@src/types/onyx';
import useLocalize from './useLocalize';
import useOnyx from './useOnyx';
+const eligiblePoliciesSelector = (policies: OnyxCollection) => {
+ return Object.values(policies ?? {}).reduce((policiesIDs, policy) => {
+ if (isPaidGroupPolicy(policy) && policy?.areCompanyCardsEnabled) {
+ policiesIDs.add(policy.id);
+ }
+ return policiesIDs;
+ }, new Set());
+};
+
const useCardFeedsForDisplay = () => {
const {localeCompare} = useLocalize();
const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
const [eligiblePoliciesIDs] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
- selector: (policies) => {
- return Object.values(policies ?? {}).reduce((policiesIDs, policy) => {
- if (isPaidGroupPolicy(policy) && policy?.areCompanyCardsEnabled) {
- policiesIDs.add(policy.id);
- }
- return policiesIDs;
- }, new Set());
- },
+ selector: eligiblePoliciesSelector,
canBeMissing: true,
});
diff --git a/src/hooks/useConditionalCreateEmptyReportConfirmation.ts b/src/hooks/useConditionalCreateEmptyReportConfirmation.ts
index a2c3158451ebe..0a564ad65884c 100644
--- a/src/hooks/useConditionalCreateEmptyReportConfirmation.ts
+++ b/src/hooks/useConditionalCreateEmptyReportConfirmation.ts
@@ -16,6 +16,8 @@ type UseConditionalCreateEmptyReportConfirmationParams = {
onCreateReport: () => void;
/** Optional callback executed when the confirmation modal is cancelled */
onCancel?: () => void;
+ /** Whether the confirmation modal should be bypassed even if an empty report exists */
+ shouldBypassConfirmation?: boolean;
};
type UseConditionalCreateEmptyReportConfirmationResult = {
@@ -36,6 +38,7 @@ export default function useConditionalCreateEmptyReportConfirmation({
policyName,
onCreateReport,
onCancel,
+ shouldBypassConfirmation = false,
}: UseConditionalCreateEmptyReportConfirmationParams): UseConditionalCreateEmptyReportConfirmationResult {
const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: true});
type ReportSummary = ReturnType[number];
@@ -54,13 +57,13 @@ export default function useConditionalCreateEmptyReportConfirmation({
});
const handleCreateReport = useCallback(() => {
- if (hasEmptyReport) {
+ if (hasEmptyReport && !shouldBypassConfirmation) {
openCreateReportConfirmation();
return;
}
onCreateReport();
- }, [hasEmptyReport, onCreateReport, openCreateReportConfirmation]);
+ }, [hasEmptyReport, onCreateReport, openCreateReportConfirmation, shouldBypassConfirmation]);
return {
handleCreateReport,
diff --git a/src/hooks/useDeleteTransactions.ts b/src/hooks/useDeleteTransactions.ts
index 3f6115894f44f..cd3ee488d9392 100644
--- a/src/hooks/useDeleteTransactions.ts
+++ b/src/hooks/useDeleteTransactions.ts
@@ -9,6 +9,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportAction, Transaction, TransactionViolations} from '@src/types/onyx';
import useArchivedReportsIdSet from './useArchivedReportsIdSet';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useOnyx from './useOnyx';
import usePermissions from './usePermissions';
@@ -32,6 +33,9 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
const [allPolicyRecentlyUsedCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES, {canBeMissing: true});
const [allReportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true});
const {isBetaEnabled} = usePermissions();
+ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
+ const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const archivedReportsIdSet = useArchivedReportsIdSet();
@@ -92,6 +96,7 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
);
Object.keys(splitTransactionsByOriginalTransactionID).forEach((transactionID) => {
+ // eslint-disable-next-line unicorn/prefer-set-has
const splitIDs = (splitTransactionsByOriginalTransactionID[transactionID] ?? []).map((transaction) => transaction.transactionID);
const childTransactions = getChildTransactions(allTransactions, allReports, transactionID).filter(
(transaction) => !splitIDs.includes(transaction?.transactionID ?? String(CONST.DEFAULT_NUMBER_ID)),
@@ -129,7 +134,10 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
chatReport,
firstIOU: originalTransactionIouActions.at(0),
isChatReportArchived: isChatIOUReportArchived,
- isNewDotRevertSplitsEnabled: isBetaEnabled(CONST.BETAS.NEWDOT_REVERT_SPLITS),
+ currentUserAccountIDParam: currentUserPersonalDetails?.accountID,
+ currentUserEmailParam: currentUserPersonalDetails?.login ?? '',
+ transactionViolations,
+ isASAPSubmitBetaEnabled,
});
});
@@ -162,7 +170,21 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
return Array.from(deletedTransactionThreadReportIDs);
},
- [reportActions, isBetaEnabled, allTransactions, allReports, report, allReportNameValuePairs, allPolicyRecentlyUsedCategories, policyCategories, policy, archivedReportsIdSet],
+ [
+ reportActions,
+ allTransactions,
+ allReports,
+ report,
+ allReportNameValuePairs,
+ allPolicyRecentlyUsedCategories,
+ policyCategories,
+ policy,
+ archivedReportsIdSet,
+ currentUserPersonalDetails.accountID,
+ currentUserPersonalDetails.login,
+ isASAPSubmitBetaEnabled,
+ transactionViolations,
+ ],
);
return {
diff --git a/src/hooks/useHandleSelectionMode.ts b/src/hooks/useHandleSelectionMode.ts
new file mode 100644
index 0000000000000..77efb9a3fa389
--- /dev/null
+++ b/src/hooks/useHandleSelectionMode.ts
@@ -0,0 +1,52 @@
+import {useIsFocused} from '@react-navigation/native';
+import {useEffect, useRef} from 'react';
+import type {ListItem} from '@components/SelectionListWithSections/types';
+import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
+import useMobileSelectionMode from './useMobileSelectionMode';
+import useResponsiveLayout from './useResponsiveLayout';
+
+function useHandleSelectionMode(selectedItems: string[] | TItem[]) {
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {isSmallScreenWidth} = useResponsiveLayout();
+ const isFocused = useIsFocused();
+
+ const isMobileSelectionModeEnabled = useMobileSelectionMode();
+ // Check if selection should be on when the modal is opened
+ const wasSelectionOnRef = useRef(false);
+ // Keep track of the number of selected items to determine if we should turn off selection mode
+ const selectionRef = useRef(0);
+
+ useEffect(() => {
+ selectionRef.current = selectedItems.length;
+
+ if (!isSmallScreenWidth) {
+ if (selectedItems.length === 0 && isMobileSelectionModeEnabled) {
+ turnOffMobileSelectionMode();
+ }
+ return;
+ }
+ if (!isFocused) {
+ return;
+ }
+ if (!wasSelectionOnRef.current && selectedItems.length > 0) {
+ wasSelectionOnRef.current = true;
+ }
+ if (selectedItems.length > 0 && !isMobileSelectionModeEnabled) {
+ turnOnMobileSelectionMode();
+ } else if (selectedItems.length === 0 && isMobileSelectionModeEnabled && !wasSelectionOnRef.current) {
+ turnOffMobileSelectionMode();
+ }
+ }, [isMobileSelectionModeEnabled, isSmallScreenWidth, isFocused, selectedItems.length]);
+
+ useEffect(
+ () => () => {
+ if (selectionRef.current !== 0) {
+ return;
+ }
+ turnOffMobileSelectionMode();
+ },
+ [],
+ );
+}
+
+export default useHandleSelectionMode;
diff --git a/src/hooks/useIsPaidPolicyAdmin.ts b/src/hooks/useIsPaidPolicyAdmin.ts
new file mode 100644
index 0000000000000..fcdfdc8e11dce
--- /dev/null
+++ b/src/hooks/useIsPaidPolicyAdmin.ts
@@ -0,0 +1,30 @@
+import {useCallback} from 'react';
+import type {OnyxCollection} from 'react-native-onyx';
+import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy} from '@src/types/onyx';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import useOnyx from './useOnyx';
+
+/**
+ * Custom hook to check if the current user is an admin of any paid policy
+ */
+function useIsPaidPolicyAdmin() {
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+
+ const isUserPaidPolicyAdminSelector = useCallback(
+ (policies: OnyxCollection) => {
+ return Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserPersonalDetails.login));
+ },
+ [currentUserPersonalDetails?.login],
+ );
+
+ const [isCurrentUserPolicyAdmin = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
+ canBeMissing: true,
+ selector: isUserPaidPolicyAdminSelector,
+ });
+
+ return isCurrentUserPolicyAdmin;
+}
+
+export default useIsPaidPolicyAdmin;
diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts
index 1851829139848..54647409d4363 100644
--- a/src/hooks/useOnboardingFlow.ts
+++ b/src/hooks/useOnboardingFlow.ts
@@ -1,11 +1,11 @@
import {isSingleNewDotEntrySelector} from '@selectors/HybridApp';
+import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding';
import {emailSelector} from '@selectors/Session';
import {useEffect, useMemo, useRef} from 'react';
import {InteractionManager} from 'react-native';
import {startOnboardingFlow} from '@libs/actions/Welcome/OnboardingFlow';
import getCurrentUrl from '@libs/Navigation/currentUrl';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
-import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import {isLoggingInAsNewUser} from '@libs/SessionUtils';
import isProductTrainingElementDismissed from '@libs/TooltipUtils';
@@ -129,6 +129,7 @@ function useOnboardingFlowRouter() {
currentOnboardingCompanySize,
currentOnboardingPurposeSelected,
onboardingInitialPath,
+ onboardingValues,
});
}
}
@@ -143,6 +144,7 @@ function useOnboardingFlowRouter() {
currentOnboardingCompanySize,
currentOnboardingPurposeSelected,
onboardingInitialPath,
+ onboardingValues,
});
}
});
diff --git a/src/hooks/useOnboardingMessages/index.ts b/src/hooks/useOnboardingMessages/index.ts
index ab9e0358a10dd..4e4eecbda4032 100644
--- a/src/hooks/useOnboardingMessages/index.ts
+++ b/src/hooks/useOnboardingMessages/index.ts
@@ -1,16 +1,9 @@
import {useMemo} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
-import useOnyx from '@hooks/useOnyx';
import {getOnboardingMessages} from '@libs/actions/Welcome/OnboardingFlow';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type IntroSelectedTask from '@src/types/onyx/IntroSelected';
-
-const hasIntroSelectedSelector = (introSelected: OnyxEntry) => !!introSelected?.choice;
export default function useOnboardingMessages() {
const {preferredLocale} = useLocalize();
- const [hasIntroSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true, selector: hasIntroSelectedSelector});
- const onboardingMessages = useMemo(() => getOnboardingMessages(hasIntroSelected, preferredLocale), [hasIntroSelected, preferredLocale]);
+ const onboardingMessages = useMemo(() => getOnboardingMessages(preferredLocale), [preferredLocale]);
return onboardingMessages;
}
diff --git a/src/hooks/usePaymentOptions.ts b/src/hooks/usePaymentOptions.ts
index 98376c0358919..cae5f5cd23643 100644
--- a/src/hooks/usePaymentOptions.ts
+++ b/src/hooks/usePaymentOptions.ts
@@ -157,7 +157,7 @@ function usePaymentOptions({
}
if (isInvoiceReport) {
- const formattedPaymentMethods = formatPaymentMethods(bankAccountList, fundList, styles);
+ const formattedPaymentMethods = formatPaymentMethods(bankAccountList, fundList, styles, translate);
const isCurrencySupported = isCurrencySupportedForDirectReimbursement(currency as CurrencyType);
const getPaymentSubitems = (payAsBusiness: boolean) =>
formattedPaymentMethods.map((formattedPaymentMethod) => ({
diff --git a/src/hooks/usePersonalPolicy.ts b/src/hooks/usePersonalPolicy.ts
index ee71cdaa1caef..911c3be214cf9 100644
--- a/src/hooks/usePersonalPolicy.ts
+++ b/src/hooks/usePersonalPolicy.ts
@@ -1,9 +1,9 @@
+import {createPoliciesSelector} from '@selectors/Policy';
import {useMemo} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type Policy from '@src/types/onyx/Policy';
-import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems';
import useOnyx from './useOnyx';
type PolicySelector = Pick;
@@ -15,8 +15,10 @@ const policySelector = (policy: OnyxEntry): PolicySelector =>
autoReporting: policy.autoReporting,
}) as PolicySelector;
+const allPoliciesSelector = (policies: OnyxCollection) => createPoliciesSelector(policies, policySelector);
+
function usePersonalPolicy() {
- const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (c) => mapOnyxCollectionItems(c, policySelector), canBeMissing: true});
+ const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: allPoliciesSelector, canBeMissing: true});
const personalPolicy = useMemo(() => Object.values(allPolicies ?? {}).find((policy) => policy?.type === CONST.POLICY.TYPE.PERSONAL), [allPolicies]);
return personalPolicy;
}
diff --git a/src/hooks/usePreferredEmojiSkinTone.ts b/src/hooks/usePreferredEmojiSkinTone.ts
index b833d0c2b79b6..36d7c04e520e7 100644
--- a/src/hooks/usePreferredEmojiSkinTone.ts
+++ b/src/hooks/usePreferredEmojiSkinTone.ts
@@ -1,11 +1,10 @@
import {useCallback} from 'react';
import {updatePreferredSkinTone as updatePreferredSkinToneAction} from '@userActions/User';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import useOnyx from './useOnyx';
export default function usePreferredEmojiSkinTone() {
- const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true});
+ const [preferredSkinTone] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true});
const updatePreferredSkinTone = useCallback(
(skinTone: number) => {
diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts
index 5d0a1ca2a5685..8cb73b30ea8ec 100644
--- a/src/hooks/useSearchHighlightAndScroll.ts
+++ b/src/hooks/useSearchHighlightAndScroll.ts
@@ -46,7 +46,7 @@ function useSearchHighlightAndScroll({
const searchTriggeredRef = useRef(false);
const hasNewItemsRef = useRef(false);
const previousSearchResults = usePrevious(searchResults?.data);
- const [newSearchResultKey, setNewSearchResultKey] = useState(null);
+ const [newSearchResultKeys, setNewSearchResultKeys] = useState | null>(null);
const highlightedIDs = useRef>(new Set());
const initializedRef = useRef(false);
const hasPendingSearchRef = useRef(false);
@@ -182,11 +182,13 @@ function useSearchHighlightAndScroll({
return;
}
- const newReportActionID = newReportActionIDs.at(0) ?? '';
- const newReportActionKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newReportActionID}`;
-
- setNewSearchResultKey(newReportActionKey);
- highlightedIDs.current.add(newReportActionID);
+ const newKeys = new Set();
+ newReportActionIDs.forEach((id) => {
+ const newReportActionKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${id}`;
+ highlightedIDs.current.add(newReportActionKey);
+ newKeys.add(newReportActionKey);
+ });
+ setNewSearchResultKeys(newKeys);
} else {
const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults);
const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data);
@@ -198,26 +200,28 @@ function useSearchHighlightAndScroll({
return;
}
- const newTransactionID = newTransactionIDs.at(0) ?? '';
- const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${newTransactionID}`;
-
- setNewSearchResultKey(newTransactionKey);
- highlightedIDs.current.add(newTransactionID);
+ const newKeys = new Set();
+ newTransactionIDs.forEach((id) => {
+ const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${id}`;
+ highlightedIDs.current.add(newTransactionKey);
+ newKeys.add(newTransactionKey);
+ });
+ setNewSearchResultKeys(newKeys);
}
}, [searchResults?.data, previousSearchResults, isChat]);
// Reset newSearchResultKey after it's been used
useEffect(() => {
- if (newSearchResultKey === null) {
+ if (newSearchResultKeys === null) {
return;
}
const timer = setTimeout(() => {
- setNewSearchResultKey(null);
+ setNewSearchResultKeys(null);
}, CONST.ANIMATED_HIGHLIGHT_START_DURATION);
return () => clearTimeout(timer);
- }, [newSearchResultKey]);
+ }, [newSearchResultKeys]);
/**
* Callback to handle scrolling to the new search result.
@@ -226,7 +230,8 @@ function useSearchHighlightAndScroll({
(data: SearchListItem[], ref: SelectionListHandle | null) => {
// Early return if there's no ref, new transaction wasn't brought in by this hook
// or there's no new search result key
- if (!ref || !triggeredByHookRef.current || newSearchResultKey === null) {
+ const newSearchResultKey = newSearchResultKeys?.values().next().value;
+ if (!ref || !triggeredByHookRef.current || !newSearchResultKey) {
return;
}
@@ -264,10 +269,10 @@ function useSearchHighlightAndScroll({
// Reset the trigger flag to prevent unintended future scrolls and highlights
triggeredByHookRef.current = false;
},
- [newSearchResultKey, isChat],
+ [newSearchResultKeys, isChat],
);
- return {newSearchResultKey, handleSelectionListScroll, newTransactions};
+ return {newSearchResultKeys, handleSelectionListScroll, newTransactions};
}
/**
diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts
index f60a5c422d207..d4604949dd6e7 100644
--- a/src/hooks/useSearchSelector.base.ts
+++ b/src/hooks/useSearchSelector.base.ts
@@ -157,6 +157,7 @@ function useSearchSelectorBase({
const [maxResults, setMaxResults] = useState(maxResultsPerPage);
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false});
const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true});
+ const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
const onListEndReached = useCallback(() => {
setMaxResults((previous) => previous + maxResultsPerPage);
@@ -176,6 +177,7 @@ function useSearchSelectorBase({
return getSearchOptions({
options: optionsWithContacts,
draftComments,
+ nvpDismissedProductTraining,
betas: betas ?? [],
isUsedInChatFinder: true,
includeReadOnly: true,
@@ -185,7 +187,7 @@ function useSearchSelectorBase({
countryCode,
});
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE:
- return getValidOptions(optionsWithContacts, draftComments, {
+ return getValidOptions(optionsWithContacts, draftComments, nvpDismissedProductTraining, {
betas: betas ?? [],
includeP2P: true,
includeSelectedOptions: false,
@@ -197,7 +199,7 @@ function useSearchSelectorBase({
includeUserToInvite,
});
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL:
- return getValidOptions(optionsWithContacts, draftComments, {
+ return getValidOptions(optionsWithContacts, draftComments, nvpDismissedProductTraining, {
...getValidOptionsConfig,
betas: betas ?? [],
searchString: computedSearchTerm,
@@ -210,6 +212,7 @@ function useSearchSelectorBase({
return getValidOptions(
optionsWithContacts,
draftComments,
+ nvpDismissedProductTraining,
{
betas,
includeMultipleParticipantReports: true,
@@ -226,7 +229,7 @@ function useSearchSelectorBase({
countryCode,
);
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_SHARE_DESTINATION:
- return getValidOptions(optionsWithContacts, draftComments, {
+ return getValidOptions(optionsWithContacts, draftComments, nvpDismissedProductTraining, {
betas,
selectedOptions,
includeMultipleParticipantReports: true,
@@ -261,6 +264,7 @@ function useSearchSelectorBase({
maxRecentReportsToShow,
getValidOptionsConfig,
selectedOptions,
+ nvpDismissedProductTraining,
]);
const isOptionSelected = useMemo(() => {
diff --git a/src/hooks/useShortMentionsList.ts b/src/hooks/useShortMentionsList.ts
index 32a04d53f9e61..69361a9013f9d 100644
--- a/src/hooks/useShortMentionsList.ts
+++ b/src/hooks/useShortMentionsList.ts
@@ -1,6 +1,6 @@
import {useMemo} from 'react';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
-import {areEmailsFromSamePrivateDomain} from '@libs/LoginUtils';
+import {getEmailDomain, isDomainPublic} from '@libs/LoginUtils';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
/**
@@ -18,14 +18,20 @@ export default function useShortMentionsList() {
return [];
}
+ const currentUserDomain = getEmailDomain(currentUserPersonalDetails.login ?? '');
+ const isCurrentUserPublicDomain = isDomainPublic(currentUserDomain);
+
return Object.values(personalDetails)
.map((personalDetail) => {
- if (!personalDetail?.login) {
+ if (!personalDetail?.login || isCurrentUserPublicDomain) {
return;
}
+ const personalDetailDomain = getEmailDomain(personalDetail.login);
+ const isPersonalDetailPublicDomain = isDomainPublic(personalDetailDomain);
+
// If the emails are not in the same private domain, we don't want to highlight them
- if (!areEmailsFromSamePrivateDomain(personalDetail.login, currentUserPersonalDetails.login ?? '')) {
+ if (isPersonalDetailPublicDomain || personalDetailDomain !== currentUserDomain) {
return;
}
diff --git a/src/hooks/useShowNotFoundPageInIOUStep.ts b/src/hooks/useShowNotFoundPageInIOUStep.ts
index bd9a199b1da91..689633198a092 100644
--- a/src/hooks/useShowNotFoundPageInIOUStep.ts
+++ b/src/hooks/useShowNotFoundPageInIOUStep.ts
@@ -23,6 +23,7 @@ const useShowNotFoundPageInIOUStep = (action: IOUAction, iouType: IOUType, repor
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true});
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`, {canBeMissing: true});
const reportActionsReportID = useMemo(() => {
let actionsReportID;
@@ -61,7 +62,7 @@ const useShowNotFoundPageInIOUStep = (action: IOUAction, iouType: IOUType, repor
} else if (isSplitExpense) {
shouldShowNotFoundPage = !canEditSplitExpense;
} else {
- shouldShowNotFoundPage = !isMoneyRequestAction(reportAction) || !canEditMoneyRequest(reportAction, false, report, policy, transaction);
+ shouldShowNotFoundPage = !isMoneyRequestAction(reportAction) || !canEditMoneyRequest(reportAction, false, iouReport, policy, transaction);
}
}
diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx
index b139457b0eaac..f06b006a826e7 100644
--- a/src/hooks/useSidebarOrderedReports.tsx
+++ b/src/hooks/useSidebarOrderedReports.tsx
@@ -111,6 +111,7 @@ function SidebarOrderedReportsContextProvider({
} else if (reportsDraftsUpdates) {
reportsToUpdate = Object.keys(reportsDraftsUpdates).map((key) => key.replace(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, ONYXKEYS.COLLECTION.REPORT));
} else if (policiesUpdates) {
+ // eslint-disable-next-line unicorn/prefer-set-has
const updatedPolicies = Object.keys(policiesUpdates).map((key) => key.replace(ONYXKEYS.COLLECTION.POLICY, ''));
reportsToUpdate = Object.entries(chatReports ?? {})
.filter(([, value]) => {
diff --git a/src/hooks/useTransactionViolationOfWorkspace.tsx b/src/hooks/useTransactionViolationOfWorkspace.tsx
index 87d391fbce85f..7da226f824f9e 100644
--- a/src/hooks/useTransactionViolationOfWorkspace.tsx
+++ b/src/hooks/useTransactionViolationOfWorkspace.tsx
@@ -1,8 +1,10 @@
+import {useCallback} from 'react';
+import type {OnyxCollection} from 'react-native-onyx';
import {extractCollectionItemID} from '@libs/CollectionUtils';
import {getReportTransactions, isChatRoom, isPolicyExpenseChat, isPolicyRelatedReport, isTaskReport} from '@libs/ReportUtils';
import type {OnyxCollectionKey} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Report} from '@src/types/onyx';
+import type {Report, TransactionViolations} from '@src/types/onyx';
import useOnyx from './useOnyx';
function useTransactionViolationOfWorkspace(policyID?: string) {
@@ -20,29 +22,35 @@ function useTransactionViolationOfWorkspace(policyID?: string) {
transactionIDSet.add(transaction.transactionID);
}
});
+
+ const transactionViolationSelector = useCallback(
+ (violations: OnyxCollection) => {
+ if (!violations) {
+ return {};
+ }
+
+ const filteredViolationKeys = Object.keys(violations).filter((violationKey) => {
+ const transactionID = extractCollectionItemID(violationKey as `${OnyxCollectionKey}${string}`);
+ return transactionIDSet.has(transactionID);
+ });
+
+ const filteredViolations = filteredViolationKeys.reduce(
+ (acc, key) => {
+ acc[key] = violations[key];
+ return acc;
+ },
+ {} as typeof violations,
+ );
+
+ return filteredViolations;
+ },
+ [transactionIDSet],
+ );
+
const [transactionViolations] = useOnyx(
ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
{
- selector: (violations) => {
- if (!violations) {
- return {};
- }
-
- const filteredViolationKeys = Object.keys(violations).filter((violationKey) => {
- const transactionID = extractCollectionItemID(violationKey as `${OnyxCollectionKey}${string}`);
- return transactionIDSet.has(transactionID);
- });
-
- const filteredViolations = filteredViolationKeys.reduce(
- (acc, key) => {
- acc[key] = violations[key];
- return acc;
- },
- {} as typeof violations,
- );
-
- return filteredViolations;
- },
+ selector: transactionViolationSelector,
canBeMissing: true,
},
[transactionIDSet],
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 943762000a908..b4ef886a0ed16 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -442,6 +442,9 @@ const translations = {
zipPostCode: 'Postleitzahl',
whatThis: 'Was ist das?',
iAcceptThe: 'Ich akzeptiere die',
+ acceptTermsAndPrivacy: `Ich akzeptiere die Expensify-Nutzungsbedingungen und Datenschutzrichtlinie`,
+ acceptTermsAndConditions: `Ich akzeptiere die Allgemeine Geschäftsbedingungen`,
+ acceptTermsOfService: `Ich akzeptiere die Expensify-Nutzungsbedingungen`,
remove: 'Entfernen',
admin: 'Admin',
owner: 'EigentĂźmer',
@@ -934,17 +937,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`Dieser Chatraum ist fĂźr alles, was mit ${reportName} zu tun hat.`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `Dieser Chat ist fßr Rechnungen zwischen ${invoicePayer} und ${invoiceReceiver}. Verwenden Sie die Schaltfläche ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}, um eine Rechnung zu senden.`,
+ `Dieser Chat ist fßr Rechnungen zwischen ${invoicePayer} und ${invoiceReceiver}. Verwenden Sie die Schaltfläche +, um eine Rechnung zu senden.`,
beginningOfChatHistory: 'Dieser Chat ist mit',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `Hier wird ${submitterDisplayName} die Ausgaben an ${workspaceName} ßbermitteln. Verwenden Sie einfach die Schaltfläche ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ `Hier wird ${submitterDisplayName} die Ausgaben an ${workspaceName} ßbermitteln. Verwenden Sie einfach die Schaltfläche +.`,
beginningOfChatHistorySelfDM: 'Dies ist Ihr persĂśnlicher Bereich. Nutzen Sie ihn fĂźr Notizen, Aufgaben, EntwĂźrfe und Erinnerungen.',
beginningOfChatHistorySystemDM: 'Willkommen! Lassen Sie uns mit der Einrichtung beginnen.',
chatWithAccountManager: 'Hier mit Ihrem Kundenbetreuer chatten',
sayHello: 'Hallo!',
yourSpace: 'Ihr Bereich',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Willkommen in ${roomName}!`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Verwenden Sie die ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} Taste, um ${additionalText} einen Ausgabenposten hinzuzufĂźgen.`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Verwenden Sie die + Taste, um ${additionalText} einen Ausgabenposten hinzuzufĂźgen.`,
askConcierge: 'Stellen Sie Fragen und erhalten Sie rund um die Uhr UnterstĂźtzung in Echtzeit.',
conciergeSupport: '24/7 Support',
create: 'erstellen',
@@ -2258,10 +2261,9 @@ ${amount} fĂźr ${merchant} - ${date}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `in ${policyName}`,
- generatingPDF: 'PDF wird generiert',
+ generatingPDF: 'PDF wird generiert...',
waitForPDF: 'Bitte warten Sie, während wir das PDF erstellen.',
errorPDF: 'Beim Versuch, Ihr PDF zu erstellen, ist ein Fehler aufgetreten.',
- generatedPDF: 'Ihr Bericht als PDF wurde erstellt!',
},
reportDescriptionPage: {
roomDescription: 'Zimmerbeschreibung',
@@ -2466,7 +2468,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
title: 'Reiche eine Ausgabe ein',
description:
'*Reiche eine Ausgabe ein*, indem du einen Betrag eingibst oder einen Beleg scannst.\n\n' +
- `1. Klicke auf den ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-Button.\n` +
+ `1. Klicke auf den +-Button.\n` +
'2. Wähle *Ausgabe erstellen*.\n' +
'3. Betrag eingeben oder Beleg scannen.\n' +
`4. Gib die E-Mail oder Telefonnummer deines Chefs ein.\n` +
@@ -2477,7 +2479,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
title: 'Reiche eine Ausgabe ein',
description:
'*Reiche eine Ausgabe ein*, indem du einen Betrag eingibst oder einen Beleg scannst.\n\n' +
- `1. Klicke auf den ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-Button.\n` +
+ `1. Klicke auf den +-Button.\n` +
'2. Wähle *Ausgabe erstellen*.\n' +
'3. Betrag eingeben oder Beleg scannen.\n' +
'4. Details bestätigen.\n' +
@@ -2488,7 +2490,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
title: 'Verfolge eine Ausgabe',
description:
'*Verfolge eine Ausgabe* in jeder Währung â mit oder ohne Beleg.\n\n' +
- `1. Klicke auf den ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-Button.\n` +
+ `1. Klicke auf den +-Button.\n` +
'2. Wähle *Ausgabe erstellen*.\n' +
'3. Betrag eingeben oder Beleg scannen.\n' +
'4. Wähle deinen *persÜnlichen* Bereich.\n' +
@@ -2571,7 +2573,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
title: 'Starte einen Chat',
description:
'*Starte einen Chat* mit jeder Person Ăźber E-Mail oder Telefonnummer.\n\n' +
- `1. Klicke auf den ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-Button.\n` +
+ `1. Klicke auf den +-Button.\n` +
'2. Wähle *Chat starten*.\n' +
'3. Gib eine E-Mail oder Telefonnummer ein.\n\n' +
'Falls die Person Expensify noch nicht nutzt, wird sie automatisch eingeladen.\n\n' +
@@ -2581,7 +2583,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
title: 'Teile eine Ausgabe',
description:
'*Teile Ausgaben* mit einer oder mehreren Personen.\n\n' +
- `1. Klicke auf den ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-Button.\n` +
+ `1. Klicke auf den +-Button.\n` +
'2. Wähle *Chat starten*.\n' +
'3. Gib E-Mail-Adressen oder Telefonnummern ein.\n' +
'4. Klicke im Chat auf den grauen *+*-Button > *Ausgabe teilen*.\n' +
@@ -2601,7 +2603,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
title: 'Erstelle deinen ersten Bericht',
description:
'So erstellst du einen Bericht:\n\n' +
- `1. Klicke auf den ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-Button.\n` +
+ `1. Klicke auf den +-Button.\n` +
'2. Wähle *Bericht erstellen*.\n' +
'3. Klicke auf *Ausgabe hinzufĂźgen*.\n' +
'4. FĂźge deine erste Ausgabe hinzu.\n\n' +
@@ -2618,10 +2620,8 @@ ${amount} fĂźr ${merchant} - ${date}`,
messages: {
onboardingEmployerOrSubmitMessage: 'Erstattungen zu erhalten ist so einfach wie eine Nachricht zu senden. Lass uns die Grundlagen durchgehen.',
onboardingPersonalSpendMessage: 'So verfolgst du deine Ausgaben mit nur wenigen Klicks.',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# Deine kostenlose Testphase hat begonnen! Lass uns alles einrichten.\nđ Hallo, ich bin dein Expensify-Einrichtungsspezialist. Da du nun einen Workspace erstellt hast, nutze deine 30-tägige kostenlose Testphase optimal, indem du die folgenden Schritte befolgst!'
- : '# Deine kostenlose Testphase hat begonnen! Lass uns alles einrichten.\nđ Hallo, ich bin dein Expensify-Einrichtungsspezialist. Ich habe bereits einen Workspace erstellt, um die Belege und Ausgaben deines Teams zu verwalten. Um deine 30-tägige kostenlose Testphase optimal zu nutzen, folge einfach den verbleibenden Einrichtungsschritten unten!',
+ onboardingManageTeamMessage:
+ '# Deine kostenlose Testversion hat begonnen! Lass uns mit der Einrichtung loslegen.\nđ Hallo, ich bin dein Expensify-Einrichtungsassistent. Jetzt, da du einen Workspace erstellt hast, hole das Beste aus deiner 30-tägigen kostenlosen Testphase heraus, indem du die folgenden Schritte befolgst!',
onboardingTrackWorkspaceMessage:
'# Lass uns loslegen\nđ Ich helfe dir! Ich habe deine Workspace-Einstellungen fĂźr Einzelunternehmer und ähnliche Unternehmen angepasst. Du kannst sie Ăźber den folgenden Link anpassen!\n\nSo verfolgst du deine Ausgaben mit nur wenigen Klicks:',
onboardingChatSplitMessage: 'Rechnungen mit Freunden zu teilen ist so einfach wie eine Nachricht zu senden. So funktioniertâs.',
@@ -4571,9 +4571,8 @@ ${amount} fĂźr ${merchant} - ${date}`,
cardholder: 'Karteninhaber',
card: 'Karte',
cardName: 'Kartenname',
- brokenConnectionErrorFirstPart: `Die Verbindung zum Karten-Feed ist unterbrochen. Bitte`,
- brokenConnectionErrorLink: 'Melden Sie sich bei Ihrer Bank an',
- brokenConnectionErrorSecondPart: 'damit wir die Verbindung erneut herstellen kĂśnnen.',
+ brokenConnectionError:
+ 'Die Verbindung zum Karten-Feed ist unterbrochen. Bitte Melden Sie sich bei Ihrer Bank an damit wir die Verbindung erneut herstellen kĂśnnen.',
assignedCard: ({assignee, link}: AssignedCardParams) => `hat ${assignee} einen ${link} zugewiesen! Importierte Transaktionen werden in diesem Chat angezeigt.`,
companyCard: 'Firmenkarte',
chooseCardFeed: 'Karten-Feed auswählen',
@@ -4624,6 +4623,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
monthly: 'Monatlich',
},
cardDetails: 'Kartendetails',
+ cardPending: ({name}: {name: string}) => `Die Karte ist derzeit ausstehend und wird ausgestellt, sobald ${name}s Konto verifiziert wurde.`,
virtual: 'Virtuell',
physical: 'Physisch',
deactivate: 'Karte deaktivieren',
@@ -4810,9 +4810,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
noAccountsFound: 'Keine Konten gefunden',
defaultCard: 'Standardkarte',
downgradeTitle: `Arbeitsbereich kann nicht herabgestuft werden`,
- downgradeSubTitleFirstPart: `Dieser Arbeitsbereich kann nicht herabgestuft werden, da mehrere Karten-Feeds verbunden sind (auĂer Expensify-Karten). Bitte`,
- downgradeSubTitleMiddlePart: `nur einen Karten-Feed behalten`,
- downgradeSubTitleLastPart: 'fortzufahren.',
+ downgradeSubTitle: `Dieser Arbeitsbereich kann nicht herabgestuft werden, da mehrere Karten-Feeds verbunden sind (auĂer Expensify-Karten). Bitte nur einen Karten-Feed behalten fortzufahren.`,
noAccountsFoundDescription: ({connection}: ConnectionParams) => `Bitte fĂźgen Sie das Konto in ${connection} hinzu und synchronisieren Sie die Verbindung erneut.`,
expensifyCardBannerTitle: 'Erhalte die Expensify-Karte',
expensifyCardBannerSubtitle:
@@ -4939,6 +4937,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
existingReportFieldNameError: 'Ein Berichtsfeld mit diesem Namen existiert bereits.',
reportFieldNameRequiredError: 'Bitte geben Sie einen Berichtsfeldnamen ein',
reportFieldTypeRequiredError: 'Bitte wählen Sie einen Berichtsfeldtyp aus',
+ circularReferenceError: 'Dieses Feld kann nicht auf sich selbst verweisen. Bitte aktualisieren Sie es.',
reportFieldInitialValueRequiredError: 'Bitte wählen Sie einen Anfangswert fßr das Berichtsfeld aus',
genericFailureMessage: 'Beim Aktualisieren des Berichtsfeldes ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.',
},
@@ -5501,8 +5500,8 @@ ${amount} fĂźr ${merchant} - ${date}`,
enableRate: 'Rate aktivieren',
status: 'Status',
unit: 'Einheit',
- taxFeatureNotEnabledMessage: 'Steuern mĂźssen im Arbeitsbereich aktiviert sein, um diese Funktion zu nutzen. Gehen Sie zu',
- changePromptMessage: 'um diese Ănderung vorzunehmen.',
+ taxFeatureNotEnabledMessage:
+ 'Steuern mĂźssen im Arbeitsbereich aktiviert sein, um diese Funktion zu nutzen. Gehen Sie zu Mehr Funktionen um diese Ănderung vorzunehmen.',
deleteDistanceRate: 'Entfernen Sie den Distanzsatz',
areYouSureDelete: () => ({
one: 'MĂśchten Sie diesen Satz wirklich lĂśschen?',
@@ -5704,6 +5703,12 @@ ${amount} fĂźr ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Entfernungsraten sind im Collect-Plan verfĂźgbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied pro Monat.` : `pro aktivem Mitglied pro Monat.`}`,
},
+ auditor: {
+ title: 'PrĂźfer',
+ description: 'PrĂźfer erhalten schreibgeschĂźtzten Zugriff auf alle Berichte fĂźr volle Transparenz und Ăberwachung der Compliance.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `PrĂźfer sind nur im Control-Plan verfĂźgbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied pro Monat.` : `pro aktivem Mitglied pro Monat.`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Mehrere Genehmigungsstufen',
description:
@@ -6211,7 +6216,7 @@ ${amount} fĂźr ${merchant} - ${date}`,
searchResults: {
emptyResults: {
title: 'Nichts zu zeigen',
- subtitle: `Versuchen Sie, Ihre Suchkriterien anzupassen oder etwas mit dem grĂźnen ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} Button zu erstellen.`,
+ subtitle: `Versuchen Sie, Ihre Suchkriterien anzupassen oder etwas mit dem + Button zu erstellen.`,
},
emptyExpenseResults: {
title: 'Sie haben noch keine Ausgaben erstellt.',
@@ -7404,6 +7409,28 @@ ${amount} fĂźr ${merchant} - ${date}`,
subtitle: `Wir konnten nicht alle Ihre Daten laden. Wir wurden benachrichtigt und untersuchen das Problem. Wenn das weiterhin besteht, wenden Sie sich bitte an`,
refreshAndTryAgain: 'Aktualisieren und erneut versuchen',
},
+ domain: {
+ notVerified: 'Nicht verifiziert',
+ retry: 'Erneut versuchen',
+ verifyDomain: {
+ title: 'Domain verifizieren',
+ beforeProceeding: ({domainName}: {domainName: string}) =>
+ `Bevor Sie fortfahren, bestätigen Sie, dass Sie ${domainName} besitzen, indem Sie die DNS-Einstellungen der Domain aktualisieren.`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `Greifen Sie auf Ihren DNS-Anbieter zu und Ăśffnen Sie die DNS-Einstellungen fĂźr ${domainName}.`,
+ addTXTRecord: 'FĂźgen Sie den folgenden TXT-Eintrag hinzu:',
+ saveChanges: 'Speichern Sie die Ănderungen und kehren Sie hierher zurĂźck, um Ihre Domain zu verifizieren.',
+ youMayNeedToConsult: `MĂśglicherweise mĂźssen Sie sich an die IT-Abteilung Ihrer Organisation wenden, um die Verifizierung abzuschlieĂen. Weitere Informationen.`,
+ warning: 'Nach der Verifizierung erhalten alle Expensify-Mitglieder in Ihrer Domain eine E-Mail, dass ihr Konto unter Ihrer Domain verwaltet wird.',
+ codeFetchError: 'Verifizierungscode konnte nicht abgerufen werden',
+ genericError: 'Wir konnten Ihre Domain nicht verifizieren. Bitte versuchen Sie es erneut und wenden Sie sich an Concierge, wenn das Problem weiterhin besteht.',
+ },
+ domainVerified: {
+ title: 'Domain verifiziert',
+ header: 'Wooo! Ihre Domain wurde verifiziert',
+ description: ({domainName}: {domainName: string}) =>
+ `Die Domain ${domainName} wurde erfolgreich verifiziert und Sie kĂśnnen jetzt SAML und andere Sicherheitsfunktionen einrichten.`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
// so if you change it here, please update it there as well.
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 0678c58f4565b..574e82991aee9 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -435,6 +435,9 @@ const translations = {
zipPostCode: 'Zip / Postcode',
whatThis: "What's this?",
iAcceptThe: 'I accept the ',
+ acceptTermsAndPrivacy: `I accept the Expensify Terms of Service and Privacy Policy`,
+ acceptTermsAndConditions: `I accept the terms and conditions`,
+ acceptTermsOfService: `I accept the Expensify Terms of Service`,
remove: 'Remove',
admin: 'Admin',
owner: 'Owner',
@@ -918,17 +921,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`This chat room is for anything ${reportName} related.`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `This chat is for invoices between ${invoicePayer} and ${invoiceReceiver}. Use the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button to send an invoice.`,
+ `This chat is for invoices between ${invoicePayer} and ${invoiceReceiver}. Use the + button to send an invoice.`,
beginningOfChatHistory: 'This chat is with ',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `This is where ${submitterDisplayName} will submit expenses to ${workspaceName}. Just use the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button.`,
+ `This is where ${submitterDisplayName} will submit expenses to ${workspaceName}. Just use the + button.`,
beginningOfChatHistorySelfDM: 'This is your personal space. Use it for notes, tasks, drafts, and reminders.',
beginningOfChatHistorySystemDM: "Welcome! Let's get you set up.",
chatWithAccountManager: 'Chat with your account manager here',
sayHello: 'Say hello!',
yourSpace: 'Your space',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Welcome to ${roomName}!`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Use the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button to ${additionalText} an expense.`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Use the + button to ${additionalText} an expense.`,
askConcierge: ' Ask questions and get 24/7 realtime support.',
conciergeSupport: '24/7 support',
create: 'create',
@@ -2218,10 +2221,9 @@ const translations = {
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `in ${policyName}`,
- generatingPDF: 'Generating PDF',
+ generatingPDF: 'Generating PDF...',
waitForPDF: 'Please wait while we generate the PDF',
errorPDF: 'There was an error when trying to generate your PDF',
- generatedPDF: 'Your report PDF has been generated!',
},
reportDescriptionPage: {
roomDescription: 'Room description',
@@ -2427,7 +2429,7 @@ const translations = {
description:
'*Submit an expense* by entering an amount or scanning a receipt.\n' +
'\n' +
- `1. Click the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button.\n` +
+ `1. Click the + button.\n` +
'2. Choose *Create expense*.\n' +
'3. Enter an amount or scan a receipt.\n' +
`4. Add your boss's email or phone number.\n` +
@@ -2440,7 +2442,7 @@ const translations = {
description:
'*Submit an expense* by entering an amount or scanning a receipt.\n' +
'\n' +
- `1. Click the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button.\n` +
+ `1. Click the + button.\n` +
'2. Choose *Create expense*.\n' +
'3. Enter an amount or scan a receipt.\n' +
'4. Confirm details.\n' +
@@ -2453,7 +2455,7 @@ const translations = {
description:
'*Track an expense* in any currency, whether you have a receipt or not.\n' +
'\n' +
- `1. Click the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button.\n` +
+ `1. Click the + button.\n` +
'2. Choose *Create expense*.\n' +
'3. Enter an amount or scan a receipt.\n' +
'4. Choose your *personal* space.\n' +
@@ -2551,7 +2553,7 @@ const translations = {
description:
'*Start a chat* with anyone using their email or phone number.\n' +
'\n' +
- `1. Click the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button.\n` +
+ `1. Click the + button.\n` +
'2. Choose *Start chat*.\n' +
'3. Enter an email or phone number.\n' +
'\n' +
@@ -2565,7 +2567,7 @@ const translations = {
description:
'*Split expenses* with one or more people.\n' +
'\n' +
- `1. Click the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button.\n` +
+ `1. Click the + button.\n` +
'2. Choose *Start chat*.\n' +
'3. Enter emails or phone numbers.\n' +
'4. Click the grey *+* button in the chat > *Split expense*.\n' +
@@ -2588,7 +2590,7 @@ const translations = {
description:
'Hereâs how to create a report:\n' +
'\n' +
- `1. Click the ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button.\n` +
+ `1. Click the + button.\n` +
'2. Choose *Create report*.\n' +
'3. Click *Add expense*.\n' +
'4. Add your first expense.\n' +
@@ -2606,10 +2608,8 @@ const translations = {
messages: {
onboardingEmployerOrSubmitMessage: 'Getting paid back is as easy as sending a message. Letâs go over the basics.',
onboardingPersonalSpendMessage: 'Hereâs how to track your spend in a few clicks.',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? "# Your free trial has started! Let's get you set up.\nđ Hey there, I'm your Expensify setup specialist. Now that you've created a workspace, make the most of your 30-day free trial by following the steps below!"
- : "# Your free trial has started! Let's get you set up.\nđ Hey there, I'm your Expensify setup specialist. I've already created a workspace to help manage your team's receipts and expenses. To make the most of your 30-day free trial, just follow the remaining setup steps below!",
+ onboardingManageTeamMessage:
+ "# Your free trial has started! Let's get you set up.\nđ Hey there, I'm your Expensify setup specialist. Now that you've created a workspace, make the most of your 30-day free trial by following the steps below!",
onboardingTrackWorkspaceMessage:
'# Letâs get you set up\nđ Iâm here to help! To get you started, Iâve tailored your workspace settings for sole proprietors and similar businesses. You can adjust your workspace by clicking the link below!\n\nHereâs how to track your spend in a few clicks:',
onboardingChatSplitMessage: 'Splitting bills with friends is as easy as sending a message. Hereâs how.',
@@ -4545,9 +4545,7 @@ const translations = {
cardholder: 'Cardholder',
card: 'Card',
cardName: 'Card name',
- brokenConnectionErrorFirstPart: `Card feed connection is broken. Please `,
- brokenConnectionErrorLink: 'log into your bank ',
- brokenConnectionErrorSecondPart: 'so we can establish the connection again.',
+ brokenConnectionError: 'Card feed connection is broken. Please log into your bank so we can establish the connection again.',
assignedCard: ({assignee, link}: AssignedCardParams) => `assigned ${assignee} a ${link}! Imported transactions will appear in this chat.`,
companyCard: 'company card',
chooseCardFeed: 'Choose card feed',
@@ -4598,6 +4596,7 @@ const translations = {
monthly: 'Monthly',
},
cardDetails: 'Card details',
+ cardPending: ({name}: {name: string}) => `Card is currently pending and will be issued once ${name}'s account is validated.`,
virtual: 'Virtual',
physical: 'Physical',
deactivate: 'Deactivate card',
@@ -4780,9 +4779,7 @@ const translations = {
noAccountsFound: 'No accounts found',
defaultCard: 'Default card',
downgradeTitle: `Can't downgrade workspace`,
- downgradeSubTitleFirstPart: `This workspace can't be downgraded because multiple card feeds are connected (excluding Expensify Cards). Please`,
- downgradeSubTitleMiddlePart: `keep only one card feed`,
- downgradeSubTitleLastPart: 'to proceed.',
+ downgradeSubTitle: `This workspace can't be downgraded because multiple card feeds are connected (excluding Expensify Cards). Please keep only one card feed to proceed.`,
noAccountsFoundDescription: ({connection}: ConnectionParams) => `Please add the account in ${connection} and sync the connection again`,
expensifyCardBannerTitle: 'Get the Expensify Card',
expensifyCardBannerSubtitle: 'Enjoy cash back on every US purchase, up to 50% off your Expensify bill, unlimited virtual cards, and so much more.',
@@ -4908,6 +4905,7 @@ const translations = {
existingReportFieldNameError: 'A report field with this name already exists',
reportFieldNameRequiredError: 'Please enter a report field name',
reportFieldTypeRequiredError: 'Please choose a report field type',
+ circularReferenceError: "This field can't refer to itself. Please update.",
reportFieldInitialValueRequiredError: 'Please choose a report field initial value',
genericFailureMessage: 'An error occurred while updating the report field. Please try again.',
},
@@ -5469,8 +5467,8 @@ const translations = {
enableRate: 'Enable rate',
status: 'Status',
unit: 'Unit',
- taxFeatureNotEnabledMessage: 'Taxes must be enabled on the workspace to use this feature. Head over to ',
- changePromptMessage: ' to make that change.',
+ taxFeatureNotEnabledMessage:
+ 'Taxes must be enabled on the workspace to use this feature. Head over to More features to make that change.',
deleteDistanceRate: 'Delete distance rate',
areYouSureDelete: () => ({
one: 'Are you sure you want to delete this rate?',
@@ -5604,7 +5602,7 @@ const translations = {
title: 'Categories',
description: 'Categories allow you to track and organize spend. Use our default categories or add your own.',
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Categories are available on the Collect plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`,
+ `Categories are available on the Collect plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`,
},
glCodes: {
title: 'GL codes',
@@ -5668,6 +5666,12 @@ const translations = {
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Distance rates are available on the Collect plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`,
},
+ auditor: {
+ title: 'Auditor',
+ description: 'Auditors get read-only access to all reports for full visibility and compliance monitoring.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Auditors are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Multiple approval levels',
description: 'Multiple approval levels is a workflow tool for companies that require more than one person to approve a report before it can be reimbursed.',
@@ -6172,7 +6176,7 @@ const translations = {
searchResults: {
emptyResults: {
title: 'Nothing to show',
- subtitle: `Try adjusting your search criteria or creating something with the green ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} button.`,
+ subtitle: `Try adjusting your search criteria or creating something with the + button.`,
},
emptyExpenseResults: {
title: "You haven't created any expenses yet",
@@ -7336,6 +7340,27 @@ const translations = {
exportInProgress: 'Export in progress',
conciergeWillSend: 'Concierge will send you the file shortly.',
},
+ domain: {
+ notVerified: 'Not verified',
+ retry: 'Retry',
+ verifyDomain: {
+ title: 'Verify domain',
+ beforeProceeding: ({domainName}: {domainName: string}) => `Before proceeding, verify that you own ${domainName} by updating its DNS settings.`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `Access your DNS provider and open DNS settings for ${domainName}.`,
+ addTXTRecord: 'Add the following TXT record:',
+ saveChanges: 'Save changes and return here to verify your domain.',
+ youMayNeedToConsult: `You may need to consult your organization's IT department to complete verification. Learn more.`,
+ warning: 'After verification, all Expensify members on your domain will receive an email that their account will be managed under your domain.',
+ codeFetchError: 'Couldnât fetch verification code',
+ genericError: "We couldn't verify your domain. Please try again and reach out to Concierge if the problem persists.",
+ },
+ domainVerified: {
+ title: 'Domain verified',
+ header: 'Wooo! Your domain has been verified',
+ description: ({domainName}: {domainName: string}) =>
+ `The domain ${domainName} has been successfully verified and you can now set up SAML and other security features.`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 49079d36b4cf4..1006e5360c222 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -423,6 +423,9 @@ const translations = {
zipPostCode: 'CĂłdigo postal',
whatThis: 'ÂżQuĂŠ es esto?',
iAcceptThe: 'Acepto los ',
+ acceptTermsAndPrivacy: `Acepto los TĂŠrminos de Servicio y la PolĂtica de Privacidad de Expensify`,
+ acceptTermsAndConditions: `Acepto los TĂŠrminos y Condiciones`,
+ acceptTermsOfService: `Acepto los TĂŠrminos de Servicio`,
remove: 'Eliminar',
admin: 'Administrador',
owner: 'DueĂąo',
@@ -625,7 +628,7 @@ const translations = {
expenseReports: 'Informes de Gastos',
rateOutOfPolicy: 'Tasa fuera de pĂłliza',
leaveWorkspace: 'Salir del espacio de trabajo',
- leaveWorkspaceConfirmation: 'Si sales de este espacio de trabajo, no podrĂĄs enviar gastos en ĂŠl',
+ leaveWorkspaceConfirmation: 'Si sales de este espacio de trabajo, no podrĂĄs enviar gastos en ĂŠl.',
leaveWorkspaceConfirmationAuditor: 'Si sales de este espacio de trabajo, no podrĂĄs ver sus informes y configuraciones.',
leaveWorkspaceConfirmationAdmin: 'Si sales de este espacio de trabajo, no podrĂĄs gestionar su configuraciĂłn.',
leaveWorkspaceConfirmationApprover: ({workspaceOwner}: {workspaceOwner: string}) =>
@@ -906,17 +909,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`Esta sala de chat es para cualquier cosa relacionada con ${reportName}.`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `Este chat es para facturas entre ${invoicePayer} y ${invoiceReceiver}. Usa el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} para enviar una factura.`,
+ `Este chat es para facturas entre ${invoicePayer} y ${invoiceReceiver}. Usa el botĂłn + para enviar una factura.`,
beginningOfChatHistory: 'Este chat es con ',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `AquĂ es donde ${submitterDisplayName} enviarĂĄ los gastos al espacio de trabajo ${workspaceName}. Solo usa el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ `AquĂ es donde ${submitterDisplayName} enviarĂĄ los gastos al espacio de trabajo ${workspaceName}. Solo usa el botĂłn +.`,
beginningOfChatHistorySelfDM: 'Este es tu espacio personal. Ăsalo para notas, tareas, borradores y recordatorios.',
beginningOfChatHistorySystemDM: 'ÂĄBienvenido! Vamos a configurar tu cuenta.',
chatWithAccountManager: 'Chatea con tu gestor de cuenta aquĂ',
sayHello: 'ÂĄSaluda!',
yourSpace: 'Tu espacio',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `ÂĄBienvenido a ${roomName}!`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Usa el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} para ${additionalText} un gasto`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Usa el botĂłn + para ${additionalText} un gasto`,
askConcierge: ' Haz preguntas y obtĂŠn soporte en tiempo real las 24/7.',
conciergeSupport: 'Soporte 24/7',
create: 'crear',
@@ -2223,10 +2226,9 @@ ${amount} para ${merchant} - ${date}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `en ${policyName}`,
- generatingPDF: 'Creando PDF',
+ generatingPDF: 'Creando PDF...',
waitForPDF: 'Por favor, espera mientras creamos el PDF',
errorPDF: 'OcurriĂł un error al crear el PDF',
- generatedPDF: 'Tu informe PDF ha sido creado!',
},
reportDescriptionPage: {
roomDescription: 'DescripciĂłn de la sala de chat',
@@ -2427,7 +2429,7 @@ ${amount} para ${merchant} - ${date}`,
title: 'EnvĂa un gasto',
description:
'*EnvĂa un gasto* introduciendo una cantidad o escaneando un recibo.\n\n' +
- `1. Haz clic en el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Haz clic en el botĂłn +.\n` +
'2. Elige *Crear gasto*.\n' +
'3. Introduce una cantidad o escanea un recibo.\n' +
'4. AĂąade el correo o telĂŠfono de tu jefe.\n' +
@@ -2438,7 +2440,7 @@ ${amount} para ${merchant} - ${date}`,
title: 'EnvĂa un gasto',
description:
'*EnvĂa un gasto* introduciendo una cantidad o escaneando un recibo.\n\n' +
- `1. Haz clic en el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Haz clic en el botĂłn +.\n` +
'2. Elige *Crear gasto*.\n' +
'3. Introduce una cantidad o escanea un recibo.\n' +
'4. Confirma los detalles.\n' +
@@ -2449,7 +2451,7 @@ ${amount} para ${merchant} - ${date}`,
title: 'Organiza un gasto',
description:
'*Organiza un gasto* en cualquier moneda, tengas recibo o no.\n\n' +
- `1. Haz clic en el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Haz clic en el botĂłn +.\n` +
'2. Elige *Crear gasto*.\n' +
'3. Introduce una cantidad o escanea un recibo.\n' +
'4. Elige tu espacio *personal*.\n' +
@@ -2533,7 +2535,7 @@ ${amount} para ${merchant} - ${date}`,
title: 'Inicia un chat',
description:
'*Inicia un chat* con cualquier persona usando su correo o nĂşmero.\n\n' +
- `1. Haz clic en el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Haz clic en el botĂłn +.\n` +
'2. Elige *Iniciar chat*.\n' +
'3. Introduce un correo o telĂŠfono.\n\n' +
'Si aĂşn no usan Expensify, se les invitarĂĄ automĂĄticamente.\n\n' +
@@ -2543,7 +2545,7 @@ ${amount} para ${merchant} - ${date}`,
title: 'Divide un gasto',
description:
'*Divide gastos* con una o mĂĄs personas.\n\n' +
- `1. Haz clic en el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Haz clic en el botĂłn +.\n` +
'2. Elige *Iniciar chat*.\n' +
'3. Introduce correos o telĂŠfonos.\n' +
'4. Haz clic en el botĂłn gris *+* en el chat > *Dividir gasto*.\n' +
@@ -2564,7 +2566,7 @@ ${amount} para ${merchant} - ${date}`,
description:
'AsĂ es como puedes crear un informe:\n' +
'\n' +
- `1. Haz clic en el botĂłn ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Haz clic en el botĂłn +.\n` +
'2. Elige *Crear informe*.\n' +
'3. Haz clic en *AĂąadir gasto*.\n' +
'4. AĂąade tu primer gasto.\n' +
@@ -2582,10 +2584,8 @@ ${amount} para ${merchant} - ${date}`,
messages: {
onboardingEmployerOrSubmitMessage: 'Que te reembolsen es tan fĂĄcil como enviar un mensaje. Repasemos lo bĂĄsico.',
onboardingPersonalSpendMessage: 'AquĂ tienes cĂłmo organizar tus gastos en unos pocos clics.',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# ÂĄTu prueba gratuita ha comenzado! Vamos a poner todo a punto.\nđ Hola, soy tu especialista de configuraciĂłn de Expensify. Ahora que has creado un espacio de trabajo, aprovecha al mĂĄximo tus 30 dĂas de prueba gratuita siguiendo los pasos que aparecen a continuaciĂłn.'
- : '# ÂĄTu prueba gratuita ha comenzado! Vamos a configurarlo.\nđ Hola, soy tu especialista asignado de Expensify. Ya he creado un espacio de trabajo para ayudarte a gestionar los recibos y gastos de tu equipo. Para aprovechar al mĂĄximo tu prueba gratuita de 30 dĂas, solo sigue los pasos de configuraciĂłn restantes a continuaciĂłn.',
+ onboardingManageTeamMessage:
+ '# ÂĄTu prueba gratuita ha comenzado! Vamos a poner todo a punto.\nđ Hola, soy tu especialista de configuraciĂłn de Expensify. Ahora que has creado un espacio de trabajo, aprovecha al mĂĄximo tus 30 dĂas de prueba gratuita siguiendo los pasos que aparecen a continuaciĂłn.',
onboardingTrackWorkspaceMessage:
'# Vamos a configurarte\nđ ÂĄEstoy aquĂ para ayudarte! Para comenzar, he personalizado la configuraciĂłn de tu espacio de trabajo para propietarios Ăşnicos y negocios similares. Puedes ajustar tu espacio de trabajo haciendo clic en el enlace de abajo.\n\nAsĂ es como puedes organizar tus gastos en unos pocos clics:',
onboardingChatSplitMessage: 'Dividir cuentas con amigos es tan fĂĄcil como enviar un mensaje. AsĂ se hace.',
@@ -4554,9 +4554,8 @@ ${amount} para ${merchant} - ${date}`,
cardholder: 'Titular de la tarjeta',
card: 'Tarjeta',
cardName: 'Nombre de la tarjeta',
- brokenConnectionErrorFirstPart: `La conexiĂłn de la fuente de tarjetas estĂĄ rota. Por favor, `,
- brokenConnectionErrorLink: 'inicia sesiĂłn en tu banco ',
- brokenConnectionErrorSecondPart: 'para que podamos restablecer la conexiĂłn.',
+ brokenConnectionError:
+ 'La conexiĂłn de la fuente de tarjetas estĂĄ rota. Por favor, inicia sesiĂłn en tu banco para que podamos restablecer la conexiĂłn.',
assignedCard: ({assignee, link}: AssignedCardParams) => `ha asignado a ${assignee} una ${link}! Las transacciones importadas aparecerĂĄn en este chat.`,
companyCard: 'tarjeta de empresa',
chooseCardFeed: 'Elige feed de tarjetas',
@@ -4609,6 +4608,7 @@ ${amount} para ${merchant} - ${date}`,
monthly: 'Mensual',
},
cardDetails: 'Datos de la tarjeta',
+ cardPending: ({name}: {name: string}) => `La tarjeta estĂĄ pendiente y se emitirĂĄ una vez que la cuenta de ${name} haya sido validada.`,
virtual: 'Virtual',
physical: 'FĂsica',
deactivate: 'Desactivar tarjeta',
@@ -4794,9 +4794,7 @@ ${amount} para ${merchant} - ${date}`,
noAccountsFound: 'No se han encontrado cuentas',
defaultCard: 'Tarjeta predeterminada',
downgradeTitle: 'No se puede degradar el espacio de trabajo',
- downgradeSubTitleFirstPart: `No es posible cambiar a una versiĂłn inferior de este espacio de trabajo porque hay varias fuentes de tarjetas conectadas (excluidas las tarjetas Expensify). Por favor`,
- downgradeSubTitleMiddlePart: 'mantenga solo una tarjeta',
- downgradeSubTitleLastPart: 'para continuar.',
+ downgradeSubTitle: `No es posible cambiar a una versiĂłn inferior de este espacio de trabajo porque hay varias fuentes de tarjetas conectadas (excluidas las tarjetas Expensify). Por favor mantenga solo una tarjeta para continuar.`,
noAccountsFoundDescription: ({connection}: ConnectionParams) => `AĂąade la cuenta en ${connection} y sincroniza la conexiĂłn de nuevo`,
expensifyCardBannerTitle: 'ObtĂŠn la Tarjeta Expensify',
expensifyCardBannerSubtitle:
@@ -4923,6 +4921,7 @@ ${amount} para ${merchant} - ${date}`,
existingReportFieldNameError: 'Ya existe un campo de informe con este nombre',
reportFieldNameRequiredError: 'Ingresa un nombre de campo de informe',
reportFieldTypeRequiredError: 'Elige un tipo de campo de informe',
+ circularReferenceError: 'Este campo no puede hacer referencia a sĂ mismo. Por favor, actualizar.',
reportFieldInitialValueRequiredError: 'Elige un valor inicial de campo de informe',
genericFailureMessage: 'Se ha producido un error al actualizar el campo de informe. Por favor, intĂŠntalo de nuevo.',
},
@@ -5483,8 +5482,8 @@ ${amount} para ${merchant} - ${date}`,
enableRate: 'Activar tasa',
status: 'Estado',
unit: 'Unidad',
- taxFeatureNotEnabledMessage: 'Los impuestos deben estar activados en el ĂĄrea de trabajo para poder utilizar esta funciĂłn. DirĂgete a ',
- changePromptMessage: ' para hacer ese cambio.',
+ taxFeatureNotEnabledMessage:
+ 'Los impuestos deben estar activados en el ĂĄrea de trabajo para poder utilizar esta funciĂłn. DirĂgete a MĂĄs funcionalidades para hacer ese cambio.',
deleteDistanceRate: 'Eliminar tasa de distancia',
areYouSureDelete: () => ({
one: 'ÂżEstĂĄs seguro de que quieres eliminar esta tasa?',
@@ -5704,6 +5703,12 @@ ${amount} para ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Las tasas de distancia estĂĄn disponibles en el plan Recopilar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`,
},
+ auditor: {
+ title: 'Auditor',
+ description: 'Los auditores tienen acceso de lectura a todos los informes para una visibilidad completa y la supervisiĂłn del cumplimiento.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Los auditores solo estĂĄn disponibles con el plan Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'MĂşltiples niveles de aprobaciĂłn',
description:
@@ -6196,7 +6201,7 @@ ${amount} para ${merchant} - ${date}`,
searchResults: {
emptyResults: {
title: 'No hay nada que ver aquĂ',
- subtitle: `Intenta ajustar tus criterios de bĂşsqueda o crear algo con el botĂłn verde ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ subtitle: `Intenta ajustar tus criterios de bĂşsqueda o crear algo con el botĂłn +.`,
},
emptyExpenseResults: {
title: 'AĂşn no has creado ningĂşn gasto',
@@ -7840,6 +7845,29 @@ ${amount} para ${merchant} - ${date}`,
subtitle: `No hemos podido cargar todos sus datos. Hemos sido notificados y estamos investigando el problema. Si esto persiste, por favor comunĂquese con`,
refreshAndTryAgain: 'Actualizar e intentar de nuevo',
},
+ domain: {
+ notVerified: 'No verificado',
+ retry: 'Reintentar',
+ verifyDomain: {
+ title: 'Verificar dominio',
+ beforeProceeding: ({domainName}: {domainName: string}) =>
+ `Antes de continuar, verifica que eres propietario de ${domainName} actualizando su configuraciĂłn DNS.`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `Accede a tu proveedor de DNS y abre la configuraciĂłn DNS de ${domainName}.`,
+ addTXTRecord: 'AĂąade el siguiente registro TXT:',
+ saveChanges: 'Guarda los cambios y vuelve aquĂ para verificar tu dominio.',
+ youMayNeedToConsult: `Es posible que necesites consultar con el servicio informĂĄtico de tu organizaciĂłn para completar la verificaciĂłn. MĂĄs informaciĂłn.`,
+ warning:
+ 'DespuĂŠs de la verificaciĂłn, todos los miembros de Expensify en tu dominio recibirĂĄn un correo electrĂłnico informando que sus cuentas serĂĄn gestionadas bajo tu dominio.',
+ codeFetchError: 'No se pudo obtener el cĂłdigo de verificaciĂłn',
+ genericError: 'No pudimos verificar tu dominio. Por favor, intĂŠntalo de nuevo y contacta con Concierge si el problema persiste.',
+ },
+ domainVerified: {
+ title: 'Dominio verificado',
+ header: 'ÂĄWooo! Tu dominio ha sido verificado',
+ description: ({domainName}: {domainName: string}) =>
+ `El dominio ${domainName} se ha verificado correctamente y ahora puedes configurar SAML y otras funciones de seguridad.`,
+ },
+ },
};
export default translations satisfies TranslationDeepObject;
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 715dc45ae6f42..207d79a65f2c4 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -442,6 +442,9 @@ const translations = {
zipPostCode: 'Code postal',
whatThis: "Qu'est-ce que c'est ?",
iAcceptThe: "J'accepte le",
+ acceptTermsAndPrivacy: `J'accepte le Conditions d'utilisation d'Expensify et Politique de confidentialitĂŠ`,
+ acceptTermsAndConditions: `J'accepte le termes et conditions`,
+ acceptTermsOfService: `J'accepte le Conditions d'utilisation d'Expensify`,
remove: 'Supprimer',
admin: 'Admin',
owner: 'PropriĂŠtaire',
@@ -931,17 +934,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`Ce salon de discussion est destinĂŠ Ă tout ce qui concerne ${reportName}.`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `Ce chat concerne les factures entre ${invoicePayer} et ${invoiceReceiver}. Utilisez le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} pour envoyer une facture.`,
+ `Ce chat concerne les factures entre ${invoicePayer} et ${invoiceReceiver}. Utilisez le bouton + pour envoyer une facture.`,
beginningOfChatHistory: 'Ce chat est avec',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `C'est ici que ${submitterDisplayName} soumettra ses dĂŠpenses Ă ${workspaceName}. Il suffit d'utiliser le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ `C'est ici que ${submitterDisplayName} soumettra ses dĂŠpenses Ă ${workspaceName}. Il suffit d'utiliser le bouton +.`,
beginningOfChatHistorySelfDM: "C'est votre espace personnel. Utilisez-le pour des notes, des tâches, des brouillons et des rappels.",
beginningOfChatHistorySystemDM: 'Bienvenue ! Commençons votre configuration.',
chatWithAccountManager: 'Discutez avec votre gestionnaire de compte ici',
sayHello: 'Dites bonjour !',
yourSpace: 'Votre espace',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Bienvenue dans ${roomName} !`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Utilisez le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} pour ${additionalText} une dĂŠpense.`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Utilisez le bouton + pour ${additionalText} une dĂŠpense.`,
askConcierge: 'Posez des questions et obtenez une assistance en temps rĂŠel 24h/24 et 7j/7.',
conciergeSupport: 'Support 24h/24 et 7j/7',
create: 'crĂŠer',
@@ -2254,10 +2257,9 @@ ${amount} pour ${merchant} - ${date}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `dans ${policyName}`,
- generatingPDF: 'GĂŠnĂŠration du PDF',
+ generatingPDF: 'GĂŠnĂŠration du PDF...',
waitForPDF: 'Veuillez patienter pendant que nous gĂŠnĂŠrons le PDF',
errorPDF: "Une erreur s'est produite lors de la tentative de gĂŠnĂŠration de votre PDF.",
- generatedPDF: 'Votre rapport PDF a ĂŠtĂŠ gĂŠnĂŠrĂŠ !',
},
reportDescriptionPage: {
roomDescription: 'Description de la chambre',
@@ -2462,7 +2464,7 @@ ${amount} pour ${merchant} - ${date}`,
title: 'Soumettre une dĂŠpense',
description:
'*Soumettez une dÊpense* en saisissant un montant ou en scannant un reçu.\n\n' +
- `1. Cliquez sur le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Cliquez sur le bouton +.\n` +
'2. Choisissez *CrĂŠer une dĂŠpense*.\n' +
'3. Saisissez un montant ou scannez un reçu.\n' +
`4. Ajoutez lâemail ou numĂŠro de tĂŠlĂŠphone de votre responsable.\n` +
@@ -2473,7 +2475,7 @@ ${amount} pour ${merchant} - ${date}`,
title: 'Soumettre une dĂŠpense',
description:
'*Soumettez une dÊpense* en saisissant un montant ou en scannant un reçu.\n\n' +
- `1. Cliquez sur le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Cliquez sur le bouton +.\n` +
'2. Choisissez *CrĂŠer une dĂŠpense*.\n' +
'3. Saisissez un montant ou scannez un reçu.\n' +
'4. Confirmez les dĂŠtails.\n' +
@@ -2484,7 +2486,7 @@ ${amount} pour ${merchant} - ${date}`,
title: 'Suivre une dĂŠpense',
description:
'*Suivez une dĂŠpense* dans nâimporte quelle devise, avec ou sans reçu.\n\n' +
- `1. Cliquez sur le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Cliquez sur le bouton +.\n` +
'2. Choisissez *CrĂŠer une dĂŠpense*.\n' +
'3. Saisissez un montant ou scannez un reçu.\n' +
'4. Choisissez votre espace *personnel*.\n' +
@@ -2566,7 +2568,7 @@ ${amount} pour ${merchant} - ${date}`,
title: 'DĂŠmarrer un chat',
description:
'*DĂŠmarrez un chat* avec quelquâun grâce Ă son email ou numĂŠro.\n\n' +
- `1. Cliquez sur le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Cliquez sur le bouton +.\n` +
'2. Choisissez *DĂŠmarrer un chat*.\n' +
'3. Entrez un email ou numĂŠro de tĂŠlĂŠphone.\n\n' +
'Sâils ne sont pas encore sur Expensify, une invitation sera envoyĂŠe automatiquement.\n\n' +
@@ -2576,7 +2578,7 @@ ${amount} pour ${merchant} - ${date}`,
title: 'Partager une dĂŠpense',
description:
'*Partagez une dĂŠpense* avec une ou plusieurs personnes.\n\n' +
- `1. Cliquez sur le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Cliquez sur le bouton +.\n` +
'2. Choisissez *DĂŠmarrer un chat*.\n' +
'3. Entrez des emails ou numĂŠros de tĂŠlĂŠphone.\n' +
'4. Cliquez sur le bouton gris *+* > *Partager une dĂŠpense*.\n' +
@@ -2596,7 +2598,7 @@ ${amount} pour ${merchant} - ${date}`,
title: 'CrĂŠer votre premier rapport',
description:
'Voici comment crĂŠer un rapport :\n\n' +
- `1. Cliquez sur le bouton ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Cliquez sur le bouton +.\n` +
'2. Choisissez *CrĂŠer un rapport*.\n' +
'3. Cliquez sur *Ajouter une dĂŠpense*.\n' +
'4. Ajoutez votre première dÊpense.\n\n' +
@@ -2613,10 +2615,8 @@ ${amount} pour ${merchant} - ${date}`,
messages: {
onboardingEmployerOrSubmitMessage: 'Se faire rembourser est aussi simple que dâenvoyer un message. Voici les bases.',
onboardingPersonalSpendMessage: 'Voici comment suivre vos dĂŠpenses en quelques clics.',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# Votre essai gratuit a commencĂŠ ! Configurons tout cela.\nđ Bonjour, je suis votre spĂŠcialiste de configuration Expensify. Maintenant que vous avez crĂŠĂŠ un espace de travail, profitez pleinement de votre essai gratuit de 30 jours en suivant les ĂŠtapes ci-dessous.'
- : '# Votre essai gratuit a commencĂŠ ! Configurons tout cela.\nđ Bonjour, je suis votre spĂŠcialiste de configuration Expensify. Jâai dĂŠjĂ crĂŠĂŠ un espace de travail pour vous aider Ă gĂŠrer les reçus et dĂŠpenses de votre ĂŠquipe. Pour profiter pleinement de votre essai gratuit de 30 jours, suivez simplement les ĂŠtapes de configuration ci-dessous.',
+ onboardingManageTeamMessage:
+ '# Votre essai gratuit a commencĂŠ ! Passons Ă la configuration.\nđ Bonjour, je suis votre spĂŠcialiste de configuration Expensify. Maintenant que vous avez crĂŠĂŠ un espace de travail, profitez pleinement de vos 30 jours dâessai gratuit en suivant les ĂŠtapes ci-dessous !',
onboardingTrackWorkspaceMessage:
'# Configurons votre espace\nđ Je suis lĂ pour vous aider ! Jâai personnalisĂŠ votre espace pour les entrepreneurs individuels et entreprises similaires. Vous pouvez le modifier via le lien ci-dessous.\n\nVoici comment suivre vos dĂŠpenses rapidement :',
onboardingChatSplitMessage: 'Partager des dĂŠpenses entre amis est aussi simple quâun message. Voici comment faire.',
@@ -4575,9 +4575,7 @@ ${amount} pour ${merchant} - ${date}`,
cardholder: 'Titulaire de carte',
card: 'Carte',
cardName: 'Nom de la carte',
- brokenConnectionErrorFirstPart: `La connexion du flux de carte est interrompue. S'il vous plaĂŽt`,
- brokenConnectionErrorLink: 'connectez-vous Ă votre banque',
- brokenConnectionErrorSecondPart: 'afin que nous puissions rĂŠtablir la connexion.',
+ brokenConnectionError: `La connexion du flux de carte est interrompue. S'il vous plaĂŽt connectez-vous Ă votre banque afin que nous puissions rĂŠtablir la connexion.`,
assignedCard: ({assignee, link}: AssignedCardParams) => `a attribuĂŠ ${link} Ă ${assignee} ! Les transactions importĂŠes apparaĂŽtront dans cette discussion.`,
companyCard: "carte d'entreprise",
chooseCardFeed: 'Choisir le flux de cartes',
@@ -4629,6 +4627,7 @@ ${amount} pour ${merchant} - ${date}`,
monthly: 'Mensuel',
},
cardDetails: 'DĂŠtails de la carte',
+ cardPending: ({name}: {name: string}) => `La carte est en attente et sera ĂŠmise une fois que le compte de ${name} aura ĂŠtĂŠ validĂŠ.`,
virtual: 'Virtuel',
physical: 'Physique',
deactivate: 'DĂŠsactiver la carte',
@@ -4818,9 +4817,7 @@ ${amount} pour ${merchant} - ${date}`,
noAccountsFound: 'Aucun compte trouvĂŠ',
defaultCard: 'Carte par dĂŠfaut',
downgradeTitle: `Impossible de rĂŠtrograder l'espace de travail`,
- downgradeSubTitleFirstPart: `Cet espace de travail ne peut pas ĂŞtre rĂŠtrogradĂŠ car plusieurs flux de cartes sont connectĂŠs (Ă l'exclusion des cartes Expensify). Veuillez`,
- downgradeSubTitleMiddlePart: `garder uniquement un flux de cartes`,
- downgradeSubTitleLastPart: 'pour continuer.',
+ downgradeSubTitle: `Cet espace de travail ne peut pas ĂŞtre rĂŠtrogradĂŠ car plusieurs flux de cartes sont connectĂŠs (Ă l'exclusion des cartes Expensify). Veuillez garder uniquement un flux de cartes pour continuer.`,
noAccountsFoundDescription: ({connection}: ConnectionParams) => `Veuillez ajouter le compte dans ${connection} et synchroniser Ă nouveau la connexion.`,
expensifyCardBannerTitle: 'Obtenez la carte Expensify',
expensifyCardBannerSubtitle:
@@ -4947,6 +4944,7 @@ ${amount} pour ${merchant} - ${date}`,
existingReportFieldNameError: 'Un champ de rapport avec ce nom existe dĂŠjĂ ',
reportFieldNameRequiredError: 'Veuillez entrer un nom de champ de rapport',
reportFieldTypeRequiredError: 'Veuillez choisir un type de champ de rapport',
+ circularReferenceError: 'Ce champ ne peut pas faire rĂŠfĂŠrence Ă lui-mĂŞme. Veuillez le mettre Ă jour.',
reportFieldInitialValueRequiredError: 'Veuillez choisir une valeur initiale pour le champ du rapport',
genericFailureMessage: "Une erreur s'est produite lors de la mise Ă jour du champ du rapport. Veuillez rĂŠessayer.",
},
@@ -5508,8 +5506,7 @@ ${amount} pour ${merchant} - ${date}`,
enableRate: 'Activer le tarif',
status: 'Statut',
unit: 'UnitĂŠ',
- taxFeatureNotEnabledMessage: "Les taxes doivent ĂŞtre activĂŠes sur l'espace de travail pour utiliser cette fonctionnalitĂŠ. Rendez-vous sur",
- changePromptMessage: 'pour effectuer ce changement.',
+ taxFeatureNotEnabledMessage: `Les taxes doivent ĂŞtre activĂŠes sur l'espace de travail pour utiliser cette fonctionnalitĂŠ. Rendez-vous sur Plus de fonctionnalitĂŠs pour effectuer ce changement.`,
deleteDistanceRate: 'Supprimer le tarif de distance',
areYouSureDelete: () => ({
one: 'Ătes-vous sĂťr de vouloir supprimer ce tarif ?',
@@ -5710,6 +5707,12 @@ ${amount} pour ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Les tarifs de distance sont disponibles sur le plan Collect, Ă partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre par mois.` : `par membre actif par mois.`}`,
},
+ auditor: {
+ title: 'Auditeur',
+ description: 'Les auditeurs ont un accès en lecture seule à tous les rapports pour une visibilitÊ totale et une surveillance de la conformitÊ.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Les auditeurs sont disponibles uniquement avec le plan Control, Ă partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre par mois.` : `par membre actif par mois.`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: "Niveaux d'approbation multiples",
description:
@@ -6216,7 +6219,7 @@ ${amount} pour ${merchant} - ${date}`,
searchResults: {
emptyResults: {
title: 'Rien Ă afficher',
- subtitle: `Essayez d'ajuster vos critères de recherche ou de crÊer quelque chose avec le bouton vert ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ subtitle: `Essayez d'ajuster vos critères de recherche ou de crÊer quelque chose avec le bouton +.`,
},
emptyExpenseResults: {
title: "Vous n'avez pas encore crĂŠĂŠ de dĂŠpenses.",
@@ -7405,6 +7408,28 @@ ${amount} pour ${merchant} - ${date}`,
subtitle: `Nous n'avons pas pu charger toutes vos donnÊes. Nous avons ÊtÊ informÊs et examinons le problème. Si cela persiste, veuillez contacter`,
refreshAndTryAgain: 'Actualisez puis rĂŠessayez',
},
+ domain: {
+ notVerified: 'Non vĂŠrifiĂŠ',
+ retry: 'RĂŠessayer',
+ verifyDomain: {
+ title: 'VĂŠrifier le domaine',
+ beforeProceeding: ({domainName}: {domainName: string}) =>
+ `Avant de poursuivre, vÊrifiez que vous êtes propriÊtaire de ${domainName} en mettant à jour ses paramètres DNS.`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `AccÊdez à votre fournisseur DNS et ouvrez les paramètres DNS pour ${domainName}.`,
+ addTXTRecord: 'Ajoutez lâenregistrement TXT suivant :',
+ saveChanges: 'Enregistrez les modifications et revenez ici pour vĂŠrifier votre domaine.',
+ youMayNeedToConsult: `Il se peut que vous deviez consulter le service informatique de votre organisation pour terminer la vĂŠrification. En savoir plus.`,
+ warning: 'Après vÊrification, tous les membres Expensify de votre domaine recevront un e-mail indiquant que leur compte sera gÊrÊ au sein de votre domaine.',
+ codeFetchError: 'Impossible de rĂŠcupĂŠrer le code de vĂŠrification',
+ genericError: "Nous n'avons pas pu vÊrifier votre domaine. Veuillez rÊessayer et contacter Concierge si le problème persiste.",
+ },
+ domainVerified: {
+ title: 'Domaine vĂŠrifiĂŠ',
+ header: 'Wouhou ! Votre domaine a ĂŠtĂŠ vĂŠrifiĂŠ',
+ description: ({domainName}: {domainName: string}) =>
+ `Le domaine ${domainName} a ÊtÊ vÊrifiÊ avec succès et vous pouvez maintenant configurer SAML et d'autres fonctionnalitÊs de sÊcuritÊ.`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
// so if you change it here, please update it there as well.
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 7b75125a636e8..b70b72b8d65c0 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -442,6 +442,9 @@ const translations = {
zipPostCode: 'CAP / Codice postale',
whatThis: "Cos'è questo?",
iAcceptThe: 'Accetto il',
+ acceptTermsAndPrivacy: `Accetto il Termini di servizio di Expensify e Informativa sulla privacy`,
+ acceptTermsAndConditions: `Accetto il termini e condizioni`,
+ acceptTermsOfService: `Accetto il Termini di servizio di Expensify`,
remove: 'Rimuovi',
admin: 'Admin',
owner: 'Proprietario',
@@ -928,17 +931,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`Questa chat è per tutto ciò che riguarda ${reportName}.`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `Questa chat è per le fatture tra ${invoicePayer} e ${invoiceReceiver}. Utilizzare il pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} per inviare una fattura.`,
+ `Questa chat è per le fatture tra ${invoicePayer} e ${invoiceReceiver}. Utilizzare il pulsante + per inviare una fattura.`,
beginningOfChatHistory: 'Questa chat è con',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `Ă qui che ${submitterDisplayName} presenterĂ le spese a ${workspaceName}. Basta usare il pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ `Ă qui che ${submitterDisplayName} presenterĂ le spese a ${workspaceName}. Basta usare il pulsante +.`,
beginningOfChatHistorySelfDM: 'Questo è il tuo spazio personale. Usalo per appunti, compiti, bozze e promemoria.',
beginningOfChatHistorySystemDM: 'Benvenuto! Iniziamo con la configurazione.',
chatWithAccountManager: 'Chatta con il tuo account manager qui',
sayHello: 'Ciao!',
yourSpace: 'Il tuo spazio',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Benvenuto in ${roomName}!`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Usa il pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} per ${additionalText} una spesa.`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Usa il pulsante + per ${additionalText} una spesa.`,
askConcierge: 'Fai domande e ricevi supporto in tempo reale 24/7.',
conciergeSupport: 'Supporto 24/7',
create: 'creare',
@@ -2245,10 +2248,9 @@ ${amount} per ${merchant} - ${date}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `in ${policyName}`,
- generatingPDF: 'Generazione PDF',
+ generatingPDF: 'Generazione PDF...',
waitForPDF: 'Attendere mentre generiamo il PDF',
errorPDF: 'Si è verificato un errore durante il tentativo di generare il tuo PDF.',
- generatedPDF: 'Il tuo PDF del rapporto è stato generato!',
},
reportDescriptionPage: {
roomDescription: 'Descrizione della stanza',
@@ -2455,7 +2457,7 @@ ${amount} per ${merchant} - ${date}`,
description:
'*Invia una spesa* inserendo un importo o scansionando una ricevuta.\n' +
'\n' +
- `1. Clicca sul pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clicca sul pulsante +.\n` +
'2. Scegli *Crea spesa*.\n' +
'3. Inserisci un importo o scansiona una ricevuta.\n' +
`4. Aggiungi lâemail o il numero di telefono del tuo responsabile.\n` +
@@ -2468,7 +2470,7 @@ ${amount} per ${merchant} - ${date}`,
description:
'*Invia una spesa* inserendo un importo o scansionando una ricevuta.\n' +
'\n' +
- `1. Clicca sul pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clicca sul pulsante +.\n` +
'2. Scegli *Crea spesa*.\n' +
'3. Inserisci un importo o scansiona una ricevuta.\n' +
'4. Conferma i dettagli.\n' +
@@ -2481,7 +2483,7 @@ ${amount} per ${merchant} - ${date}`,
description:
'*Monitora una spesa* in qualsiasi valuta, con o senza ricevuta.\n' +
'\n' +
- `1. Clicca sul pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clicca sul pulsante +.\n` +
'2. Scegli *Crea spesa*.\n' +
'3. Inserisci un importo o scansiona una ricevuta.\n' +
'4. Scegli il tuo spazio *personale*.\n' +
@@ -2575,7 +2577,7 @@ ${amount} per ${merchant} - ${date}`,
description:
'*Avvia una chat* con chiunque utilizzando la loro email o numero di telefono.\n' +
'\n' +
- `1. Clicca sul pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clicca sul pulsante +.\n` +
'2. Scegli *Avvia chat*.\n' +
'3. Inserisci unâemail o numero di telefono.\n' +
'\n' +
@@ -2588,7 +2590,7 @@ ${amount} per ${merchant} - ${date}`,
description:
'*Dividi le spese* con una o piĂš persone.\n' +
'\n' +
- `1. Clicca sul pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clicca sul pulsante +.\n` +
'2. Scegli *Avvia chat*.\n' +
'3. Inserisci email o numeri di telefono.\n' +
'4. Clicca sul pulsante grigio *+* nella chat > *Dividi spesa*.\n' +
@@ -2610,7 +2612,7 @@ ${amount} per ${merchant} - ${date}`,
description:
'Ecco come creare un report:\n' +
'\n' +
- `1. Clicca sul pulsante ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clicca sul pulsante +.\n` +
'2. Scegli *Crea report*.\n' +
'3. Clicca su *Aggiungi spesa*.\n' +
'4. Aggiungi la tua prima spesa.\n' +
@@ -2628,10 +2630,8 @@ ${amount} per ${merchant} - ${date}`,
messages: {
onboardingEmployerOrSubmitMessage: 'Ricevere un rimborso è facile come inviare un messaggio. Vediamo le basi.',
onboardingPersonalSpendMessage: 'Ecco come monitorare le tue spese in pochi clic.',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# La tua prova gratuita è iniziata! Configuriamo tutto.\nđ Ciao, sono il tuo specialista di configurazione Expensify. Ora che hai creato uno spazio di lavoro, sfrutta al massimo la tua prova gratuita di 30 giorni seguendo i passaggi qui sotto!'
- : '# La tua prova gratuita è iniziata! Configuriamo tutto.\nđ Ciao, sono il tuo specialista di configurazione Expensify. Ho giĂ creato uno spazio di lavoro per aiutarti a gestire le ricevute e le spese del tuo team. Per sfruttare al massimo la tua prova gratuita di 30 giorni, segui semplicemente i restanti passaggi di configurazione qui sotto!',
+ onboardingManageTeamMessage:
+ '# La tua prova gratuita è iniziata! Procediamo con la configurazione.\nđ Ciao, sono il tuo specialista di configurazione Expensify. Ora che hai creato uno spazio di lavoro, sfrutta al massimo i tuoi 30 giorni di prova gratuita seguendo i passaggi indicati di seguito!',
onboardingTrackWorkspaceMessage:
'# Iniziamo\nđ Sono qui per aiutarti! Per iniziare, ho personalizzato le impostazioni dello spazio di lavoro per ditte individuali e aziende simili. Puoi modificarle cliccando il link qui sotto!\n\nEcco come monitorare le tue spese in pochi clic:',
onboardingChatSplitMessage: 'Dividere le spese con gli amici è facile come inviare un messaggio. Ecco come.',
@@ -4582,9 +4582,8 @@ ${amount} per ${merchant} - ${date}`,
cardholder: 'Titolare della carta',
card: 'Carta',
cardName: 'Nome della carta',
- brokenConnectionErrorFirstPart: `La connessione del feed della carta è interrotta. Per favore`,
- brokenConnectionErrorLink: 'accedi al tuo conto bancario',
- brokenConnectionErrorSecondPart: 'cosĂŹ possiamo ristabilire la connessione.',
+ brokenConnectionError:
+ 'La connessione del feed della carta è interrotta. Per favore accedi al tuo conto bancario cosÏ possiamo ristabilire la connessione.',
assignedCard: ({assignee, link}: AssignedCardParams) => `assegnato ${assignee} un ${link}! Le transazioni importate appariranno in questa chat.`,
companyCard: 'carta aziendale',
chooseCardFeed: 'Scegli il feed della carta',
@@ -4635,6 +4634,7 @@ ${amount} per ${merchant} - ${date}`,
monthly: 'Mensile',
},
cardDetails: 'Dettagli della carta',
+ cardPending: ({name}: {name: string}) => `La carta è attualmente in sospeso e verrà emessa una volta che l'account di ${name} sarà convalidato.`,
virtual: 'Virtuale',
physical: 'Fisico',
deactivate: 'Disattiva carta',
@@ -4821,9 +4821,8 @@ ${amount} per ${merchant} - ${date}`,
noAccountsFound: 'Nessun account trovato',
defaultCard: 'Carta predefinita',
downgradeTitle: `Impossibile effettuare il downgrade dello spazio di lavoro`,
- downgradeSubTitleFirstPart: `Questo workspace non può essere declassato perchÊ sono collegati piÚ flussi di carte (escludendo le carte Expensify). Per favore`,
- downgradeSubTitleMiddlePart: `mantieni solo un feed di carte`,
- downgradeSubTitleLastPart: 'per procedere.',
+ downgradeSubTitle: `Questo workspace non può essere declassato perchÊ sono collegati piÚ flussi di carte (escludendo le carte Expensify). Per favore mantieni solo un feed di carte per procedere.`,
+
noAccountsFoundDescription: ({connection}: ConnectionParams) => `Per favore, aggiungi l'account in ${connection} e sincronizza nuovamente la connessione.`,
expensifyCardBannerTitle: 'Ottieni la Expensify Card',
expensifyCardBannerSubtitle:
@@ -4950,6 +4949,7 @@ ${amount} per ${merchant} - ${date}`,
existingReportFieldNameError: 'Un campo del report con questo nome esiste giĂ ',
reportFieldNameRequiredError: 'Inserisci un nome per il campo del report',
reportFieldTypeRequiredError: 'Si prega di scegliere un tipo di campo del report',
+ circularReferenceError: 'Questo campo non può fare riferimento a se stesso. Aggiorna.',
reportFieldInitialValueRequiredError: 'Si prega di scegliere un valore iniziale per il campo del report',
genericFailureMessage: "Si è verificato un errore durante l'aggiornamento del campo del report. Per favore riprova.",
},
@@ -5510,7 +5510,8 @@ ${amount} per ${merchant} - ${date}`,
enableRate: 'Abilita tariffa',
status: 'Stato',
unit: 'Unit',
- taxFeatureNotEnabledMessage: 'Le tasse devono essere abilitate nello spazio di lavoro per utilizzare questa funzione. Vai su',
+ taxFeatureNotEnabledMessage:
+ 'Le tasse devono essere abilitate nello spazio di lavoro per utilizzare questa funzione. Vai su PiĂš funzionalitĂ per apportare quella modifica. ',
changePromptMessage: 'per apportare quella modifica.',
deleteDistanceRate: 'Elimina tariffa distanza',
areYouSureDelete: () => ({
@@ -5712,6 +5713,12 @@ ${amount} per ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Le tariffe a distanza sono disponibili con il piano Collect, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`,
},
+ auditor: {
+ title: 'Revisore',
+ description: 'I revisori hanno accesso in sola lettura a tutti i report per piena visibilitĂ e monitoraggio della conformitĂ .',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `I revisori sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Livelli di approvazione multipli',
description:
@@ -6222,7 +6229,7 @@ ${amount} per ${merchant} - ${date}`,
searchResults: {
emptyResults: {
title: 'Niente da mostrare',
- subtitle: `Prova a modificare i criteri di ricerca o a creare qualcosa con il pulsante verde ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ subtitle: `Prova a modificare i criteri di ricerca o a creare qualcosa con il pulsante +.`,
},
emptyExpenseResults: {
title: 'Non hai ancora creato nessuna spesa.',
@@ -7410,6 +7417,28 @@ ${amount} per ${merchant} - ${date}`,
subtitle: `Non siamo riusciti a caricare tutti i tuoi dati. Siamo stati avvisati e stiamo esaminando il problema. Se il problema persiste, contatta`,
refreshAndTryAgain: 'Aggiorna e riprova',
},
+ domain: {
+ notVerified: 'Non verificato',
+ retry: 'Riprova',
+ verifyDomain: {
+ title: 'Verifica dominio',
+ beforeProceeding: ({domainName}: {domainName: string}) =>
+ `Prima di procedere, verifica di essere il proprietario di ${domainName} aggiornando le impostazioni DNS.`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `Accedi al tuo provider DNS e apri le impostazioni DNS per ${domainName}.`,
+ addTXTRecord: 'Aggiungi il seguente record TXT:',
+ saveChanges: 'Salva le modifiche e torna qui per verificare il tuo dominio.',
+ youMayNeedToConsult: `Potresti dover contattare il reparto IT della tua organizzazione per completare la verifica. Scopri di piĂš.`,
+ warning: "Dopo la verifica, tutti i membri di Expensify del tuo dominio riceveranno un'email che li informa che il loro account sarĂ gestito all'interno del tuo dominio.",
+ codeFetchError: 'Impossibile recuperare il codice di verifica',
+ genericError: 'Non siamo riusciti a verificare il tuo dominio. Riprova e contatta Concierge se il problema persiste.',
+ },
+ domainVerified: {
+ title: 'Dominio verificato',
+ header: 'Wooo! Il tuo dominio è stato verificato',
+ description: ({domainName}: {domainName: string}) =>
+ `Il dominio ${domainName} è stato verificato con successo e ora puoi configurare SAML e altre funzionalità di sicurezza.`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
// so if you change it here, please update it there as well.
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index cc792be36cc36..5bed328f8f975 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -442,6 +442,9 @@ const translations = {
zipPostCode: 'éľäžżçŞĺˇ',
whatThis: 'ăăăŻä˝ă§ăăďź',
iAcceptThe: 'ćżčŤžăăžă',
+ acceptTermsAndPrivacy: `ćżčŤžăăžă Expensify ĺŠç¨čŚç´ ăăăł ăăŠă¤ăăˇăźăăŞăˇăź`,
+ acceptTermsAndConditions: `ćżčŤžăăžă ĺŠç¨čŚç´`,
+ acceptTermsOfService: `ćżčŤžăăžă Expensify ĺŠç¨čŚç´`,
remove: 'ĺé¤',
admin: '玥çč ',
owner: 'ăŞăźăăź',
@@ -929,17 +932,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`ăăŽăăŁăăăŤăźă ăŻă${reportName}ăŤé˘ăăăă¨ăŞăä˝ă§ăăŠăăă`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `ăăŽăăŁăăăŻă${invoicePayer}ă¨${invoiceReceiver}éăŽčŤćąć¸ç¨ă§ăăčŤćąć¸ăé俥ăăăŤăŻă${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} ăăżăłă使ç¨ăăŚăă ăăă`,
+ `ăăŽăăŁăăăŻă${invoicePayer}ă¨${invoiceReceiver}éăŽčŤćąć¸ç¨ă§ăăčŤćąć¸ăé俥ăăăŤăŻă+ ăăżăłă使ç¨ăăŚăă ăăă`,
beginningOfChatHistory: 'ăăŽăăŁăăăŻ',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `ăăă§${submitterDisplayName}ă${workspaceName}ăŤçľč˛ťăćĺşăăžăă${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}ăăżăłăăŻăŞăăŻăăŚăă ăăă`,
+ `ăăă§${submitterDisplayName}ă${workspaceName}ăŤçľč˛ťăćĺşăăžăă+ăăżăłăăŻăŞăăŻăăŚăă ăăă`,
beginningOfChatHistorySelfDM: 'ăăăŻăăŞăăŽĺäşşăšăăźăšă§ăăăĄă˘ăăżăšăŻăä¸ć¸ăăăŞăă¤ăłăăźăŤä˝żç¨ăăŚăă ăăă',
beginningOfChatHistorySystemDM: 'ăăăăďźăťăăă˘ăăăĺ§ăăžăăăă',
chatWithAccountManager: 'ăăĄăă§ă˘ăŤăŚăłăăăăźă¸ăŁăźă¨ăăŁăăăăŚăă ăă',
sayHello: 'ăăăŤăĄăŻďź',
yourSpace: 'ăăŞăăŽăšăăźăš',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `${roomName}ă¸ăăăăďź`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} ăăżăłă使ç¨ăăŚçľč˛ťă${additionalText}ăăžăă`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` + ăăżăłă使ç¨ăăŚçľč˛ťă${additionalText}ăăžăă`,
askConcierge: '質ĺăăăŚă24ćé365ćĽăŞă˘ăŤăżă¤ă ăľăăźăăĺăăžăăăă',
conciergeSupport: '24ćéĺš´ä¸çĄäźăľăăźă',
create: 'ä˝ćăă',
@@ -2236,10 +2239,9 @@ ${date} - ${merchant}ăŤ${amount}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `${policyName} ĺ `,
- generatingPDF: 'PDFăçćä¸',
+ generatingPDF: 'PDFăçćä¸...',
waitForPDF: 'PDFăçćăăăžă§ăĺž ăĄăă ăă',
errorPDF: 'PDFăŽçćä¸ăŤă¨ăŠăźăçşçăăžăăă',
- generatedPDF: 'ăăŞăăŽăŹăăźăPDFăçćăăăžăăďź',
},
reportDescriptionPage: {
roomDescription: 'é¨ĺąăŽčŞŹć',
@@ -2618,10 +2620,8 @@ ${date} - ${merchant}ăŤ${amount}`,
messages: {
onboardingEmployerOrSubmitMessage: 'ćŻćăăĺăĺăăŽăŻăăĄăăťăźă¸ăéăăŽă¨ĺăăăăç°Ąĺă§ăăĺşćŹă確čŞăăžăăăă',
onboardingPersonalSpendMessage: 'ć°ĺăŻăŞăăŻăăă ăă§ăăŞăăŽćŻĺşă追补ăăćšćłăŻćŹĄăŽă¨ăăă§ăă',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# çĄćăăŠă¤ă˘ăŤăéĺ§ăăžăăďźăăăăťăăă˘ăăăĺ§ăăžăăăă\nđ ăăăŤăĄăŻăExpensify ăťăăă˘ăăăšăăˇăŁăŞăšăăŽç§ă§ăăăŻăźăŻăšăăźăšăä˝ćăăăŽă§ă30ćĽéăŽçĄćăăŠă¤ă˘ăŤăć大éĺŠç¨ăăä¸č¨ăŽćé ăŤĺžăŁăŚăă ăăă'
- : '# çĄćăăŠă¤ă˘ăŤăéĺ§ăăžăăďźăăăăťăăă˘ăăăĺ§ăăžăăăă\nđ ăăăŤăĄăŻăExpensify ăťăăă˘ăăăšăăˇăŁăŞăšăăŽç§ă§ăăăăźă ăŽé ĺć¸ăçľč˛ťă玥çăăăăăŤăăă§ăŤăŻăźăŻăšăăźăšăä˝ćăăžăăă30ćĽéăŽçĄćăăŠă¤ă˘ăŤăć大éĺŠç¨ăăăăăŤăä¸č¨ăŽćŽăăŽćé ăŤĺžăŁăŚăă ăăă',
+ onboardingManageTeamMessage:
+ '# çĄćăăŠă¤ă˘ăŤăéĺ§ăăžăăďźăăăăťăăă˘ăăăĺ§ăăžăăăă\nđ ăăăŤăĄăŻăExpensify ăťăăă˘ăăăšăăˇăŁăŞăšăăŽç§ă§ăăăŻăźăŻăšăăźăšăä˝ćăăăŽă§ă30ćĽéăŽçĄćăăŠă¤ă˘ăŤăć大éĺŠç¨ăăä¸č¨ăŽćé ăŤĺžăŁăŚăă ăăă',
onboardingTrackWorkspaceMessage:
'# ăťăăă˘ăăăăžăăă\năŁăŚăăćäźăăăžăďźéĺ§ăŤăăăŁăŚăăăŞăăŽăŻăźăŻăšăăźăšč¨ĺŽăĺäşşäşćĽä¸ťăéĄäźźăŽäźćĽăŤĺăăăŚčŞżć´ăăžăăă䝼ä¸ăŽăŞăłăŻăăŻăŞăăŻăăă¨ăăŻăźăŻăšăăźăšă調ć´ă§ăăžăďź\n\nć°ĺăŻăŞăăŻăăă ăă§ăăŞăăŽćŻĺşă追补ăăćšćłăŻćŹĄăŽă¨ăăă§ăă',
onboardingChatSplitMessage: 'ĺéă¨ăŽčŤćąć¸ăŽĺĺ˛ăŻăăĄăăťăźă¸ăéăăŽă¨ĺăăăăç°Ąĺă§ăăćšćłăŻćŹĄăŽă¨ăăă§ăă',
@@ -4544,9 +4544,7 @@ ${date} - ${merchant}ăŤ${amount}`,
cardholder: 'ăŤăźăăăŤăăź',
card: 'ăŤăźă',
cardName: 'ăŤăźăĺ',
- brokenConnectionErrorFirstPart: `ăŤăźăăăŁăźăăŽćĽçśăĺăăŚăăžăăăŠăă`,
- brokenConnectionErrorLink: 'éčĄăŤăă°ă¤ăłăă',
- brokenConnectionErrorSecondPart: 'ăăă§ăĺăłćĽçśă確çŤă§ăăžăă',
+ brokenConnectionError: 'ăŤăźăăăŁăźăăŽćĽçśăĺăăŚăăžăăăŠăă éčĄăŤăă°ă¤ăł ăăă¨ăĺăłćĽçśă確çŤă§ăăžăă',
assignedCard: ({assignee, link}: AssignedCardParams) => `${assignee}ăŤ${link}ăĺ˛ăĺ˝ăŚăžăăďźă¤ăłăăźăăăăĺĺźăŻăăŽăăŁăăăŤčĄ¨ç¤şăăăžăă`,
companyCard: 'äźç¤žăŤăźă',
chooseCardFeed: 'ăŤăźăăăŁăźăăé¸ć',
@@ -4597,6 +4595,7 @@ ${date} - ${merchant}ăŤ${amount}`,
monthly: 'ćŻć',
},
cardDetails: 'ăŤăźăăŽčŠłç´°',
+ cardPending: ({name}: {name: string}) => `${name}ăŽă˘ăŤăŚăłăă確čŞăă揥珏ăăŤăźăăçşčĄăăăžăăçžĺ¨ăŻäżçä¸ă§ăă`,
virtual: 'ăăźăăŁăŤ',
physical: 'çŠçç',
deactivate: 'ăŤăźăăçĄĺšĺăă',
@@ -4780,9 +4779,8 @@ ${date} - ${merchant}ăŤ${amount}`,
noAccountsFound: 'ă˘ăŤăŚăłăăčŚă¤ăăăžăă',
defaultCard: 'ăăăŠăŤăăŤăźă',
downgradeTitle: `ăŻăźăŻăšăăźăšăăăŚăłă°ăŹăźăă§ăăžăă`,
- downgradeSubTitleFirstPart: `ăăŽăŻăźăŻăšăăźăšăŻăč¤ć°ăŽăŤăźăăăŁăźăăćĽçśăăăŚăăăăďźExpensifyăŤăźăăé¤ăďźăăăŚăłă°ăŹăźăă§ăăžăăăăŠăă`,
- downgradeSubTitleMiddlePart: `ăŤăźăăăŁăźăă1ă¤ă ăäżć`,
- downgradeSubTitleLastPart: 'çśčĄăăă',
+ downgradeSubTitle: `ăăŽăŻăźăŻăšăăźăšăŻăč¤ć°ăŽăŤăźăăăŁăźăăćĽçśăăăŚăăăăďźExpensifyăŤăźăăé¤ăďźăăăŚăłă°ăŹăźăă§ăăžăăăăŠăă ăŤăźăăăŁăźăă1ă¤ă ăäżć çśčĄăăă`,
+
noAccountsFoundDescription: ({connection}: ConnectionParams) => `${connection}ăŤă˘ăŤăŚăłăăčż˝ĺ ăăĺ庌ćĽçśăĺćăăŚăă ăăă`,
expensifyCardBannerTitle: 'ExpensifyăŤăźăăĺĺžăă',
expensifyCardBannerSubtitle: 'ăăšăŚăŽçąłĺ˝ă§ăŽčłźĺ Ľă§ăăŁăăˇăĽăăăŻă漽ăăżăExpensifyăŽčŤćąć¸ăć大50%ăŞăăçĄĺśéăŽăăźăăŁăŤăŤăźăăŞăŠăăăăŤĺ¤ăăŽçšĺ ¸ăăăăžăă',
@@ -4878,7 +4876,7 @@ ${date} - ${merchant}ăŤ${amount}`,
textType: 'ăăăšă',
dateType: 'ćĽäť',
dropdownType: 'ăŞăšă',
- formulaType: 'Formula',
+ formulaType: 'ć°ĺź',
textAlternateText: 'čŞçąĺ ĽĺăăŁăźăŤăăčż˝ĺ ăăŚăă ăăă',
dateAlternateText: 'ćĽäťé¸ćç¨ăŽăŤăŹăłăăźăčż˝ĺ ăăžăă',
dropdownAlternateText: 'é¸ćč˘ăŽăŞăšăăčż˝ĺ ăăŚăă ăăă',
@@ -4908,6 +4906,7 @@ ${date} - ${merchant}ăŤ${amount}`,
existingReportFieldNameError: 'ăăŽĺĺăŽăŹăăźăăăŁăźăŤăăŻć˘ăŤĺĺ¨ăăžă',
reportFieldNameRequiredError: 'ăŹăăźăăăŁăźăŤăĺăĺ ĽĺăăŚăă ăă',
reportFieldTypeRequiredError: 'ăŹăăźăăăŁăźăŤăăŽăżă¤ăăé¸ćăăŚăă ăă',
+ circularReferenceError: 'ăăŽăăŁăźăŤăăŻčŞčşŤăĺç §ă§ăăžăăăć´ć°ăăŚăă ăăă',
reportFieldInitialValueRequiredError: 'ăŹăăźăăăŁăźăŤăăŽĺćĺ¤ăé¸ćăăŚăă ăă',
genericFailureMessage: 'ăŹăăźăăăŁăźăŤăăŽć´ć°ä¸ăŤă¨ăŠăźăçşçăăžăăăăăä¸ĺşŚă芌ăăă ăăă',
},
@@ -5463,8 +5462,8 @@ ${date} - ${merchant}ăŤ${amount}`,
enableRate: 'ăŹăźăăćĺšăŤăă',
status: 'ăšăăźăżăš',
unit: 'ĺä˝',
- taxFeatureNotEnabledMessage: 'ăăŽćŠč˝ă使ç¨ăăăŤăŻăăŻăźăŻăšăăźăšă§ç¨éăćĺšăŤăăĺż čŚăăăăžăăăăĄăăŤç§ťĺăăŚ',
- changePromptMessage: 'ăăŽĺ¤ć´ăčĄăăăăŤă',
+ taxFeatureNotEnabledMessage:
+ 'ăăŽćŠč˝ă使ç¨ăăăŤăŻăăŻăźăŻăšăăźăšă§ç¨éăćĺšăŤăăĺż čŚăăăăžăăăăĄăăŤç§ťĺă㌠ăăăŤĺ¤ăăŽćŠč˝ ăŤčĄăŁăŚăăŽĺ¤ć´ăčĄăŁăŚăă ăăă',
deleteDistanceRate: 'čˇé˘ćéăĺé¤',
areYouSureDelete: () => ({
one: 'ăăŽăŹăźăăĺé¤ăăŚăăăăăă§ăăďź',
@@ -5661,6 +5660,12 @@ ${date} - ${merchant}ăŤ${amount}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`čˇé˘ćéăŻăCollectăăŠăłă§ĺŠç¨ĺŻč˝ă§ăćé㯠${formattedPrice} ${hasTeam2025Pricing ? `ăĄăłăăźăă¨ăŤćéĄă` : `ă˘ăŻăăŁăăĄăłăăź1äşşăăăćéĄă`}`,
},
+ auditor: {
+ title: 'çŁćťäşş',
+ description: 'çŁćťäşşăŻăăšăŚăŽăŹăăźăăŤĺŻžăăŚčŞăżĺăĺ°ç¨ă˘ăŻăťăšăĺŻč˝ă§ăĺŽĺ ¨ăŞĺŻčŚć§ă¨ăłăłăăŠă¤ă˘ăłăšçŁčŚăćäžăăžăă',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `çŁćťäşşăŻ Control ăăŠăłă§ăŽăżĺŠç¨ĺŻč˝ă§ăćé㯠${formattedPrice} ${hasTeam2025Pricing ? `ăĄăłăăźăă¨ăŤćéĄă§ăă` : `ă˘ăŻăăŁăăĄăłăăź1äşşăăăćéĄă§ăă`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'č¤ć°ăŽćżčŞăŹăăŤ',
description: 'č¤ć°ăŽćżčŞăŹăăŤăŻăćăćťăăčĄăăăĺăŤč¤ć°ăŽäşşăăŹăăźăăćżčŞăăĺż čŚăăăäźćĽĺăăŽăŻăźăŻăăăźăăźăŤă§ăă',
@@ -6158,7 +6163,7 @@ ${date} - ${merchant}ăŤ${amount}`,
searchResults: {
emptyResults: {
title: '襨示ăăăăŽăăăăžăă',
- subtitle: `ć¤ç´˘ćĄäťśă調ć´ăăăăçˇč˛ăŽ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}ăăżăłă§ä˝ăăä˝ćăăŚăżăŚăă ăăă`,
+ subtitle: `ć¤ç´˘ćĄäťśă調ć´ăăăă+ăăżăłă§ä˝ăăä˝ćăăŚăżăŚăă ăăă`,
},
emptyExpenseResults: {
title: 'ăžă çľč˛ťăä˝ćăăăŚăăžăăă',
@@ -7336,6 +7341,27 @@ ${date} - ${merchant}ăŤ${amount}`,
subtitle: `ăăšăŚăŽăăźăżăčŞăżčžźăăă¨ăă§ăăžăăă§ăăăéçĽăĺăăŚăăăĺéĄă調ćťăăŚăăžăăăăŽçść ăçśăĺ ´ĺăŻăăĺăĺăăăă ăăă`,
refreshAndTryAgain: 'ĺčŞăżčžźăżăăŚăăăä¸ĺşŚă芌ăăă ăă',
},
+ domain: {
+ notVerified: 'ćŞç˘şčŞ',
+ retry: 'ĺ芌čĄ',
+ verifyDomain: {
+ title: 'ăăĄă¤ăłă確čŞ',
+ beforeProceeding: ({domainName}: {domainName: string}) => `çśčĄăăĺăŤăDNSč¨ĺŽăć´ć°ăăŚ${domainName}ăŽććč ă§ăăăă¨ă確čŞăăŚăă ăăă`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `DNSăăăă¤ăăźăŤă˘ăŻăťăšăă${domainName} ăŽDNSč¨ĺŽăéăăŚăă ăăă`,
+ addTXTRecord: '揥ăŽTXTăŹăłăźăăčż˝ĺ ăăŚăă ăă:',
+ saveChanges: 'ĺ¤ć´ăäżĺăăŚăăăăŤćťăăăăĄă¤ăłă確čŞăăŚăă ăăă',
+ youMayNeedToConsult: `ć¤č¨źăĺŽäşăăăŤăŻăçľçšăŽITé¨éăŤç¸čŤăăĺż čŚăăăĺ ´ĺăăăăžăă芳細ăŻăăĄăă`,
+ warning: '確čŞăĺŽäşăăă¨ă貴礞ăŽăăĄă¤ăłăŽăăšăŚăŽExpensifyăĄăłăăźăŤăă˘ăŤăŚăłăă貴礞ăŽăăĄă¤ăłă§çŽĄçăăăć¨ăŽăĄăźăŤăé俥ăăăžăă',
+ codeFetchError: 'čŞč¨źăłăźăăĺĺžă§ăăžăăă§ăă',
+ genericError: 'ăăĄă¤ăłă確čŞă§ăăžăăă§ăăăăăä¸ĺşŚă芌ăăă ăăăĺéĄăč§ŁćąşăăŞăĺ ´ĺ㯠Concierge ăŤăéŁçľĄăă ăăă',
+ },
+ domainVerified: {
+ title: 'ăăĄă¤ăłç˘şčŞć¸ăż',
+ header: 'ăăŁăďźăăŞăăŽăăĄă¤ăłă確čŞăăăžăă',
+ description: ({domainName}: {domainName: string}) =>
+ `ăăĄă¤ăł ${domainName} ăŻćŁĺ¸¸ăŤć¤č¨źăăăSAML ăăăŽäťăŽăťăăĽăŞăăŁćŠč˝ăč¨ĺŽă§ăăăăăŤăŞăăžăăă`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
// so if you change it here, please update it there as well.
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 53b3e9921439f..4fefcdcfc5708 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -442,6 +442,9 @@ const translations = {
zipPostCode: 'Postcode',
whatThis: 'Wat is dit?',
iAcceptThe: 'Ik accepteer de',
+ acceptTermsAndPrivacy: `Ik accepteer de Expensify Gebruiksvoorwaarden en Privacybeleid`,
+ acceptTermsAndConditions: `Ik accepteer de algemene voorwaarden`,
+ acceptTermsOfService: `Ik accepteer de Expensify Gebruiksvoorwaarden`,
remove: 'Verwijderen',
admin: 'Admin',
owner: 'Eigenaar',
@@ -927,17 +930,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`Deze chatroom is voor alles wat met ${reportName} te maken heeft.`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `Deze chat is voor facturen tussen ${invoicePayer} en ${invoiceReceiver}. Gebruik de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} knop om een factuur te sturen.`,
+ `Deze chat is voor facturen tussen ${invoicePayer} en ${invoiceReceiver}. Gebruik de + knop om een factuur te sturen.`,
beginningOfChatHistory: 'Deze chat is met',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `Dit is waar ${submitterDisplayName} kosten zal indienen bij ${workspaceName}. Gebruik gewoon de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} knop.`,
+ `Dit is waar ${submitterDisplayName} kosten zal indienen bij ${workspaceName}. Gebruik gewoon de + knop.`,
beginningOfChatHistorySelfDM: 'Dit is je persoonlijke ruimte. Gebruik het voor notities, taken, concepten en herinneringen.',
beginningOfChatHistorySystemDM: 'Welkom! Laten we je instellen.',
chatWithAccountManager: 'Chat hier met uw accountmanager',
sayHello: 'Zeg hallo!',
yourSpace: 'Uw ruimte',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Welkom bij ${roomName}!`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Gebruik de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} knop om een uitgave te ${additionalText}.`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Gebruik de + knop om een uitgave te ${additionalText}.`,
askConcierge: 'Stel vragen en krijg 24/7 realtime ondersteuning.',
conciergeSupport: '24/7 ondersteuning',
create: 'maken',
@@ -2246,10 +2249,9 @@ ${amount} voor ${merchant} - ${date}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `in ${policyName}`,
- generatingPDF: 'PDF genereren',
+ generatingPDF: 'PDF genereren...',
waitForPDF: 'Even geduld terwijl we de PDF genereren.',
errorPDF: 'Er is een fout opgetreden bij het genereren van uw PDF.',
- generatedPDF: 'Je rapport-PDF is gegenereerd!',
},
reportDescriptionPage: {
roomDescription: 'Kamerbeschrijving',
@@ -2457,7 +2459,7 @@ ${amount} voor ${merchant} - ${date}`,
description:
'*Dien een uitgave in* door een bedrag in te voeren of een bon te scannen.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Uitgave aanmaken*.\n' +
'3. Voer een bedrag in of scan een bon.\n' +
`4. Voeg het e-mailadres of telefoonnummer van uw baas toe.\n` +
@@ -2470,7 +2472,7 @@ ${amount} voor ${merchant} - ${date}`,
description:
'*Dien een uitgave in* door een bedrag in te voeren of een bon te scannen.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Uitgave aanmaken*.\n' +
'3. Voer een bedrag in of scan een bon.\n' +
'4. Bevestig de details.\n' +
@@ -2483,7 +2485,7 @@ ${amount} voor ${merchant} - ${date}`,
description:
'*Volg een uitgave* in elke valuta, of u nu een bon heeft of niet.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Uitgave aanmaken*.\n' +
'3. Voer een bedrag in of scan een bon.\n' +
'4. Kies uw *persoonlijke* ruimte.\n' +
@@ -2577,7 +2579,7 @@ ${amount} voor ${merchant} - ${date}`,
description:
'*Start een chat* met iedereen met behulp van hun e-mailadres of telefoonnummer.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Start chat*.\n' +
'3. Voer een e-mailadres of telefoonnummer in.\n' +
'\n' +
@@ -2590,7 +2592,7 @@ ${amount} voor ${merchant} - ${date}`,
description:
'*Splits uitgaven* met ĂŠĂŠn of meer personen.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Start chat*.\n' +
'3. Voer e-mailadressen of telefoonnummers in.\n' +
'4. Klik op de grijze *+*-knop in de chat > *Splits uitgave*.\n' +
@@ -2612,7 +2614,7 @@ ${amount} voor ${merchant} - ${date}`,
description:
'Zo maakt u een rapport:\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Rapport aanmaken*.\n' +
'3. Klik op *Uitgave toevoegen*.\n' +
'4. Voeg uw eerste uitgave toe.\n' +
@@ -2630,10 +2632,8 @@ ${amount} voor ${merchant} - ${date}`,
messages: {
onboardingEmployerOrSubmitMessage: 'Terugbetaald krijgen is net zo eenvoudig als een bericht sturen. Laten we de basis doornemen.',
onboardingPersonalSpendMessage: 'Zo volgt u uw uitgaven in een paar klikken.',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# Je gratis proefperiode is gestart! Laten we aan de slag gaan.\nđ Hoi, ik ben je Expensify-instellingsspecialist. Nu je een werkruimte hebt gemaakt, haal het meeste uit je 30 dagen gratis proefperiode door de onderstaande stappen te volgen!'
- : '# Je gratis proefperiode is gestart! Laten we aan de slag gaan.\nđ Hoi, ik ben je Expensify-instellingsspecialist. Ik heb al een werkruimte gemaakt om je te helpen met het beheren van de bonnetjes en uitgaven van je team. Haal het meeste uit je 30 dagen gratis proefperiode door eenvoudig de resterende instellingsstappen hieronder te volgen!',
+ onboardingManageTeamMessage:
+ '# Je gratis proefperiode is begonnen! Laten we aan de slag gaan met de installatie.\nđ Hallo, ik ben je Expensify-installatiespecialist. Nu je een workspace hebt gemaakt, haal het meeste uit je 30 dagen gratis proefperiode door de onderstaande stappen te volgen!',
onboardingTrackWorkspaceMessage:
'# Laten we u instellen\nđ Ik ben hier om te helpen! Om u op weg te helpen, heb ik uw werkruimte-instellingen afgestemd op eenmanszaken en soortgelijke bedrijven. U kunt uw werkruimte aanpassen door op de onderstaande link te klikken!\n\nZo volgt u uw uitgaven in een paar klikken:',
onboardingChatSplitMessage: 'Rekeningen splitsen met vrienden is net zo eenvoudig als een bericht sturen. Zo doet u dat.',
@@ -4580,9 +4580,7 @@ ${amount} voor ${merchant} - ${date}`,
cardholder: 'Kaart houder',
card: 'Kaart',
cardName: 'Kaartnaam',
- brokenConnectionErrorFirstPart: `Kaartfeedverbinding is verbroken. Alsjeblieft`,
- brokenConnectionErrorLink: 'log in op uw bank',
- brokenConnectionErrorSecondPart: 'zodat we de verbinding opnieuw kunnen herstellen.',
+ brokenConnectionError: 'Kaartfeedverbinding is verbroken. Alsjeblieft log in op uw bank zodat we de verbinding opnieuw kunnen herstellen.',
assignedCard: ({assignee, link}: AssignedCardParams) => `heeft ${assignee} een ${link} toegewezen! GeĂŻmporteerde transacties zullen in deze chat verschijnen.`,
companyCard: 'bedrijfskaart',
chooseCardFeed: 'Kies kaartfeed',
@@ -4634,6 +4632,7 @@ ${amount} voor ${merchant} - ${date}`,
monthly: 'Maandelijks',
},
cardDetails: 'Kaartgegevens',
+ cardPending: ({name}: {name: string}) => `De kaart is momenteel in behandeling en wordt uitgegeven zodra het account van ${name} is gevalideerd.`,
virtual: 'Virtueel',
physical: 'Fysiek',
deactivate: 'Deactiveer kaart',
@@ -4817,9 +4816,8 @@ ${amount} voor ${merchant} - ${date}`,
noAccountsFound: 'Geen accounts gevonden',
defaultCard: 'Standaardkaart',
downgradeTitle: `Kan werkruimte niet downgraden`,
- downgradeSubTitleFirstPart: `Deze werkruimte kan niet worden gedowngraded omdat er meerdere kaartfeeds zijn verbonden (met uitzondering van Expensify Cards). Alstublieft`,
- downgradeSubTitleMiddlePart: `houd slechts ĂŠĂŠn kaartfeed`,
- downgradeSubTitleLastPart: 'om door te gaan.',
+ downgradeSubTitle: `Deze werkruimte kan niet worden gedowngraded omdat er meerdere kaartfeeds zijn verbonden (met uitzondering van Expensify Cards). Alstublieft houd slechts ĂŠĂŠn kaartfeed om door te gaan.`,
+
noAccountsFoundDescription: ({connection}: ConnectionParams) => `Voeg het account toe in ${connection} en synchroniseer de verbinding opnieuw.`,
expensifyCardBannerTitle: 'Verkrijg de Expensify Card',
expensifyCardBannerSubtitle: 'Geniet van cashback op elke aankoop in de VS, tot 50% korting op je Expensify-rekening, onbeperkte virtuele kaarten en nog veel meer.',
@@ -4945,6 +4943,7 @@ ${amount} voor ${merchant} - ${date}`,
existingReportFieldNameError: 'Er bestaat al een rapportveld met deze naam',
reportFieldNameRequiredError: 'Voer een rapportveldnaam in alstublieft',
reportFieldTypeRequiredError: 'Kies een rapportveldtype aub',
+ circularReferenceError: 'Dit veld kan niet naar zichzelf verwijzen. Werk het bij.',
reportFieldInitialValueRequiredError: 'Kies een initiĂŤle waarde voor een rapportveld alstublieft.',
genericFailureMessage: 'Er is een fout opgetreden bij het bijwerken van het rapportveld. Probeer het opnieuw.',
},
@@ -5503,8 +5502,8 @@ ${amount} voor ${merchant} - ${date}`,
enableRate: 'Tarief inschakelen',
status: 'Status',
unit: 'Eenheid',
- taxFeatureNotEnabledMessage: 'Belastingen moeten zijn ingeschakeld in de werkruimte om deze functie te gebruiken. Ga naar',
- changePromptMessage: 'om die wijziging door te voeren.',
+ taxFeatureNotEnabledMessage:
+ 'Belastingen moeten zijn ingeschakeld in de werkruimte om deze functie te gebruiken. Ga naar Meer functies om die wijziging door te voeren.',
deleteDistanceRate: 'Verwijder afstandstarief',
areYouSureDelete: () => ({
one: 'Weet je zeker dat je dit tarief wilt verwijderen?',
@@ -5703,6 +5702,12 @@ ${amount} voor ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Afstandstarieven zijn beschikbaar op het Collect-abonnement, beginnend bij ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actief lid per maand.`}`,
},
+ auditor: {
+ title: 'Auditor',
+ description: 'Auditors krijgen alleen-lezen toegang tot alle rapporten voor volledige zichtbaarheid en nalevingscontrole.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Auditors zijn alleen beschikbaar in het Control-plan, beginnend bij ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actief lid per maand.`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Meerdere goedkeuringsniveaus',
description: 'Meerdere goedkeuringsniveaus is een workflowtool voor bedrijven die vereisen dat meer dan ĂŠĂŠn persoon een rapport goedkeurt voordat het kan worden vergoed.',
@@ -6207,7 +6212,7 @@ ${amount} voor ${merchant} - ${date}`,
searchResults: {
emptyResults: {
title: 'Niets om te laten zien',
- subtitle: `Probeer je zoekcriteria aan te passen of iets te maken met de groene ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} knop.`,
+ subtitle: `Probeer je zoekcriteria aan te passen of iets te maken met de + knop.`,
},
emptyExpenseResults: {
title: 'Je hebt nog geen uitgaven gemaakt.',
@@ -7386,6 +7391,28 @@ ${amount} voor ${merchant} - ${date}`,
subtitle: `We hebben niet al uw gegevens kunnen laden. We zijn op de hoogte gesteld en onderzoeken het probleem. Als dit aanhoudt, neem dan contact op met`,
refreshAndTryAgain: 'Vernieuw en probeer het opnieuw',
},
+ domain: {
+ notVerified: 'Niet geverifieerd',
+ retry: 'Opnieuw proberen',
+ verifyDomain: {
+ title: 'Domein verifiĂŤren',
+ beforeProceeding: ({domainName}: {domainName: string}) =>
+ `Controleer voordat je verdergaat of je eigenaar bent van ${domainName} door de DNS-instellingen bij te werken.`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `Ga naar je DNS-provider en open de DNS-instellingen voor ${domainName}.`,
+ addTXTRecord: 'Voeg het volgende TXT-record toe:',
+ saveChanges: 'Sla wijzigingen op en kom hier terug om je domein te verifiĂŤren.',
+ youMayNeedToConsult: `Misschien moet je contact opnemen met de IT-afdeling van je organisatie om de verificatie te voltooien. Meer informatie.`,
+ warning: 'Na verificatie ontvangen alle Expensify-leden op je domein een e-mail waarin staat dat hun account onder je domein wordt beheerd.',
+ codeFetchError: 'Kon de verificatiecode niet ophalen',
+ genericError: 'We konden uw domein niet verifiĂŤren. Probeer het opnieuw en neem contact op met Concierge als het probleem aanhoudt.',
+ },
+ domainVerified: {
+ title: 'Domein geverifieerd',
+ header: 'Woehoe! Je domein is geverifieerd',
+ description: ({domainName}: {domainName: string}) =>
+ `Het domein ${domainName} is succesvol geverifieerd en je kunt nu SAML en andere beveiligingsfuncties instellen.`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
// so if you change it here, please update it there as well.
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 40dda5d0bd6d9..c6c0dd6b4f162 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -442,6 +442,9 @@ const translations = {
zipPostCode: 'Kod pocztowy',
whatThis: 'Co to jest?',
iAcceptThe: 'AkceptujÄ',
+ acceptTermsAndPrivacy: `AkceptujÄ Warunki korzystania z usĹugi Expensify i Polityka prywatnoĹci`,
+ acceptTermsAndConditions: `AkceptujÄ warunki i zasady`,
+ acceptTermsOfService: `AkceptujÄ Warunki korzystania z usĹugi Expensify`,
remove: 'UsuĹ',
admin: 'Admin',
owner: 'WĹaĹciciel',
@@ -929,17 +932,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`Ten czat jest przeznaczony do wszystkiego, co zwiÄ zane z ${reportName}.`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `Ten czat sĹuĹźy do wystawiania faktur miÄdzy ${invoicePayer} i ${invoiceReceiver}. UĹźyj przycisku ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}, aby wysĹaÄ fakturÄ.`,
+ `Ten czat sĹuĹźy do wystawiania faktur miÄdzy ${invoicePayer} i ${invoiceReceiver}. UĹźyj przycisku +, aby wysĹaÄ fakturÄ.`,
beginningOfChatHistory: 'Ta rozmowa jest z',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `W tym miejscu ${submitterDisplayName} bÄdzie przesyĹaÄ wydatki do ${workspaceName}. Wystarczy uĹźyÄ przycisku ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ `W tym miejscu ${submitterDisplayName} bÄdzie przesyĹaÄ wydatki do ${workspaceName}. Wystarczy uĹźyÄ przycisku +.`,
beginningOfChatHistorySelfDM: 'To jest Twoja przestrzeĹ osobista. UĹźywaj jej do notatek, zadaĹ, szkicĂłw i przypomnieĹ.',
beginningOfChatHistorySystemDM: 'Witamy! Zacznijmy konfiguracjÄ.',
chatWithAccountManager: 'Czat z Twoim opiekunem konta tutaj',
sayHello: 'Powiedz czeĹÄ!',
yourSpace: 'Twoja przestrzeĹ',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Witamy w ${roomName}!`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` UĹźyj przycisku ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}, aby ${additionalText} wydatek.`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` UĹźyj przycisku +, aby ${additionalText} wydatek.`,
askConcierge: 'Zadawaj pytania i otrzymuj wsparcie w czasie rzeczywistym 24/7.',
conciergeSupport: 'CaĹodobowe wsparcie',
create: 'utwĂłrz',
@@ -2241,10 +2244,9 @@ ${amount} dla ${merchant} - ${date}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `w ${policyName}`,
- generatingPDF: 'Generowanie PDF',
+ generatingPDF: 'Generowanie PDF...',
waitForPDF: 'ProszÄ czekaÄ, generujemy PDF',
errorPDF: 'WystÄ piĹ bĹÄ d podczas prĂłby wygenerowania Twojego PDF-a.',
- generatedPDF: 'TwĂłj raport PDF zostaĹ wygenerowany!',
},
reportDescriptionPage: {
roomDescription: 'Opis pokoju',
@@ -2451,7 +2453,7 @@ ${amount} dla ${merchant} - ${date}`,
description:
'*Dien een uitgave in* door een bedrag in te voeren of een bon te scannen.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Uitgave aanmaken*.\n' +
'3. Voer een bedrag in of scan een bon.\n' +
`4. Voeg het e-mailadres of telefoonnummer van uw baas toe.\n` +
@@ -2464,7 +2466,7 @@ ${amount} dla ${merchant} - ${date}`,
description:
'*Dien een uitgave in* door een bedrag in te voeren of een bon te scannen.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Uitgave aanmaken*.\n' +
'3. Voer een bedrag in of scan een bon.\n' +
'4. Bevestig de details.\n' +
@@ -2477,7 +2479,7 @@ ${amount} dla ${merchant} - ${date}`,
description:
'*Volg een uitgave* in elke valuta, of u nu een bon heeft of niet.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Uitgave aanmaken*.\n' +
'3. Voer een bedrag in of scan een bon.\n' +
'4. Kies uw *persoonlijke* ruimte.\n' +
@@ -2571,7 +2573,7 @@ ${amount} dla ${merchant} - ${date}`,
description:
'*Start een chat* met iedereen met behulp van hun e-mailadres of telefoonnummer.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Start chat*.\n' +
'3. Voer een e-mailadres of telefoonnummer in.\n' +
'\n' +
@@ -2584,7 +2586,7 @@ ${amount} dla ${merchant} - ${date}`,
description:
'*Splits uitgaven* met ĂŠĂŠn of meer personen.\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Start chat*.\n' +
'3. Voer e-mailadressen of telefoonnummers in.\n' +
'4. Klik op de grijze *+*-knop in de chat > *Splits uitgave*.\n' +
@@ -2606,7 +2608,7 @@ ${amount} dla ${merchant} - ${date}`,
description:
'Zo maakt u een rapport:\n' +
'\n' +
- `1. Klik op de ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}-knop.\n` +
+ `1. Klik op de +-knop.\n` +
'2. Kies *Rapport aanmaken*.\n' +
'3. Klik op *Uitgave toevoegen*.\n' +
'4. Voeg uw eerste uitgave toe.\n' +
@@ -2624,10 +2626,8 @@ ${amount} dla ${merchant} - ${date}`,
messages: {
onboardingEmployerOrSubmitMessage: 'Terugbetaald krijgen is net zo eenvoudig als een bericht sturen. Laten we de basis doornemen.',
onboardingPersonalSpendMessage: 'Zo volgt u uw uitgaven in een paar klikken.',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# TwĂłj bezpĹatny okres prĂłbny wĹaĹnie siÄ rozpoczÄ Ĺ! Skonfigurujmy wszystko.\nđ CzeĹÄ, jestem twoim specjalistÄ ds. konfiguracji Expensify. Teraz, gdy utworzyĹeĹ przestrzeĹ roboczÄ , w peĹni wykorzystaj 30-dniowy bezpĹatny okres prĂłbny, wykonujÄ c poniĹźsze kroki!'
- : '# TwĂłj bezpĹatny okres prĂłbny wĹaĹnie siÄ rozpoczÄ Ĺ! Skonfigurujmy wszystko.\nđ CzeĹÄ, jestem twoim specjalistÄ ds. konfiguracji Expensify. JuĹź utworzyĹem przestrzeĹ roboczÄ , aby pomĂłc w zarzÄ dzaniu paragonami i wydatkami twojego zespoĹu. Aby w peĹni wykorzystaÄ 30-dniowy bezpĹatny okres prĂłbny, po prostu wykonaj poniĹźsze pozostaĹe kroki konfiguracji!',
+ onboardingManageTeamMessage:
+ '# TwĂłj bezpĹatny okres prĂłbny wĹaĹnie siÄ rozpoczÄ Ĺ! Zacznijmy konfiguracjÄ.\nđ CzeĹÄ, jestem Twoim specjalistÄ ds. konfiguracji Expensify. Teraz, gdy utworzyĹeĹ przestrzeĹ roboczÄ , wykorzystaj w peĹni swoje 30 dni bezpĹatnego okresu prĂłbnego, postÄpujÄ c zgodnie z poniĹźszymi krokami!',
onboardingTrackWorkspaceMessage:
'# Laten we u instellen\nđ Ik ben hier om te helpen! Om u op weg te helpen, heb ik uw werkruimte-instellingen afgestemd op eenmanszaken en soortgelijke bedrijven. U kunt uw werkruimte aanpassen door op de onderstaande link te klikken!\n\nZo volgt u uw uitgaven in een paar klikken:',
onboardingChatSplitMessage: 'Rekeningen splitsen met vrienden is net zo eenvoudig als een bericht sturen. Zo doet u dat.',
@@ -4568,9 +4568,7 @@ ${amount} dla ${merchant} - ${date}`,
cardholder: 'Posiadacz karty',
card: 'Karta',
cardName: 'Nazwa karty',
- brokenConnectionErrorFirstPart: `PoĹÄ czenie z kanaĹem karty jest przerwane. ProszÄ`,
- brokenConnectionErrorLink: 'zaloguj siÄ do swojego banku',
- brokenConnectionErrorSecondPart: 'abyĹmy mogli ponownie nawiÄ zaÄ poĹÄ czenie.',
+ brokenConnectionError: 'PoĹÄ czenie z kanaĹem karty jest przerwane. ProszÄ zaloguj siÄ do swojego banku abyĹmy mogli ponownie nawiÄ zaÄ poĹÄ czenie.',
assignedCard: ({assignee, link}: AssignedCardParams) => `przypisano ${assignee} ${link}! Zaimportowane transakcje pojawiÄ siÄ w tym czacie.`,
companyCard: 'karta firmowa',
chooseCardFeed: 'Wybierz kanaĹ kart',
@@ -4622,6 +4620,7 @@ ${amount} dla ${merchant} - ${date}`,
monthly: 'MiesiÄczny',
},
cardDetails: 'SzczegĂłĹy karty',
+ cardPending: ({name}: {name: string}) => `Karta jest obecnie w oczekiwaniu i zostanie wydana po zweryfikowaniu konta ${name}.`,
virtual: 'Wirtualny',
physical: 'Fizyczny',
deactivate: 'Dezaktywuj kartÄ',
@@ -4806,9 +4805,8 @@ ${amount} dla ${merchant} - ${date}`,
noAccountsFound: 'Nie znaleziono kont',
defaultCard: 'DomyĹlna karta',
downgradeTitle: `Nie moĹźna obniĹźyÄ poziomu workspace.`,
- downgradeSubTitleFirstPart: `Tego miejsca pracy nie moĹźna obniĹźyÄ, poniewaĹź jest poĹÄ czonych wiele kanaĹĂłw kart (z wyĹÄ czeniem kart Expensify). ProszÄ`,
- downgradeSubTitleMiddlePart: `zachowaj tylko jeden kanaĹ kart`,
- downgradeSubTitleLastPart: 'aby kontynuowaÄ.',
+ downgradeSubTitle: `Tego miejsca pracy nie moĹźna obniĹźyÄ, poniewaĹź jest poĹÄ czonych wiele kanaĹĂłw kart (z wyĹÄ czeniem kart Expensify). ProszÄ zachowaj tylko jeden kanaĹ kart aby kontynuowaÄ.`,
+
noAccountsFoundDescription: ({connection}: ConnectionParams) => `ProszÄ dodaÄ konto w ${connection} i ponownie zsynchronizowaÄ poĹÄ czenie.`,
expensifyCardBannerTitle: 'ZdobÄ dĹş kartÄ Expensify',
expensifyCardBannerSubtitle:
@@ -4935,6 +4933,7 @@ ${amount} dla ${merchant} - ${date}`,
existingReportFieldNameError: 'Pole raportu o tej nazwie juĹź istnieje',
reportFieldNameRequiredError: 'ProszÄ wprowadziÄ nazwÄ pola raportu',
reportFieldTypeRequiredError: 'ProszÄ wybraÄ typ pola raportu',
+ circularReferenceError: 'To pole nie moĹźe odnosiÄ siÄ do siebie samego. ProszÄ zaktualizowaÄ.',
reportFieldInitialValueRequiredError: 'ProszÄ wybraÄ poczÄ tkowÄ wartoĹÄ pola raportu',
genericFailureMessage: 'WystÄ piĹ bĹÄ d podczas aktualizacji pola raportu. ProszÄ sprĂłbowaÄ ponownie.',
},
@@ -5492,8 +5491,8 @@ ${amount} dla ${merchant} - ${date}`,
enableRate: 'WĹÄ cz stawkÄ',
status: 'Status',
unit: 'Jednostka',
- taxFeatureNotEnabledMessage: 'Podatki muszÄ byÄ wĹÄ czone w przestrzeni roboczej, aby uĹźyÄ tej funkcji. PrzejdĹş do',
- changePromptMessage: 'aby dokonaÄ tej zmiany.',
+ taxFeatureNotEnabledMessage:
+ 'Podatki muszÄ byÄ wĹÄ czone w przestrzeni roboczej, aby uĹźyÄ tej funkcji. PrzejdĹş do WiÄcej funkcjiaby dokonaÄ tej zmiany.',
deleteDistanceRate: 'UsuĹ stawkÄ za odlegĹoĹÄ',
areYouSureDelete: () => ({
one: 'Czy na pewno chcesz usunÄ Ä tÄ stawkÄ?',
@@ -5692,6 +5691,12 @@ ${amount} dla ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Stawki za odlegĹoĹÄ sÄ dostÄpne w planie Collect, zaczynajÄ c od ${formattedPrice} ${hasTeam2025Pricing ? `za czĹonka miesiÄcznie.` : `na aktywnego czĹonka miesiÄcznie.`}`,
},
+ auditor: {
+ title: 'Audytor',
+ description: 'Audytorzy majÄ dostÄp tylko do odczytu wszystkich raportĂłw, zapewniajÄ c peĹnÄ przejrzystoĹÄ i monitorowanie zgodnoĹci.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Audytorzy sÄ dostÄpni tylko w planie Control, zaczynajÄ c od ${formattedPrice} ${hasTeam2025Pricing ? `za czĹonka miesiÄcznie.` : `na aktywnego czĹonka miesiÄcznie.`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Wiele poziomĂłw zatwierdzania',
description:
@@ -6194,7 +6199,7 @@ ${amount} dla ${merchant} - ${date}`,
searchResults: {
emptyResults: {
title: 'Brak danych do wyĹwietlenia',
- subtitle: `SprĂłbuj dostosowaÄ kryteria wyszukiwania lub utwĂłrz coĹ za pomocÄ zielonego przycisku ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ subtitle: `SprĂłbuj dostosowaÄ kryteria wyszukiwania lub utwĂłrz coĹ za pomocÄ przycisku +.`,
},
emptyExpenseResults: {
title: 'Nie utworzyĹeĹ jeszcze Ĺźadnych wydatkĂłw.',
@@ -7374,6 +7379,28 @@ ${amount} dla ${merchant} - ${date}`,
subtitle: `Nie udaĹo nam siÄ wczytaÄ wszystkich Twoich danych. ZostaliĹmy o tym powiadomieni i badamy problem. JeĹli problem bÄdzie siÄ utrzymywaĹ, skontaktuj siÄ z`,
refreshAndTryAgain: 'OdĹwieĹź i sprĂłbuj ponownie',
},
+ domain: {
+ notVerified: 'Niezweryfikowano',
+ retry: 'SprĂłbuj ponownie',
+ verifyDomain: {
+ title: 'Zweryfikuj domenÄ',
+ beforeProceeding: ({domainName}: {domainName: string}) =>
+ `Zanim przejdziesz dalej, potwierdĹş, Ĺźe jesteĹ wĹaĹcicielem ${domainName}, aktualizujÄ c jego ustawienia DNS.`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `Uzyskaj dostÄp do swojego dostawcy DNS i otwĂłrz ustawienia DNS dla ${domainName}.`,
+ addTXTRecord: 'Dodaj nastÄpujÄ cy rekord TXT:',
+ saveChanges: 'Zapisz zmiany i wrĂłÄ tutaj, aby zweryfikowaÄ swojÄ domenÄ.',
+ youMayNeedToConsult: `MoĹźe byÄ konieczna konsultacja z dziaĹem IT Twojej organizacji, aby zakoĹczyÄ weryfikacjÄ. Dowiedz siÄ wiÄcej.`,
+ warning: 'Po weryfikacji wszyscy czĹonkowie Expensify w Twojej domenie otrzymajÄ wiadomoĹÄ e-mail z informacjÄ , Ĺźe ich konta bÄdÄ zarzÄ dzane w ramach Twojej domeny.',
+ codeFetchError: 'Nie udaĹo siÄ pobraÄ kodu weryfikacyjnego',
+ genericError: 'Nie udaĹo nam siÄ zweryfikowaÄ Twojej domeny. SprĂłbuj ponownie i skontaktuj siÄ z Concierge, jeĹli problem bÄdzie siÄ utrzymywaĹ.',
+ },
+ domainVerified: {
+ title: 'Domena zweryfikowana',
+ header: 'Hurra! Twoja domena zostaĹa zweryfikowana',
+ description: ({domainName}: {domainName: string}) =>
+ `Domena ${domainName} zostaĹa pomyĹlnie zweryfikowana i moĹźesz teraz skonfigurowaÄ SAML oraz inne funkcje zabezpieczeĹ.`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
// so if you change it here, please update it there as well.
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 67cc651980fb1..31b648b04bd85 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -442,6 +442,9 @@ const translations = {
zipPostCode: 'CEP / CĂłdigo Postal',
whatThis: 'O que ĂŠ isso?',
iAcceptThe: 'Eu aceito o',
+ acceptTermsAndPrivacy: `Eu aceito o Termos de Serviço da Expensify e PolĂtica de Privacidade`,
+ acceptTermsAndConditions: `Eu aceito o termos e condiçþes`,
+ acceptTermsOfService: `Eu aceito o Termos de Serviço da Expensify`,
remove: 'Remover',
admin: 'Administração',
owner: 'ProprietĂĄrio',
@@ -930,17 +933,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`Esta sala de bate-papo ĂŠ para qualquer coisa relacionada ao ${reportName}.`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `Este bate-papo ĂŠ para faturas entre ${invoicePayer} e a ${invoiceReceiver}. Use o botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} para enviar uma fatura.`,
+ `Este bate-papo ĂŠ para faturas entre ${invoicePayer} e a ${invoiceReceiver}. Use o botĂŁo + para enviar uma fatura.`,
beginningOfChatHistory: 'Este chat ĂŠ com',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `Ă aqui que ${submitterDisplayName} enviarĂĄ as despesas para a ${workspaceName}. Basta usar o botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ `Ă aqui que ${submitterDisplayName} enviarĂĄ as despesas para a ${workspaceName}. Basta usar o botĂŁo +.`,
beginningOfChatHistorySelfDM: 'Este Ê o seu espaço pessoal. Use-o para anotaçþes, tarefas, rascunhos e lembretes.',
beginningOfChatHistorySystemDM: 'Bem-vindo! Vamos configurĂĄ-lo.',
chatWithAccountManager: 'Converse com o seu gerente de conta aqui',
sayHello: 'Diga olĂĄ!',
yourSpace: 'Seu espaço',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Bem-vindo(a) ao ${roomName}!`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Use o botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} para ${additionalText} uma despesa.`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` Use o botĂŁo + para ${additionalText} uma despesa.`,
askConcierge: 'Faça perguntas e receba suporte em tempo real 24/7.',
conciergeSupport: 'Suporte 24/7',
create: 'criar',
@@ -2238,10 +2241,9 @@ ${amount} para ${merchant} - ${date}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `em ${policyName}`,
- generatingPDF: 'Gerando PDF',
+ generatingPDF: 'Gerando PDF...',
waitForPDF: 'Por favor, aguarde enquanto geramos o PDF.',
errorPDF: 'Ocorreu um erro ao tentar gerar seu PDF.',
- generatedPDF: 'Seu PDF de relatĂłrio foi gerado!',
},
reportDescriptionPage: {
roomDescription: 'Descrição do quarto',
@@ -2448,7 +2450,7 @@ ${amount} para ${merchant} - ${date}`,
description:
'*Envie uma despesa* inserindo um valor ou digitalizando um recibo.\n' +
'\n' +
- `1. Clique no botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clique no botĂŁo +.\n` +
'2. Escolha *Criar despesa*.\n' +
'3. Insira um valor ou digitalize um recibo.\n' +
`4. Adicione o e-mail ou nĂşmero de telefone do seu chefe.\n` +
@@ -2461,7 +2463,7 @@ ${amount} para ${merchant} - ${date}`,
description:
'*Envie uma despesa* inserindo um valor ou digitalizando um recibo.\n' +
'\n' +
- `1. Clique no botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clique no botĂŁo +.\n` +
'2. Escolha *Criar despesa*.\n' +
'3. Insira um valor ou digitalize um recibo.\n' +
'4. Confirme os detalhes.\n' +
@@ -2474,7 +2476,7 @@ ${amount} para ${merchant} - ${date}`,
description:
'*Rastreie uma despesa* em qualquer moeda, com ou sem recibo.\n' +
'\n' +
- `1. Clique no botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clique no botĂŁo +.\n` +
'2. Escolha *Criar despesa*.\n' +
'3. Insira um valor ou digitalize um recibo.\n' +
'4. Escolha seu espaço *pessoal*.\n' +
@@ -2568,7 +2570,7 @@ ${amount} para ${merchant} - ${date}`,
description:
'*Inicie um bate-papo* com qualquer pessoa usando seu e-mail ou nĂşmero de telefone.\n' +
'\n' +
- `1. Clique no botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clique no botĂŁo +.\n` +
'2. Escolha *Iniciar bate-papo*.\n' +
'3. Insira um e-mail ou nĂşmero de telefone.\n' +
'\n' +
@@ -2581,7 +2583,7 @@ ${amount} para ${merchant} - ${date}`,
description:
'*Divida despesas* com uma ou mais pessoas.\n' +
'\n' +
- `1. Clique no botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clique no botĂŁo +.\n` +
'2. Escolha *Iniciar bate-papo*.\n' +
'3. Insira e-mails ou nĂşmeros de telefone.\n' +
'4. Clique no botĂŁo cinza *+* no bate-papo > *Dividir despesa*.\n' +
@@ -2603,7 +2605,7 @@ ${amount} para ${merchant} - ${date}`,
description:
'Veja como criar um relatĂłrio:\n' +
'\n' +
- `1. Clique no botĂŁo ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.\n` +
+ `1. Clique no botĂŁo +.\n` +
'2. Escolha *Criar relatĂłrio*.\n' +
'3. Clique em *Adicionar despesa*.\n' +
'4. Adicione sua primeira despesa.\n' +
@@ -2621,10 +2623,8 @@ ${amount} para ${merchant} - ${date}`,
messages: {
onboardingEmployerOrSubmitMessage: 'Ser reembolsado ĂŠ tĂŁo fĂĄcil quanto enviar uma mensagem. Vamos ver o bĂĄsico.',
onboardingPersonalSpendMessage: 'Veja como rastrear seus gastos em poucos cliques.',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# Seu teste gratuito começou! Vamos configurar tudo.\nđ OlĂĄ, sou seu especialista de configuração da Expensify. Agora que vocĂŞ criou um workspace, aproveite ao mĂĄximo seus 30 dias de teste gratuito seguindo as etapas abaixo!'
- : '# Seu teste gratuito começou! Vamos configurar tudo.\nđ OlĂĄ, sou seu especialista de configuração da Expensify. JĂĄ criei um workspace para ajudar a gerenciar os recibos e despesas da sua equipe. Para aproveitar ao mĂĄximo seus 30 dias de teste gratuito, basta seguir as etapas restantes de configuração abaixo!',
+ onboardingManageTeamMessage:
+ '# Seu teste gratuito começou! Vamos configurar tudo.\nđ OlĂĄ, sou seu especialista de configuração da Expensify. Agora que vocĂŞ criou um workspace, aproveite ao mĂĄximo seus 30 dias de teste gratuito seguindo as etapas abaixo!',
onboardingTrackWorkspaceMessage:
'# Vamos configurar vocĂŞ\nđ Estou aqui para ajudar! Para vocĂŞ começar, adaptei as configuraçþes do seu espaço de trabalho para microempreendedores individuais e empresas semelhantes. VocĂŞ pode ajustar seu espaço de trabalho clicando no link abaixo!\n\nVeja como rastrear seus gastos em poucos cliques:',
onboardingChatSplitMessage: 'Dividir contas com amigos ĂŠ tĂŁo fĂĄcil quanto enviar uma mensagem. Veja como.',
@@ -4568,9 +4568,8 @@ ${amount} para ${merchant} - ${date}`,
cardholder: 'Titular do cartĂŁo',
card: 'CartĂŁo',
cardName: 'Nome do cartĂŁo',
- brokenConnectionErrorFirstPart: `A conexĂŁo do feed do cartĂŁo estĂĄ quebrada. Por favor,`,
- brokenConnectionErrorLink: 'faça login no seu banco',
- brokenConnectionErrorSecondPart: 'para que possamos estabelecer a conexĂŁo novamente.',
+ brokenConnectionError:
+ 'A conexão do feed do cartão estå quebrada. Por favor, faça login no seu banco para que possamos estabelecer a conexão novamente.',
assignedCard: ({assignee, link}: AssignedCardParams) => `atribuiu ${assignee} um ${link}! As transaçþes importadas aparecerão neste chat.`,
companyCard: 'cartĂŁo corporativo',
chooseCardFeed: 'Escolher feed de cartĂŁo',
@@ -4621,6 +4620,7 @@ ${amount} para ${merchant} - ${date}`,
monthly: 'Mensalmente',
},
cardDetails: 'Detalhes do cartĂŁo',
+ cardPending: ({name}: {name: string}) => `O cartĂŁo estĂĄ pendente e serĂĄ emitido assim que a conta de ${name} for validada.`,
virtual: 'Virtual',
physical: 'FĂsico',
deactivate: 'Desativar cartĂŁo',
@@ -4806,9 +4806,8 @@ ${amount} para ${merchant} - ${date}`,
noAccountsFound: 'Nenhuma conta encontrada',
defaultCard: 'CartĂŁo padrĂŁo',
downgradeTitle: `NĂŁo ĂŠ possĂvel rebaixar o espaço de trabalho`,
- downgradeSubTitleFirstPart: `Este espaço de trabalho não pode ser rebaixado porque vårios feeds de cartão estão conectados (excluindo os Cartþes Expensify). Por favor,`,
- downgradeSubTitleMiddlePart: `manter apenas um feed de cartĂŁo`,
- downgradeSubTitleLastPart: 'para prosseguir.',
+ downgradeSubTitle: `Este espaço de trabalho não pode ser rebaixado porque vårios feeds de cartão estão conectados (excluindo os Cartþes Expensify). Por favor, manter apenas um feed de cartão para prosseguir.`,
+
noAccountsFoundDescription: ({connection}: ConnectionParams) => `Por favor, adicione a conta em ${connection} e sincronize a conexĂŁo novamente.`,
expensifyCardBannerTitle: 'Obtenha o CartĂŁo Expensify',
expensifyCardBannerSubtitle: 'Aproveite o cashback em todas as compras nos EUA, atĂŠ 50% de desconto na sua fatura do Expensify, cartĂľes virtuais ilimitados e muito mais.',
@@ -4934,6 +4933,7 @@ ${amount} para ${merchant} - ${date}`,
existingReportFieldNameError: 'Um campo de relatĂłrio com este nome jĂĄ existe',
reportFieldNameRequiredError: 'Por favor, insira um nome de campo de relatĂłrio',
reportFieldTypeRequiredError: 'Por favor, escolha um tipo de campo de relatĂłrio',
+ circularReferenceError: 'Este campo nĂŁo pode se referir a si mesmo. Atualize.',
reportFieldInitialValueRequiredError: 'Por favor, escolha um valor inicial para o campo do relatĂłrio',
genericFailureMessage: 'Ocorreu um erro ao atualizar o campo do relatĂłrio. Por favor, tente novamente.',
},
@@ -5495,8 +5495,8 @@ ${amount} para ${merchant} - ${date}`,
enableRate: 'Habilitar taxa',
status: 'Status',
unit: 'Unidade',
- taxFeatureNotEnabledMessage: 'Os impostos devem estar ativados no espaço de trabalho para usar este recurso. Vå para',
- changePromptMessage: 'para fazer essa alteração.',
+ taxFeatureNotEnabledMessage:
+ 'Os impostos devem estar ativados no espaço de trabalho para usar este recurso. Vå para Mais funcionalidades para fazer essa alteração.',
deleteDistanceRate: 'Excluir taxa de distância',
areYouSureDelete: () => ({
one: 'Tem certeza de que deseja excluir esta taxa?',
@@ -5696,6 +5696,12 @@ ${amount} para ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`As tarifas de distância estĂŁo disponĂveis no plano Collect, começando em ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mĂŞs.` : `por membro ativo por mĂŞs.`}`,
},
+ auditor: {
+ title: 'Auditor',
+ description: 'Os auditores tĂŞm acesso somente de leitura a todos os relatĂłrios para visibilidade total e monitoramento de conformidade.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Os auditores estĂŁo disponĂveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mĂŞs.` : `por membro ativo por mĂŞs.`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'VĂĄrios nĂveis de aprovação',
description:
@@ -6202,7 +6208,7 @@ ${amount} para ${merchant} - ${date}`,
searchResults: {
emptyResults: {
title: 'Nada para mostrar',
- subtitle: `Tente ajustar seus critĂŠrios de busca ou criar algo com o botĂŁo verde ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}.`,
+ subtitle: `Tente ajustar seus critĂŠrios de busca ou criar algo com o botĂŁo +.`,
},
emptyExpenseResults: {
title: 'VocĂŞ ainda nĂŁo criou nenhuma despesa ainda',
@@ -7388,6 +7394,28 @@ ${amount} para ${merchant} - ${date}`,
subtitle: `NĂŁo conseguimos carregar todos os seus dados. Fomos notificados e estamos investigando o problema. Se isso persistir, entre em contato com`,
refreshAndTryAgain: 'Atualize e tente novamente',
},
+ domain: {
+ notVerified: 'NĂŁo verificado',
+ retry: 'Tentar novamente',
+ verifyDomain: {
+ title: 'Verificar domĂnio',
+ beforeProceeding: ({domainName}: {domainName: string}) =>
+ `Antes de prosseguir, verifique se você Ê o proprietårio de ${domainName} atualizando as configuraçþes de DNS.`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `Acesse seu provedor de DNS e abra as configuraçþes de DNS para ${domainName}.`,
+ addTXTRecord: 'Adicione o seguinte registro TXT:',
+ saveChanges: 'Salve as alteraçþes e volte aqui para verificar seu domĂnio.',
+ youMayNeedToConsult: `Talvez seja necessårio consultar o departamento de TI da sua organização para concluir a verificação. Saiba mais.`,
+ warning: 'ApĂłs a verificação, todos os membros do Expensify no seu domĂnio receberĂŁo um e-mail informando que suas contas serĂŁo gerenciadas sob seu domĂnio.',
+ codeFetchError: 'NĂŁo foi possĂvel obter o cĂłdigo de verificação',
+ genericError: 'NĂŁo conseguimos verificar seu domĂnio. Tente novamente e entre em contato com o Concierge se o problema persistir.',
+ },
+ domainVerified: {
+ title: 'DomĂnio verificado',
+ header: 'Uhul! Seu domĂnio foi verificado',
+ description: ({domainName}: {domainName: string}) =>
+ `O domĂnio ${domainName} foi verificado com sucesso e agora vocĂŞ pode configurar SAML e outros recursos de segurança.`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
// so if you change it here, please update it there as well.
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 73f88ff1671a2..31f4e8aa1f6b5 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -442,6 +442,9 @@ const translations = {
zipPostCode: 'éŽćżçźç ',
whatThis: 'čżćŻäťäšďź',
iAcceptThe: 'ććĽĺ',
+ acceptTermsAndPrivacy: `ććĽĺ Expensify ćĺĄćĄćŹž ĺ éç§ćżç`,
+ acceptTermsAndConditions: `ććĽĺ ćĄćŹžĺćĄäťś`,
+ acceptTermsOfService: `ććĽĺ Expensify ćĺĄćĄćŹž`,
remove: 'ç§ťé¤',
admin: '玥çĺ',
owner: 'ććč ',
@@ -924,17 +927,17 @@ const translations = {
beginningOfChatHistoryUserRoom: ({reportName, reportDetailsLink}: BeginningOfChatHistoryUserRoomParams) =>
`ćŹč夊厤ç¨äşä¸ ${reportName} ćĺ łçäťťä˝ĺ 厚ă`,
beginningOfChatHistoryInvoiceRoom: ({invoicePayer, invoiceReceiver}: BeginningOfChatHistoryInvoiceRoomParams) =>
- `诼č夊ç¨äş ${invoicePayer} ĺ ${invoiceReceiver} äšé´çĺ缨ăä˝żç¨ ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} ćéŽĺéĺ缨ă`,
+ `诼č夊ç¨äş ${invoicePayer} ĺ ${invoiceReceiver} äšé´çĺ缨ăä˝żç¨ + ćéŽĺéĺ缨ă`,
beginningOfChatHistory: 'ć¤č夊ćŻä¸',
beginningOfChatHistoryPolicyExpenseChat: ({workspaceName, submitterDisplayName}: BeginningOfChatHistoryPolicyExpenseChatParams) =>
- `čżćŻ${submitterDisplayName} ĺ${workspaceName} ć交贚ç¨çĺ°ćšăä˝żç¨ ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} ćéŽĺłĺŻă`,
+ `čżćŻ${submitterDisplayName} ĺ${workspaceName} ć交贚ç¨çĺ°ćšăä˝żç¨ + ćéŽĺłĺŻă`,
beginningOfChatHistorySelfDM: 'čżćŻć¨ç个人犺é´ăç¨äşčްĺ˝çŹčްăäťťĺĄăč稿ĺćéă',
beginningOfChatHistorySystemDM: '揢čżďźčŽŠć䝏为ć¨čżčĄčŽžç˝Žă',
chatWithAccountManager: 'ĺ¨čżéä¸ć¨ç厢ćˇçťçč夊',
sayHello: 'čŻ´ä˝ ĺĽ˝ďź',
yourSpace: 'ć¨ç犺é´',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `揢čżćĽĺ°${roomName}ďź`,
- usePlusButton: ({additionalText}: UsePlusButtonParams) => ` ä˝żç¨ ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} ćéŽ${additionalText}ä¸çŹč´šç¨ă`,
+ usePlusButton: ({additionalText}: UsePlusButtonParams) => ` ä˝żç¨ + ćéŽ${additionalText}ä¸çŹč´šç¨ă`,
askConcierge: 'éćśćéŽĺšśčˇĺžĺ ¨ĺ¤ŠĺĺŽćśćŻćă',
conciergeSupport: '24/7 ćŻć',
create: 'ĺĺťş',
@@ -2213,10 +2216,9 @@ ${merchant}ç${amount} - ${date}`,
},
reportDetailsPage: {
inWorkspace: ({policyName}: ReportPolicyNameParams) => `ĺ¨${policyName}ä¸`,
- generatingPDF: 'çćPDF',
+ generatingPDF: 'çćPDF...',
waitForPDF: '诡ç¨ĺďźć䝏ćŁĺ¨çć PDFă',
errorPDF: 'çćPDFćśĺşç°é误ă',
- generatedPDF: 'ć¨çćĽĺ PDF 塲çćďź',
},
reportDescriptionPage: {
roomDescription: 'ćżé´ćčż°',
@@ -2592,10 +2594,8 @@ ${merchant}ç${amount} - ${date}`,
messages: {
onboardingEmployerOrSubmitMessage: 'ćĽéĺ°ąĺĺéćśćŻä¸ć ˇçŽĺă莊ć䝏ćĽççĺşćŹçĽčŻă',
onboardingPersonalSpendMessage: '䝼ä¸ćŻĺŚä˝ĺ¨ĺ 揥çšĺťä¸čˇč¸Şć¨çćŻĺşă',
- onboardingManageTeamMessage: ({hasIntroSelected}: {hasIntroSelected: boolean}) =>
- hasIntroSelected
- ? '# ć¨çĺ č´ščŻç¨ĺˇ˛çťĺźĺ§ďźčŽŠć䝏帎ć¨ĺŽć莞罎ă\nđ ć¨ĺĽ˝ďźććŻć¨ç Expensify 莞罎ä¸ĺăç°ĺ¨ć¨ĺˇ˛çťĺĺťşäşä¸ä¸ŞĺˇĽä˝ĺşďźčݎĺ ĺĺŠç¨ 30 夊ĺ č´ščŻç¨ďźĺšśćç §ä¸é˘çćĽéޤćä˝ďź'
- : '# ć¨çĺ č´ščŻç¨ĺˇ˛çťĺźĺ§ďźčŽŠć䝏帎ć¨ĺŽć莞罎ă\nđ ć¨ĺĽ˝ďźććŻć¨ç Expensify 莞çťä¸ĺăć塲çťĺĺťşäşä¸ä¸ŞĺˇĽä˝ĺşďźç¨äşĺ¸ŽĺŠçŽĄçć¨ĺ˘éçćśćŽĺč´šç¨ă为äşĺ ĺĺŠç¨ 30 夊ĺ č´ščŻç¨ďźčݎćç §ä¸é˘çĺŠä˝ćĽéޤćä˝ďź',
+ onboardingManageTeamMessage:
+ '# ć¨çĺ č´ščŻç¨ĺˇ˛çťĺźĺ§ďźčŽŠć䝏帎ć¨ĺŽć莞罎ă\nđ ć¨ĺĽ˝ďźććŻć¨ç Expensify 莞罎ä¸ĺăç°ĺ¨ć¨ĺˇ˛çťĺĺťşäşä¸ä¸ŞĺˇĽä˝ĺşďźčݎĺ ĺĺŠç¨ 30 夊ĺ č´ščŻç¨ďźĺšśćç §ä¸é˘çćĽéޤćä˝ďź',
onboardingTrackWorkspaceMessage:
'# 莊ć䝏ćĽčŽžç˝Žć¨çĺ¸ćˇ\nð ććĽĺ¸Žĺżäşďźä¸şäşĺ¸ŽĺŠć¨ĺźĺ§ďźć塲为个ä˝çťčĽč ĺçąťäźźäźä¸é躍ĺŽĺśäşć¨ç塼ä˝ĺşčŽžç˝Žăć¨ĺŻäťĽéčżçšĺťä¸é˘çéžćĽćĽč°ć´ć¨ç塼ä˝ĺşďź\n\n䝼ä¸ćŻĺŚä˝ĺ¨ĺ 揥çšĺťä¸čˇč¸Şć¨çćŻĺşďź',
onboardingChatSplitMessage: 'ä¸ćĺĺćč´Śĺĺ°ąĺĺéćśćŻä¸ć ˇçŽĺă䝼ä¸ćŻćšćłă',
@@ -4486,9 +4486,7 @@ ${merchant}ç${amount} - ${date}`,
cardholder: 'ćĺĄäşş',
card: 'ĺĄç',
cardName: 'ĺĄçĺç§°',
- brokenConnectionErrorFirstPart: `ĺĄç俥ćŻćľčżćĽĺˇ˛ćĺźă诡`,
- brokenConnectionErrorLink: 'çťĺ˝ć¨çéśčĄč´Śćˇ',
- brokenConnectionErrorSecondPart: '䝼䞿ć䝏ĺŻäťĽéć°ĺťşçŤčżćĽă',
+ brokenConnectionError: 'ĺĄç俥ćŻćľčżćĽĺˇ˛ćĺźă诡 çťĺ˝ć¨çéśčĄč´Śćˇ 䝼䞿ć䝏ĺŻäťĽéć°ĺťşçŤčżćĽă',
assignedCard: ({assignee, link}: AssignedCardParams) => `塲ĺé ${assignee}ä¸ä¸Ş${link}ďźĺŻźĺ Ľç交ćĺ°ćžç¤şĺ¨ć¤č夊ä¸ă`,
companyCard: 'ĺ Źĺ¸ĺĄ',
chooseCardFeed: 'éćŠĺĄç俥ćŻćľ',
@@ -4538,6 +4536,7 @@ ${merchant}ç${amount} - ${date}`,
monthly: 'ćŻć',
},
cardDetails: 'ĺĄç诌ć ',
+ cardPending: ({name}: {name: string}) => `ĺĄççŽĺĺž ĺ¤çďźĺ°ĺ¨éŞčŻ${name}çč´Śćˇĺĺćžă`,
virtual: 'Virtual',
physical: 'çŠçç',
deactivate: 'ĺç¨ĺĄç',
@@ -4717,9 +4716,8 @@ ${merchant}ç${amount} - ${date}`,
noAccountsFound: 'ćŞćžĺ°č´Śćˇ',
defaultCard: 'éťčޤĺĄç',
downgradeTitle: `ć ćłé级塼ä˝ĺş`,
- downgradeSubTitleFirstPart: `çąäşčżćĽäşĺ¤ä¸ŞĺĄçéŚéďźä¸ĺ ćŹExpensifyĺĄďźďźć¤ĺˇĽä˝ĺşć ćłéçş§ă诡`,
- downgradeSubTitleMiddlePart: `äť äżçä¸ä¸ŞĺĄç俥ćŻćľ`,
- downgradeSubTitleLastPart: 'çť§çťă',
+ downgradeSubTitle: `çąäşčżćĽäşĺ¤ä¸ŞĺĄçéŚéďźä¸ĺ ćŹExpensifyĺĄďźďźć¤ĺˇĽä˝ĺşć ćłéçş§ă诡 äť äżçä¸ä¸ŞĺĄç俥ćŻćľ çť§çťă`,
+
noAccountsFoundDescription: ({connection}: ConnectionParams) => `诡ĺ¨${connection}ä¸ćˇťĺ č´Śćˇĺšśĺ揥ĺćĽčżćĽă`,
expensifyCardBannerTitle: 'čˇĺExpensifyĺĄ',
expensifyCardBannerSubtitle: '亍ĺćŻçŹçžĺ˝ćśč´šçç°éčżčżďźExpensifyč´ŚĺćéŤĺŻäşŤ50%ććŁďźć éčćĺĄçć´ĺ¤äźć ă',
@@ -4843,8 +4841,9 @@ ${merchant}ç${amount} - ${date}`,
existingReportFieldNameError: 'ĺ ˇćć¤ĺç§°çćĽčĄ¨ĺ掾塲ĺĺ¨',
reportFieldNameRequiredError: '诡čžĺ ĽćĽĺĺ掾ĺç§°',
reportFieldTypeRequiredError: '诡éćŠćĽĺĺ掾繝ĺ',
+ circularReferenceError: '诼ĺ掾ä¸č˝ĺźç¨čŞčşŤă诡ć´ć°ă',
reportFieldInitialValueRequiredError: '诡éćŠćĽĺĺ掾çĺĺ§ĺź',
- genericFailureMessage: 'ć´ć°ćĽĺĺ掾ćśĺçé误ă诡ĺčŻä¸ćŹĄă',
+ genericFailureMessage: 'ć´ć°ćĽĺĺ掾ćśĺ çé误ă诡ĺčŻä¸ćŹĄă',
},
tags: {
tagName: 'ć çžĺç§°',
@@ -5394,8 +5393,7 @@ ${merchant}ç${amount} - ${date}`,
enableRate: 'ĺŻç¨č´šç',
status: 'çść',
unit: 'ĺä˝',
- taxFeatureNotEnabledMessage: 'čŚä˝żç¨ć¤ĺč˝ďźĺż 饝ĺ¨ĺˇĽä˝ĺşĺŻç¨ç¨č´šăĺĺž',
- changePromptMessage: 'čżčĄčŻĽć´ćšă',
+ taxFeatureNotEnabledMessage: 'čŚä˝żç¨ć¤ĺč˝ďźĺż 饝ĺ¨ĺˇĽä˝ĺşĺŻç¨ç¨č´šăĺĺž ć´ĺ¤ĺč˝ čżčĄčŻĽć´ćšă',
deleteDistanceRate: 'ĺ é¤čˇçŚťč´šç',
areYouSureDelete: () => ({
one: 'ć¨çĄŽĺŽčŚĺ é¤ć¤č´šçĺďź',
@@ -5589,6 +5587,12 @@ ${merchant}ç${amount} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`ĺ¨ Collect 莥ĺä¸ćäžçčˇçŚťč´šçďźčľˇäťˇä¸ş ${formattedPrice} ${hasTeam2025Pricing ? `ćŻä˝ćĺćŻćă` : `ćŻä˝ć´ťčˇćĺćŻćă`}`,
},
+ auditor: {
+ title: '厥莥ĺ',
+ description: '厥莥ĺĺŻĺŻšćććĽĺčżčĄĺŞčŻťčŽżéŽďźäťĽĺŽç°ĺ ¨é˘ĺŻč§ć§ĺĺč§çć§ă',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `厥莥ĺäť ĺ¨ Control 莥ĺä¸ćäžďźčľˇäťˇä¸ş ${formattedPrice} ${hasTeam2025Pricing ? `ćŻä˝ćĺćŻćă` : `ćŻä˝ć´ťčˇćĺćŻćă`}`,
+ },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'ĺ¤çş§ĺŽĄćš',
description: 'ĺ¤çş§ĺŽĄćšćŻä¸ç§ĺˇĽä˝ćľĺˇĽĺ ˇďźéç¨äşčŚćąä¸äşşäťĽä¸ĺŽĄćšćĽéĺĺćč˝čżčĄćĽéçĺ Źĺ¸ă',
@@ -6072,7 +6076,7 @@ ${merchant}ç${amount} - ${date}`,
searchResults: {
emptyResults: {
title: 'ć ĺ 厚ćžç¤ş',
- subtitle: `ĺ°čŻč°ć´ć¨çćç´˘ćĄäťść使ç¨çťżč˛ç ${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} ćéŽĺĺťşĺ 厚ă`,
+ subtitle: `ĺ°čŻč°ć´ć¨çćç´˘ćĄäťśćä˝żç¨ + ćéŽĺĺťşĺ 厚ă`,
},
emptyExpenseResults: {
title: 'ć¨čżć˛Ąćĺĺťşäťťä˝č´šç¨',
@@ -7222,6 +7226,27 @@ ${merchant}ç${amount} - ${date}`,
},
avatarPage: {title: 'çźčžä¸Şäşşčľćĺžç', upload: 'ä¸äź ', uploadPhoto: 'ä¸äź ç §ç', selectAvatar: 'éćŠĺ¤´ĺ', chooseCustomAvatar: 'ćéćŠčŞĺŽäšĺ¤´ĺ'},
openAppFailureModal: {title: 'ĺşäşçšéŽé˘...', subtitle: `ć䝏ćŞč˝ĺ č˝˝ć¨çććć°ćŽăć䝏塲ćśĺ°éçĽďźćŁĺ¨č°ćĽć¤éŽé˘ăĺŚćéŽé˘äťçśĺĺ¨ďźčݎčçłť`, refreshAndTryAgain: 'ĺˇć°ĺšśéčŻ'},
+ domain: {
+ notVerified: 'ćŞéŞčŻ',
+ retry: 'éčŻ',
+ verifyDomain: {
+ title: 'éŞčŻĺĺ',
+ beforeProceeding: ({domainName}: {domainName: string}) => `ĺ¨çť§çťäšĺďźčݎéčżć´ć°ĺ ś DNS 莞罎ćĽéŞčŻć¨ćĽć ${domainName}ă`,
+ accessYourDNS: ({domainName}: {domainName: string}) => `莿éŽć¨ç DNS ćäžĺďźĺšśćĺź ${domainName} ç DNS 莞罎ă`,
+ addTXTRecord: '桝ĺ äťĽä¸ TXT 莰ĺ˝ďź',
+ saveChanges: 'äżĺć´ćšĺšśčżĺć¤ĺ¤äťĽéŞčŻć¨çĺĺă',
+ youMayNeedToConsult: `ć¨ĺŻč˝éčŚĺ¨čŻ˘ć¨çťçťç IT é¨é¨äťĽĺŽćéŞčŻăäşč§Łć´ĺ¤ă`,
+ warning: 'éŞčŻĺŽćĺďźć¨çĺä¸çćć Expensify ćĺĺ°ćśĺ°ä¸ĺ°çľĺéŽäťśďźĺçĽäťäťŹçč´Śćˇĺ°çąć¨çĺčżčĄçŽĄçă',
+ codeFetchError: 'ć ćłčˇĺéŞčŻç ',
+ genericError: 'ć䝏ć ćłéŞčŻć¨çĺĺă诡éčŻďźĺŚćéŽé˘äťçśĺĺ¨ďźčݎčçłť Conciergeă',
+ },
+ domainVerified: {
+ title: 'ĺĺ塲éŞčŻ',
+ header: 'ĺĺŚďźć¨çĺĺ塲éčżéŞčŻ',
+ description: ({domainName}: {domainName: string}) =>
+ `ĺĺ ${domainName} 塲ćĺéŞčŻďźć¨ç°ĺ¨ĺŻäťĽčŽžç˝Ž SAML ĺĺ śäťĺŽĺ ¨ĺč˝ă`,
+ },
+ },
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
// so if you change it here, please update it there as well.
diff --git a/src/libs/API/parameters/CreateWorkspaceParams.ts b/src/libs/API/parameters/CreateWorkspaceParams.ts
index 6836b599ae750..83da89bc57c01 100644
--- a/src/libs/API/parameters/CreateWorkspaceParams.ts
+++ b/src/libs/API/parameters/CreateWorkspaceParams.ts
@@ -18,6 +18,7 @@ type CreateWorkspaceParams = {
userReportedIntegration?: string;
memberData?: string;
features?: string;
+ areDistanceRatesEnabled?: boolean;
};
export default CreateWorkspaceParams;
diff --git a/src/libs/API/parameters/DomainParams.ts b/src/libs/API/parameters/DomainParams.ts
new file mode 100644
index 0000000000000..38fa1ad8780a4
--- /dev/null
+++ b/src/libs/API/parameters/DomainParams.ts
@@ -0,0 +1,5 @@
+type DomainParams = {
+ domainName: string;
+};
+
+export default DomainParams;
diff --git a/src/libs/API/parameters/RejectMoneyRequestParams.ts b/src/libs/API/parameters/RejectMoneyRequestParams.ts
index a49c17bac363e..193b78a479dba 100644
--- a/src/libs/API/parameters/RejectMoneyRequestParams.ts
+++ b/src/libs/API/parameters/RejectMoneyRequestParams.ts
@@ -3,8 +3,12 @@ type RejectMoneyRequestParams = {
reportID: string;
comment: string;
rejectedToReportID?: string;
+ reportPreviewReportActionID?: string;
rejectedActionReportActionID: string;
rejectedCommentReportActionID: string;
+ createdIOUReportActionID?: string;
+ expenseMovedReportActionID?: string;
+ expenseCreatedReportActionID?: string;
};
export default RejectMoneyRequestParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index e1f7fb3a1c4d1..9bc2d39d5b2b5 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -437,3 +437,4 @@ export type {default as AddReportApproverParams} from './AddReportApproverParams
export type {default as EnableGlobalReimbursementsForUSDBankAccountParams} from './EnableGlobalReimbursementsForUSDBankAccountParams';
export type {default as SendReminderForCorpaySignerInformationParams} from './SendReminderForCorpaySignerInformationParams';
export type {default as SendScheduleCallNudgeParams} from './SendScheduleCallNudge';
+export type {default as DomainParams} from './DomainParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index e3ea06cb6cfa7..c3a659e47c255 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -516,6 +516,7 @@ const WRITE_COMMANDS = {
ADD_REPORT_APPROVER: 'AddReportApprover',
REQUEST_UNLOCK_ACCOUNT: 'RequestUnlockAccount',
SEND_SCHEDULE_CALL_NUDGE: 'SendScheduleCallNudge',
+ VALIDATE_DOMAIN: 'ValidateDomain',
} as const;
type WriteCommand = ValueOf;
@@ -1051,6 +1052,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.ASSIGN_REPORT_TO_ME]: Parameters.AssignReportToMeParams;
[WRITE_COMMANDS.ADD_REPORT_APPROVER]: Parameters.AddReportApproverParams;
[WRITE_COMMANDS.REQUEST_UNLOCK_ACCOUNT]: Parameters.LockAccountParams;
+
+ // Domain API
+ [WRITE_COMMANDS.VALIDATE_DOMAIN]: Parameters.DomainParams;
};
const READ_COMMANDS = {
@@ -1127,6 +1131,7 @@ const READ_COMMANDS = {
OPEN_UNREPORTED_EXPENSES_PAGE: 'OpenUnreportedExpensesPage',
GET_GUIDE_CALL_AVAILABILITY_SCHEDULE: 'GetGuideCallAvailabilitySchedule',
GET_TRANSACTIONS_FOR_MERGING: 'GetTransactionsForMerging',
+ GET_DOMAIN_VALIDATE_CODE: 'GetDomainValidateCode',
} as const;
type ReadCommand = ValueOf;
@@ -1205,6 +1210,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_UNREPORTED_EXPENSES_PAGE]: Parameters.OpenUnreportedExpensesPageParams;
[READ_COMMANDS.GET_GUIDE_CALL_AVAILABILITY_SCHEDULE]: Parameters.GetGuideCallAvailabilityScheduleParams;
[READ_COMMANDS.GET_TRANSACTIONS_FOR_MERGING]: Parameters.GetTransactionsForMergingParams;
+ [READ_COMMANDS.GET_DOMAIN_VALIDATE_CODE]: Parameters.DomainParams;
};
const SIDE_EFFECT_REQUEST_COMMANDS = {
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index 5abc29ee9a54e..652e0cfeeb811 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -529,6 +529,7 @@ function isSelectedFeedExpired(directFeed: DirectCardFeedData | undefined): bool
/** Returns list of cards which can be assigned */
function getFilteredCardList(list: WorkspaceCardsList | undefined, directFeed: DirectCardFeedData | undefined, workspaceCardFeeds: OnyxCollection) {
const {cardList: customFeedCardsToAssign, ...cards} = list ?? {};
+ // eslint-disable-next-line unicorn/prefer-set-has
const assignedCards = Object.values(cards).map((card) => card.cardName);
// Get cards assigned across all workspaces
@@ -576,6 +577,7 @@ function checkIfNewFeedConnected(prevFeedsData: CompanyFeeds, currentFeedsData:
}
function filterInactiveCards(cards: CardList | undefined): CardList {
+ // eslint-disable-next-line unicorn/prefer-set-has
const closedStates: number[] = [CONST.EXPENSIFY_CARD.STATE.CLOSED, CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED, CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED];
return filterObject(cards ?? {}, (key, card) => !closedStates.includes(card.state));
}
diff --git a/src/libs/CategoryOptionListUtils.ts b/src/libs/CategoryOptionListUtils.ts
index 0124f7dc610b8..be2569e8447b1 100644
--- a/src/libs/CategoryOptionListUtils.ts
+++ b/src/libs/CategoryOptionListUtils.ts
@@ -1,15 +1,13 @@
// eslint-disable-next-line you-dont-need-lodash-underscore/get
import lodashGet from 'lodash/get';
import lodashSet from 'lodash/set';
-import type {LocaleContextProps} from '@components/LocaleContextProvider';
+import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider';
import CONST from '@src/CONST';
import type {PolicyCategories} from '@src/types/onyx';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import times from '@src/utils/times';
import {getDecodedCategoryName} from './CategoryUtils';
-// eslint-disable-next-line @typescript-eslint/no-deprecated
-import {translateLocal} from './Localize';
import type {OptionTree, SectionBase} from './OptionsListUtils';
import tokenizedSearch from './tokenizedSearch';
@@ -94,6 +92,7 @@ function getCategoryListSections({
selectedOptions = [],
recentlyUsedCategories = [],
maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
+ translate,
}: {
categories: PolicyCategories;
localeCompare: LocaleContextProps['localeCompare'];
@@ -101,9 +100,11 @@ function getCategoryListSections({
searchValue?: string;
recentlyUsedCategories?: string[];
maxRecentReportsToShow?: number;
+ translate: LocalizedTranslate;
}): CategoryTreeSection[] {
const sortedCategories = sortCategories(categories, localeCompare);
const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled);
+ // eslint-disable-next-line unicorn/prefer-set-has
const enabledCategoriesNames = enabledCategories.map((category) => category.name);
const selectedOptionsWithDisabledState: Category[] = [];
const categorySections: CategoryTreeSection[] = [];
@@ -162,6 +163,7 @@ function getCategoryListSections({
});
}
+ // eslint-disable-next-line unicorn/prefer-set-has
const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name);
const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name));
@@ -194,8 +196,7 @@ function getCategoryListSections({
const data = getCategoryOptionTree(cutRecentlyUsedCategories, true);
categorySections.push({
// "Recent" section
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- title: translateLocal('common.recent'),
+ title: translate('common.recent'),
shouldShow: true,
data,
indexOffset: data.length,
@@ -205,8 +206,7 @@ function getCategoryListSections({
const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState);
categorySections.push({
// "All" section when items amount more than the threshold
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- title: translateLocal('common.all'),
+ title: translate('common.all'),
shouldShow: true,
data,
indexOffset: data.length,
diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts
index 39616f800d7b8..fb87060ed1fae 100644
--- a/src/libs/CategoryUtils.ts
+++ b/src/libs/CategoryUtils.ts
@@ -103,6 +103,13 @@ function isCategoryMissing(category: string | undefined): boolean {
return emptyCategories.includes(category ?? '');
}
+function isCategoryDescriptionRequired(policyCategories: PolicyCategories | undefined, category: string | undefined, areRulesEnabled: boolean | undefined): boolean {
+ if (!policyCategories || !category || !areRulesEnabled) {
+ return false;
+ }
+ return !!policyCategories[category]?.areCommentsRequired;
+}
+
function getDecodedCategoryName(categoryName: string) {
return Str.htmlDecode(categoryName);
}
@@ -116,5 +123,6 @@ export {
updateCategoryInMccGroup,
getEnabledCategoriesCount,
isCategoryMissing,
+ isCategoryDescriptionRequired,
getDecodedCategoryName,
};
diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts
index fc5139ec96fcb..80830b0579ebc 100644
--- a/src/libs/DebugUtils.ts
+++ b/src/libs/DebugUtils.ts
@@ -926,7 +926,6 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
}
switch (key) {
case 'reportID':
- case 'reportName':
case 'currency':
case 'tag':
case 'category':
@@ -1051,7 +1050,6 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
participants: CONST.RED_BRICK_ROAD_PENDING_ACTION,
receipt: CONST.RED_BRICK_ROAD_PENDING_ACTION,
reportID: CONST.RED_BRICK_ROAD_PENDING_ACTION,
- reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION,
routes: CONST.RED_BRICK_ROAD_PENDING_ACTION,
transactionID: CONST.RED_BRICK_ROAD_PENDING_ACTION,
tag: CONST.RED_BRICK_ROAD_PENDING_ACTION,
diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx
index ba0c73ba0eca0..7658da6c6a44c 100644
--- a/src/libs/EmojiUtils.tsx
+++ b/src/libs/EmojiUtils.tsx
@@ -436,6 +436,17 @@ function suggestEmojis(text: string, locale: Locale = CONST.LOCALES.DEFAULT, lim
return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData));
}
+/**
+ * Retrieve preferredSkinTone as Number to prevent legacy 'default' String value
+ */
+const getPreferredSkinToneIndex = (value: OnyxEntry): number => {
+ if (value !== null && Number.isInteger(Number(value))) {
+ return Number(value);
+ }
+
+ return CONST.EMOJI_DEFAULT_SKIN_TONE;
+};
+
/**
* Given an emoji object it returns the correct emoji code
* based on the users preferred skin tone.
@@ -675,6 +686,7 @@ export {
replaceEmojis,
suggestEmojis,
getEmojiCodeWithSkinColor,
+ getPreferredSkinToneIndex,
getPreferredEmojiCode,
getUniqueEmojiCodes,
getEmojiReactionDetails,
diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts
index abe4533d1f7a9..a2a76551dbece 100644
--- a/src/libs/ExportOnyxState/common.ts
+++ b/src/libs/ExportOnyxState/common.ts
@@ -75,6 +75,7 @@ const ONYX_KEY_EXPORT_RULES: Record = {
},
};
+// eslint-disable-next-line unicorn/prefer-set-has
const onyxKeysToRemove: Array> = [
ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID,
ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID,
@@ -85,6 +86,7 @@ const onyxKeysToRemove: Array> = [
ONYXKEYS.ONFIDO_APPLICANT_ID,
];
+// eslint-disable-next-line unicorn/prefer-set-has
const keysToMask = [
'addressCity',
'addressName',
@@ -130,8 +132,10 @@ const keysToMask = [
'zipCode',
];
+// eslint-disable-next-line unicorn/prefer-set-has
const amountKeysToRandomize = ['amount', 'modifiedAmount', 'originalAmount', 'total', 'unheldTotal', 'unheldNonReimbursableTotal', 'nonReimbursableTotal'];
+// eslint-disable-next-line unicorn/prefer-set-has
const nodesToFullyMask = ['reservationList'];
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts
index ddd9f5b963260..47f0fadd1590e 100644
--- a/src/libs/Formula.ts
+++ b/src/libs/Formula.ts
@@ -1,11 +1,13 @@
+import {endOfDay, endOfMonth, endOfWeek, getDay, lastDayOfMonth, set, startOfMonth, startOfWeek, subDays} from 'date-fns';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {Policy, Report, Transaction} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {getCurrencySymbol} from './CurrencyUtils';
import {formatDate} from './FormulaDatetime';
import {getAllReportActions} from './ReportActionsUtils';
-import {getReportTransactions} from './ReportUtils';
+import {getMoneyRequestSpendBreakdown, getReportTransactions} from './ReportUtils';
import {getCreated, isPartialTransaction} from './TransactionUtils';
type FormulaPart = {
@@ -28,6 +30,8 @@ type FormulaContext = {
transaction?: Transaction;
};
+type FieldList = Record;
+
const FORMULA_PART_TYPES = {
REPORT: 'report',
FIELD: 'field',
@@ -189,6 +193,70 @@ function parsePart(definition: string): FormulaPart {
return part;
}
+/**
+ * Check if the report field formula value is containing circular references, e.g example: A -> A, A->B->A, A->B->C->A, etc
+ */
+function hasCircularReferences(fieldValue: string, fieldName: string, fieldList?: FieldList): boolean {
+ const formulaValues = extract(fieldValue);
+ if (formulaValues.length === 0 || isEmptyObject(fieldList)) {
+ return false;
+ }
+
+ const visitedLists = new Set();
+ const fieldsByName = new Map(Object.values(fieldList).map((field) => [field.name, field]));
+
+ // Helper function to check if a field has circular references
+ const hasCircularReferencesRecursive = (currentFieldValue: string, currentFieldName: string): boolean => {
+ // If we've already visited this field in the current path, return true
+ if (visitedLists.has(currentFieldName)) {
+ return true;
+ }
+
+ // Add current field to the visited lists
+ visitedLists.add(currentFieldName);
+
+ // Extract all formula values from the current field
+ const currentFormulaValues = extract(currentFieldValue);
+
+ for (const formula of currentFormulaValues) {
+ const part = parsePart(formula);
+
+ // Only check field references (skip report, user, or freetext)
+ if (part.type !== FORMULA_PART_TYPES.FIELD) {
+ continue;
+ }
+
+ // Get the referenced field name (first element in fieldPath)
+ const referencedFieldName = part.fieldPath.at(0)?.trim();
+ if (!referencedFieldName) {
+ continue;
+ }
+
+ // Check if this reference creates a cycle
+ if (referencedFieldName === fieldName || visitedLists.has(referencedFieldName)) {
+ visitedLists.delete(currentFieldName);
+ return true;
+ }
+
+ const referencedField = fieldsByName.get(referencedFieldName);
+
+ if (referencedField?.defaultValue) {
+ // Recursively check the referenced field
+ if (hasCircularReferencesRecursive(referencedField.defaultValue, referencedFieldName)) {
+ visitedLists.delete(currentFieldName);
+ return true;
+ }
+ }
+ }
+
+ // Remove current field from visited lists
+ visitedLists.delete(currentFieldName);
+ return false;
+ };
+
+ return hasCircularReferencesRecursive(fieldValue, fieldName);
+}
+
/**
* Compute the value of a formula given a context
*/
@@ -233,6 +301,28 @@ function compute(formula?: string, context?: FormulaContext): string {
return result;
}
+/**
+ * Compute auto-reporting info for a report formula part
+ */
+function computeAutoReportingInfo(part: FormulaPart, context: FormulaContext, subField: string | undefined, format: string | undefined): string {
+ const {report, policy} = context;
+
+ if (!subField) {
+ return part.definition;
+ }
+
+ const {startDate, endDate} = getAutoReportingDates(policy, report);
+
+ switch (subField.toLowerCase()) {
+ case 'start':
+ return formatDate(startDate?.toISOString(), format);
+ case 'end':
+ return formatDate(endDate?.toISOString(), format);
+ default:
+ return part.definition;
+ }
+}
+
/**
* Compute the value of a report formula part
*/
@@ -256,6 +346,8 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string {
return formatDate(getNewestTransactionDate(report.reportID, context), format);
case 'total':
return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency);
+ case 'reimbursable':
+ return formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, getCurrencySymbol(report.currency ?? '') ?? report.currency);
case 'currency':
return report.currency ?? '';
case 'policyname':
@@ -265,6 +357,12 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string {
// Backend will always return at least one report action (of type created) and its date is equal to report's creation date
// We can make it slightly more efficient in the future by ensuring report.created is always present in backend's responses
return formatDate(getOldestReportActionDate(report.reportID), format);
+ case 'autoreporting': {
+ const subField = additionalPath.at(0);
+ // For multi-part formulas, format is everything after the subfield
+ const autoReportingFormat = additionalPath.length > 1 ? additionalPath.slice(1).join(':') : undefined;
+ return computeAutoReportingInfo(part, context, subField, autoReportingFormat);
+ }
default:
return part.definition;
}
@@ -482,6 +580,139 @@ function getOldestTransactionDate(reportID: string, context?: FormulaContext): s
return oldestDate;
}
+/**
+ * Calculate monthly reporting period for a specific day offset
+ */
+function getMonthlyReportingPeriod(currentDate: Date, offsetDay: number): {startDate: Date; endDate: Date} {
+ const currentDay = currentDate.getDate();
+ const currentYear = currentDate.getFullYear();
+ const currentMonth = currentDate.getMonth();
+
+ if (currentDay <= offsetDay) {
+ // We haven't reached the reporting day yet - period is from last month's offset+1 to this month's offset
+ const prevMonth = currentMonth - 1;
+ const prevYear = prevMonth < 0 ? currentYear - 1 : currentYear;
+ const adjustedPrevMonth = prevMonth < 0 ? 11 : prevMonth;
+
+ const prevMonthDays = lastDayOfMonth(new Date(prevYear, adjustedPrevMonth, 1)).getDate();
+ const prevOffsetDay = Math.min(offsetDay, prevMonthDays);
+
+ const currentMonthDays = lastDayOfMonth(currentDate).getDate();
+ const currentOffsetDay = Math.min(offsetDay, currentMonthDays);
+
+ return {
+ startDate: new Date(prevYear, adjustedPrevMonth, prevOffsetDay + 1, 0, 0, 0, 0),
+ endDate: new Date(currentYear, currentMonth, currentOffsetDay, 23, 59, 59, 999),
+ };
+ }
+
+ // We've passed the reporting day - period is from this month's offset+1 to next month's offset
+ const nextMonth = currentMonth + 1;
+ const nextYear = nextMonth > 11 ? currentYear + 1 : currentYear;
+ const adjustedNextMonth = nextMonth > 11 ? 0 : nextMonth;
+
+ const currentMonthDays = lastDayOfMonth(currentDate).getDate();
+ const currentOffsetDay = Math.min(offsetDay, currentMonthDays);
+
+ const nextMonthDays = lastDayOfMonth(new Date(nextYear, adjustedNextMonth, 1)).getDate();
+ const nextOffsetDay = Math.min(offsetDay, nextMonthDays);
+
+ return {
+ startDate: new Date(currentYear, currentMonth, currentOffsetDay + 1, 0, 0, 0, 0),
+ endDate: new Date(nextYear, adjustedNextMonth, nextOffsetDay, 23, 59, 59, 999),
+ };
+}
+
+/**
+ * Calculate monthly reporting period for last business day
+ */
+function getMonthlyLastBusinessDayPeriod(currentDate: Date): {startDate: Date; endDate: Date} {
+ let endDate = endOfMonth(currentDate);
+
+ // Move backward to find last business day (Mon-Fri)
+ while (getDay(endDate) === 0 || getDay(endDate) === 6) {
+ endDate = subDays(endDate, 1);
+ }
+
+ return {
+ startDate: startOfMonth(currentDate),
+ endDate: endOfDay(endDate),
+ };
+}
+
+/**
+ * Calculate the start and end dates for auto-reporting based on the frequency and current date
+ */
+function getAutoReportingDates(policy: OnyxEntry, report: Report, currentDate = new Date()): {startDate: Date | undefined; endDate: Date | undefined} {
+ const frequency = policy?.autoReportingFrequency;
+ const offset = policy?.autoReportingOffset;
+
+ // Return undefined if no frequency is set
+ if (!frequency || !policy) {
+ return {startDate: undefined, endDate: undefined};
+ }
+
+ let startDate: Date;
+ let endDate: Date;
+
+ switch (frequency) {
+ case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY: {
+ // Weekly: use the app's configured week start convention (Monday)
+ const weekStartsOn = CONST.WEEK_STARTS_ON;
+ startDate = startOfWeek(currentDate, {weekStartsOn});
+ endDate = endOfWeek(currentDate, {weekStartsOn});
+ break;
+ }
+
+ case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY: {
+ // Semi-monthly: 1st-15th or 16th-end of month
+ const dayOfMonth = currentDate.getDate();
+ if (dayOfMonth <= 15) {
+ startDate = startOfMonth(currentDate);
+ endDate = set(currentDate, {date: 15, hours: 23, minutes: 59, seconds: 59, milliseconds: 999});
+ } else {
+ startDate = set(currentDate, {date: 16, hours: 0, minutes: 0, seconds: 0, milliseconds: 0});
+ endDate = endOfMonth(currentDate);
+ }
+ break;
+ }
+
+ case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY: {
+ // Monthly reporting with different offset configurations
+ if (offset === CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH) {
+ const period = getMonthlyLastBusinessDayPeriod(currentDate);
+ startDate = period.startDate;
+ endDate = period.endDate;
+ } else if (typeof offset === 'number') {
+ const period = getMonthlyReportingPeriod(currentDate, offset);
+ startDate = period.startDate;
+ endDate = period.endDate;
+ } else {
+ // Default to full month
+ startDate = startOfMonth(currentDate);
+ endDate = endOfMonth(currentDate);
+ }
+ break;
+ }
+
+ case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP: {
+ // For trip-based, use oldest transaction as start
+ const oldestTransactionDateString = getOldestTransactionDate(report.reportID);
+ startDate = oldestTransactionDateString ? new Date(oldestTransactionDateString) : currentDate;
+ endDate = currentDate;
+ break;
+ }
+
+ default:
+ // For any other frequency, use current date as both start and end
+ startDate = currentDate;
+ endDate = currentDate;
+ break;
+ }
+
+ return {startDate, endDate};
+}
+
/**
* Get the date of the newest transaction for a given report
*/
@@ -518,6 +749,6 @@ function getNewestTransactionDate(reportID: string, context?: FormulaContext): s
return newestDate;
}
-export {FORMULA_PART_TYPES, compute, extract, parse};
+export {FORMULA_PART_TYPES, compute, extract, getAutoReportingDates, parse, hasCircularReferences};
-export type {FormulaContext, FormulaPart};
+export type {FormulaContext, FormulaPart, FieldList};
diff --git a/src/libs/FraudProtection/index.ts b/src/libs/FraudProtection/index.ts
index 1783f5441c967..330725cd590ea 100644
--- a/src/libs/FraudProtection/index.ts
+++ b/src/libs/FraudProtection/index.ts
@@ -25,6 +25,7 @@ Onyx.connectWithoutView({
callback: (account) => {
setAttribute('email', account?.primaryLogin ?? '');
setAttribute('mfa', account?.requiresTwoFactorAuth ? '2fa_enabled' : '2fa_disabled');
+ setAttribute('is_validated', account?.validated ? 'true' : 'false');
},
});
diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts
index 0f0cf0bea05af..188e31b7de9e7 100644
--- a/src/libs/Fullstory/index.ts
+++ b/src/libs/Fullstory/index.ts
@@ -88,7 +88,10 @@ const FS: Fullstory = {
anonymize: () => FullStory(CONST.FULLSTORY.OPERATION.SET_IDENTITY, {anonymous: true}),
- getSessionId: () => {
+ getSessionId: async () => {
+ if (!isInitialized()) {
+ return;
+ }
return FullStory('getSessionAsync', {format: 'id'});
},
};
diff --git a/src/libs/Fullstory/types.ts b/src/libs/Fullstory/types.ts
index 4c1901c8a43ba..e78490d48dce6 100644
--- a/src/libs/Fullstory/types.ts
+++ b/src/libs/Fullstory/types.ts
@@ -68,7 +68,7 @@ type Fullstory = {
anonymize: () => void;
/**
- * Returns the current Fullstory session ID.
+ * Returns the current FullStory session ID.
*/
getSessionId: () => Promise;
};
diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts
index 90ce7c50ceddf..28c441c4dcd20 100644
--- a/src/libs/HttpUtils.ts
+++ b/src/libs/HttpUtils.ts
@@ -42,6 +42,7 @@ abortControllerMap.set(ABORT_COMMANDS.SearchForReports, new AbortController());
/**
* The API commands that require the skew calculation
*/
+// eslint-disable-next-line unicorn/prefer-set-has
const addSkewList: string[] = [WRITE_COMMANDS.OPEN_REPORT, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, WRITE_COMMANDS.OPEN_APP];
/**
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index b8a28fe3b644f..3547b10ba9ff9 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -4,7 +4,6 @@ import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {OnyxInputOrEntry, PersonalDetails, Policy, Report} from '@src/types/onyx';
import type {Attendee} from '@src/types/onyx/IOU';
-import type {SearchPolicy} from '@src/types/onyx/SearchResults';
import SafeString from '@src/utils/SafeString';
import type {IOURequestType} from './actions/IOU';
import {getCurrencyUnit} from './CurrencyUtils';
@@ -192,7 +191,7 @@ function isMovingTransactionFromTrackExpense(action?: IOUAction) {
return false;
}
-function shouldShowReceiptEmptyState(iouType: IOUType, action: IOUAction, policy: OnyxInputOrEntry | SearchPolicy, isPerDiemRequest: boolean) {
+function shouldShowReceiptEmptyState(iouType: IOUType, action: IOUAction, policy: OnyxInputOrEntry, isPerDiemRequest: boolean) {
// Determine when to show the receipt empty state:
// - Show for pay, submit or track expense types
// - Hide for per diem requests
diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts
index b07d9ccdcc620..9a2f07326a541 100644
--- a/src/libs/LoginUtils.ts
+++ b/src/libs/LoginUtils.ts
@@ -35,10 +35,21 @@ function appendCountryCode(phone: string, countryCode: number): string {
* Check email is public domain or not
*/
function isEmailPublicDomain(email: string): boolean {
- const emailDomain = Str.extractEmailDomain(email).toLowerCase();
+ const emailDomain = getEmailDomain(email);
return PUBLIC_DOMAINS_SET.has(emailDomain);
}
+function isDomainPublic(domain: string): boolean {
+ return PUBLIC_DOMAINS_SET.has(domain);
+}
+
+/**
+ * Get the domain for an email
+ */
+function getEmailDomain(email: string): string {
+ return Str.extractEmailDomain(email).toLowerCase();
+}
+
/**
* Check if number is valid
* @returns a valid phone number formatted
@@ -114,4 +125,6 @@ export {
postSAMLLogin,
handleSAMLLoginError,
formatE164PhoneNumber,
+ getEmailDomain,
+ isDomainPublic,
};
diff --git a/src/libs/MergeTransactionUtils.ts b/src/libs/MergeTransactionUtils.ts
index d56524cbd84cf..c66f289c3f419 100644
--- a/src/libs/MergeTransactionUtils.ts
+++ b/src/libs/MergeTransactionUtils.ts
@@ -35,9 +35,6 @@ type MergeFieldData = {
options: MergeFieldOption[];
};
-/** Type for merge transaction values that can be null to clear existing values in Onyx */
-type MergeTransactionUpdateValues = Partial>;
-
const MERGE_FIELD_TRANSLATION_KEYS = {
amount: 'iou.amount',
currency: 'iou.currency',
@@ -328,7 +325,6 @@ function buildMergedTransactionData(targetTransaction: OnyxEntry, m
created: mergeTransaction.created,
modifiedCreated: mergeTransaction.created,
reportID: mergeTransaction.reportID,
- reportName: mergeTransaction.reportName,
};
}
@@ -379,11 +375,7 @@ function getDisplayValue(field: MergeFieldKey, transaction: Transaction, transla
return getCommaSeparatedTagNameWithSanitizedColons(SafeString(fieldValue));
}
if (field === 'reportID') {
- if (fieldValue === CONST.REPORT.UNREPORTED_REPORT_ID) {
- return translate('common.none');
- }
-
- return transaction?.reportName ?? getReportName(getReportOrDraftReport(SafeString(fieldValue)));
+ return fieldValue === CONST.REPORT.UNREPORTED_REPORT_ID ? translate('common.none') : getReportName(getReportOrDraftReport(SafeString(fieldValue)));
}
if (field === 'attendees') {
return Array.isArray(fieldValue) ? getAttendeesListDisplayString(fieldValue) : '';
@@ -437,26 +429,6 @@ function buildMergeFieldsData(
});
}
-/**
- * Build updated values for merge transaction field selection
- * Handles special cases like currency for amount field, reportID
- */
-function getMergeFieldUpdatedValues(transaction: OnyxEntry, field: K, fieldValue: MergeTransaction[K]): MergeTransactionUpdateValues {
- const updatedValues: MergeTransactionUpdateValues = {
- [field]: fieldValue,
- };
-
- if (field === 'amount') {
- updatedValues.currency = getCurrency(transaction);
- }
-
- if (field === 'reportID') {
- updatedValues.reportName = transaction?.reportName ?? getReportName(getReportOrDraftReport(getReportIDForExpense(transaction)));
- }
-
- return updatedValues;
-}
-
export {
getSourceTransactionFromMergeTransaction,
getTargetTransactionFromMergeTransaction,
@@ -473,9 +445,8 @@ export {
getDisplayValue,
buildMergeFieldsData,
getReportIDForExpense,
- getMergeFieldUpdatedValues,
getMergeFieldErrorText,
MERGE_FIELDS,
};
-export type {MergeFieldKey, MergeFieldData, MergeTransactionUpdateValues};
+export type {MergeFieldKey, MergeFieldData};
diff --git a/src/libs/Middleware/FraudMonitoring.ts b/src/libs/Middleware/FraudMonitoring.ts
index da836e42099ab..ceae84bd31490 100644
--- a/src/libs/Middleware/FraudMonitoring.ts
+++ b/src/libs/Middleware/FraudMonitoring.ts
@@ -37,6 +37,14 @@ const fraudSignalFactoryByApiCommand: Record = {
[WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_MANUALLY]: () => ({event: FRAUD_PROTECTION_EVENT.BUSINESS_BANK_ACCOUNT_SETUP}),
[WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: () => ({event: FRAUD_PROTECTION_EVENT.BUSINESS_BANK_ACCOUNT_SETUP}),
[WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: () => ({event: FRAUD_PROTECTION_EVENT.PERSONAL_BANK_ACCOUNT_SETUP}),
+ [WRITE_COMMANDS.INVITE_TO_GROUP_CHAT]: (_, responseData) => {
+ const newAccountCountAttribute = responseData?.newAccountCount ? {key: 'new_account_count', value: responseData?.newAccountCount as string} : undefined;
+ return {event: FRAUD_PROTECTION_EVENT.NEW_EMAILS_INVITED, attribute: newAccountCountAttribute};
+ },
+ [WRITE_COMMANDS.INVITE_TO_ROOM]: (_, responseData) => {
+ const newAccountCountAttribute = responseData?.newAccountCount ? {key: 'new_account_count', value: responseData?.newAccountCount as string} : undefined;
+ return {event: FRAUD_PROTECTION_EVENT.NEW_EMAILS_INVITED, attribute: newAccountCountAttribute};
+ },
};
const FraudMonitoring: Middleware = (response, request) =>
diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts
index 2b432cd52608b..8729444695269 100644
--- a/src/libs/Middleware/SaveResponseInOnyx.ts
+++ b/src/libs/Middleware/SaveResponseInOnyx.ts
@@ -5,6 +5,7 @@ import type Middleware from './types';
// If we're executing any of these requests, we don't need to trigger our OnyxUpdates flow to update the current data even if our current value is out of
// date because all these requests are updating the app to the most current state.
+// eslint-disable-next-line unicorn/prefer-set-has
const requestsToIgnoreLastUpdateID: string[] = [
WRITE_COMMANDS.OPEN_APP,
SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP,
diff --git a/src/libs/MoneyRequestReportUtils.ts b/src/libs/MoneyRequestReportUtils.ts
index 89f75a24f5512..28706b8038899 100644
--- a/src/libs/MoneyRequestReportUtils.ts
+++ b/src/libs/MoneyRequestReportUtils.ts
@@ -21,6 +21,7 @@ import {isTransactionPendingDelete} from './TransactionUtils';
* In MoneyRequestReport we filter out some IOU action types, because expense/transaction data is displayed in a separate list
* at the top
*/
+// eslint-disable-next-line unicorn/prefer-set-has
const IOU_ACTIONS_TO_FILTER_OUT: Array = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.TRACK];
/**
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 21d071b98a1c3..d4b46ec8acf63 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -34,7 +34,6 @@ import NavBarManager from '@libs/NavBarManager';
import getCurrentUrl from '@libs/Navigation/currentUrl';
import Navigation from '@libs/Navigation/Navigation';
import Animations, {InternalPlatformAnimations} from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
-import Presentation from '@libs/Navigation/PlatformStackNavigation/navigationOptions/presentation';
import type {AuthScreensParamList} from '@libs/Navigation/types';
import NetworkConnection from '@libs/NetworkConnection';
import Pusher from '@libs/Pusher';
@@ -83,7 +82,6 @@ const loadLogOutPreviousUserPage = () => require('../../..
const loadConciergePage = () => require('../../../pages/ConciergePage').default;
const loadTrackExpensePage = () => require('../../../pages/TrackExpensePage').default;
const loadSubmitExpensePage = () => require('../../../pages/SubmitExpensePage').default;
-const loadReceiptView = () => require('../../../pages/TransactionReceiptPage').default;
const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default;
const loadReportSplitNavigator = () => require('./Navigators/ReportsSplitNavigator').default;
@@ -568,6 +566,18 @@ function AuthScreens() {
getComponent={loadAttachmentModalScreen}
listeners={modalScreenListeners}
/>
+
+
-
- require('../../../../pages/iou/request/IOURequestRedirectToStartPage').default,
[SCREENS.MONEY_REQUEST.CREATE]: () => require('../../../../pages/iou/request/IOURequestStartPage').default,
[SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: () => require('../../../../pages/iou/request/step/IOURequestStepConfirmation').default,
+ [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: () => require('../../../../pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage').default,
[SCREENS.MONEY_REQUEST.STEP_AMOUNT]: () => require('../../../../pages/iou/request/step/IOURequestStepAmount').default,
[SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: () => require('../../../../pages/iou/request/step/IOURequestStepTaxAmountPage').default,
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: () => require('../../../../pages/iou/request/step/IOURequestStepTaxRatePage').default,
@@ -185,6 +187,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/DistanceRequestStartPage').default,
[SCREENS.MONEY_REQUEST.STEP_DISTANCE_MAP]: () => require('../../../../pages/iou/request/step/IOURequestStepDistanceMap').default,
[SCREENS.MONEY_REQUEST.STEP_DISTANCE_MANUAL]: () => require('../../../../pages/iou/request/step/IOURequestStepDistanceManual').default,
+ [SCREENS.SET_DEFAULT_WORKSPACE]: () => require('../../../../pages/SetDefaultWorkspacePage').default,
});
const TravelModalStackNavigator = createModalStackNavigator({
@@ -213,10 +216,6 @@ const NewReportWorkspaceSelectionModalStackNavigator = createModalStackNavigator
[SCREENS.NEW_REPORT_WORKSPACE_SELECTION.ROOT]: () => require('../../../../pages/NewReportWorkspaceSelectionPage').default,
});
-const SetDefaultWorkspaceModalStackNavigator = createModalStackNavigator({
- [SCREENS.SET_DEFAULT_WORKSPACE.ROOT]: () => require('../../../../pages/SetDefaultWorkspacePage').default,
-});
-
const ReportDetailsModalStackNavigator = createModalStackNavigator({
[SCREENS.REPORT_DETAILS.ROOT]: () => require('../../../../pages/ReportDetailsPage').default,
[SCREENS.REPORT_DETAILS.SHARE_CODE]: () => require('../../../../pages/home/report/ReportDetailsShareCodePage').default,
@@ -255,6 +254,11 @@ const TaskModalStackNavigator = createModalStackNavigator require('../../../../pages/tasks/TaskAssigneeSelectorModal').default,
});
+const ReportVerifyAccountModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/home/report/ReportVerifyAccountPage').default,
+ [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchReportVerifyAccountPage').default,
+});
+
const ReportDescriptionModalStackNavigator = createModalStackNavigator({
[SCREENS.REPORT_DESCRIPTION_ROOT]: () => require('../../../../pages/ReportDescriptionPage').default,
});
@@ -294,6 +298,8 @@ const ExpensifyCardModalStackNavigator = createModalStackNavigator({
const DomainCardModalStackNavigator = createModalStackNavigator({
[SCREENS.DOMAIN_CARD.DOMAIN_CARD_DETAIL]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/index').default,
[SCREENS.DOMAIN_CARD.DOMAIN_CARD_REPORT_FRAUD]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default,
+ [SCREENS.DOMAIN_CARD.DOMAIN_CARD_UPDATE_ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default,
+ [SCREENS.DOMAIN_CARD.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardVerifyAccountPage').default,
});
const ReportParticipantsModalStackNavigator = createModalStackNavigator({
@@ -683,8 +689,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage').default,
[SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: () => require('../../../../pages/workspace/companyCards/BankConnection').default,
[SCREENS.WORKSPACE.COMPANY_CARDS_ADD_NEW]: () => require('../../../../pages/workspace/companyCards/addNew/AddNewCardPage').default,
- [SCREENS.WORKSPACE.COMPANY_CARDS_TRANSACTION_START_DATE]: () =>
- require('../../../../pages/workspace/companyCards/assignCard/TransactionStartDateSelectorPage').default,
[SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage').default,
[SCREENS.WORKSPACE.COMPANY_CARD_NAME]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage').default,
[SCREENS.WORKSPACE.COMPANY_CARD_EXPORT]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage').default,
@@ -830,6 +834,8 @@ const MergeTransactionStackNavigator = createModalStackNavigator({
[SCREENS.SEARCH.REPORT_RHP]: () => require('../../../../pages/home/ReportScreen').default,
+ [SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchRootVerifyAccountPage').default,
+ [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchMoneyRequestReportVerifyAccountPage').default,
[SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: () => require('../../../../pages/Search/SearchHoldReasonPage').default,
[SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: () => require('../../../../pages/Search/SearchHoldReasonPage').default,
[SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: () => require('../../../../pages/Search/SearchTransactionsChangeReport').default,
@@ -917,6 +923,11 @@ const ScheduleCallModalStackNavigator = createModalStackNavigator require('../../../../pages/ScheduleCall/ScheduleCallConfirmationPage').default,
});
+const WorkspacesDomainModalStackNavigator = createModalStackNavigator({
+ [SCREENS.WORKSPACES_VERIFY_DOMAIN]: () => require('../../../../pages/domain/VerifyDomainPage').default,
+ [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: () => require('../../../../pages/domain/DomainVerifiedPage').default,
+});
+
export {
AddPersonalBankAccountModalStackNavigator,
EditRequestStackNavigator,
@@ -931,7 +942,6 @@ export {
ReferralModalStackNavigator,
TravelModalStackNavigator,
NewReportWorkspaceSelectionModalStackNavigator,
- SetDefaultWorkspaceModalStackNavigator,
ReportDescriptionModalStackNavigator,
ReportDetailsModalStackNavigator,
ReportChangeWorkspaceModalStackNavigator,
@@ -948,6 +958,7 @@ export {
DomainCardModalStackNavigator,
SplitDetailsModalStackNavigator,
TaskModalStackNavigator,
+ ReportVerifyAccountModalStackNavigator,
WalletStatementStackNavigator,
TransactionDuplicateStackNavigator,
SearchReportModalStackNavigator,
@@ -963,4 +974,5 @@ export {
AddUnreportedExpenseModalStackNavigator,
ScheduleCallModalStackNavigator,
MergeTransactionStackNavigator,
+ WorkspacesDomainModalStackNavigator,
};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index 6c6e525903b37..1e2b5003d38a4 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -107,10 +107,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.NEW_REPORT_WORKSPACE_SELECTION}
component={ModalStackNavigators.NewReportWorkspaceSelectionModalStackNavigator}
/>
-
+
+
{/* The second overlay is here to cover the wide rhp screen underneath */}
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
index 3cdcf9fe2cf7f..d40cef6a074c1 100644
--- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
@@ -11,6 +11,7 @@ import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import type {OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType} from './types';
+// eslint-disable-next-line unicorn/prefer-set-has
const MODAL_ROUTES_TO_DISMISS: string[] = [
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
NAVIGATORS.RIGHT_MODAL_NAVIGATOR,
diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState.ts
index 8cbd48f5a98e0..a234b530a49fa 100644
--- a/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState.ts
+++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState.ts
@@ -4,6 +4,7 @@ import {useEffect} from 'react';
const preservedNavigatorStates: Record> = {};
const cleanPreservedNavigatorStates = (state: NavigationState) => {
+ // eslint-disable-next-line unicorn/prefer-set-has
const currentSplitNavigatorKeys = state.routes.map((route) => route.key);
for (const key of Object.keys(preservedNavigatorStates)) {
diff --git a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts
index c7581905db801..9cb6a7aed8ecc 100644
--- a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts
+++ b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts
@@ -24,8 +24,8 @@ import {getPreservedNavigatorState} from './createSplitNavigator/usePreserveNavi
// This timing is used to call the preload function after a tab change, when the initial tab screen has already been rendered.
const TIMING_TO_CALL_PRELOAD = 1000;
-// Currently, only the Inbox, Workspaces, Account tabs are preloaded. The remaining tabs will be supported soon.
-const TABS_TO_PRELOAD = [NAVIGATION_TABS.HOME, NAVIGATION_TABS.WORKSPACES, NAVIGATION_TABS.SETTINGS];
+// Currently, only the Account and Workspaces tabs are preloaded. The remaining tabs will be supported soon.
+const TABS_TO_PRELOAD = [NAVIGATION_TABS.SETTINGS, NAVIGATION_TABS.WORKSPACES];
function preloadWorkspacesTab(navigation: PlatformStackNavigationProp) {
const state = getWorkspacesTabStateFromSessionStorage() ?? navigation.getState();
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index 1549486c1849f..b751f16aa4079 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -35,6 +35,7 @@ import navigationRef from './navigationRef';
import type {NavigationPartialRoute, NavigationRoute, NavigationStateRoute, ReportsSplitNavigatorParamList, RootNavigatorParamList, State} from './types';
// Routes which are part of the flow to set up 2FA
+// eslint-disable-next-line unicorn/prefer-set-has
const SET_UP_2FA_ROUTES: Route[] = [
ROUTES.REQUIRE_TWO_FACTOR_AUTH,
ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.REQUIRE_TWO_FACTOR_AUTH),
diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx
index 8251044b728d6..4130cd8bb62e1 100644
--- a/src/libs/Navigation/NavigationRoot.tsx
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -1,6 +1,8 @@
import type {NavigationState} from '@react-navigation/native';
import {DarkTheme, DefaultTheme, findFocusedRoute, getPathFromState, NavigationContainer} from '@react-navigation/native';
+import {hasCompletedGuidedSetupFlowSelector, wasInvitedToNewDotSelector} from '@selectors/Onboarding';
import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react';
+import {useOnboardingValues} from '@components/OnyxListItemProvider';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
import useCurrentReportID from '@hooks/useCurrentReportID';
import useOnyx from '@hooks/useOnyx';
@@ -11,7 +13,6 @@ import useThemePreference from '@hooks/useThemePreference';
import Firebase from '@libs/Firebase';
import FS from '@libs/Fullstory';
import Log from '@libs/Log';
-import {hasCompletedGuidedSetupFlowSelector, wasInvitedToNewDotSelector} from '@libs/onboardingSelectors';
import shouldOpenLastVisitedPath from '@libs/shouldOpenLastVisitedPath';
import {getPathFromURL} from '@libs/Url';
import {updateLastVisitedPath} from '@userActions/App';
@@ -108,7 +109,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
const [currentOnboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, {canBeMissing: true});
const [currentOnboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true});
const [onboardingInitialPath] = useOnyx(ONYXKEYS.ONBOARDING_LAST_VISITED_PATH, {canBeMissing: true});
-
+ const onboardingValues = useOnboardingValues();
const previousAuthenticated = usePrevious(authenticated);
const initialState = useMemo(() => {
@@ -148,6 +149,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
currentOnboardingPurposeSelected,
currentOnboardingCompanySize,
onboardingInitialPath,
+ onboardingValues,
}),
linkingConfig.config,
);
diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
index 683ab77d84dc2..8f4b76aeceab6 100644
--- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
@@ -16,6 +16,7 @@ import type {Report} from '@src/types/onyx';
import getMatchingNewRoute from './getMatchingNewRoute';
import getParamsFromRoute from './getParamsFromRoute';
import {isFullScreenName} from './isNavigatorName';
+import normalizePath from './normalizePath';
import replacePathInNestedState from './replacePathInNestedState';
let allReports: OnyxCollection;
@@ -90,7 +91,9 @@ function getMatchingFullScreenRoute(route: NavigationPartialRoute) {
if (RHP_TO_WORKSPACES_LIST[route.name]) {
return {
name: SCREENS.WORKSPACES_LIST,
- path: ROUTES.WORKSPACES_LIST.route,
+ // prepending a slash to ensure closing the RHP after refreshing the page
+ // replaces the whole path with "/workspaces", instead of just replacing the last url segment ("/x/y/workspaces")
+ path: normalizePath(ROUTES.WORKSPACES_LIST.route),
};
}
diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts
index 3b92bce9df20a..a733606877875 100644
--- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts
+++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts
@@ -4,10 +4,12 @@ import SCREENS from '@src/SCREENS';
// This file is used to define RHP screens that are in relation to the search screen.
const SEARCH_TO_RHP: Partial> = {
[SCREENS.SEARCH.ROOT]: [
+ SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT,
SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_GROUP_BY_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP,
SCREENS.SEARCH.REPORT_RHP,
+ SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT,
SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP,
SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_RHP,
diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts
index b920487f1d148..40eebe61d4be7 100644
--- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts
+++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts
@@ -1,8 +1,7 @@
-import type {WorkspaceDuplicateNavigatorParamList} from '@navigation/types';
import SCREENS from '@src/SCREENS';
-const WORKSPACES_LIST_TO_RHP: Partial> = {
- [SCREENS.WORKSPACE_DUPLICATE.ROOT]: [SCREENS.WORKSPACE_DUPLICATE.SELECT_FEATURES, SCREENS.WORKSPACE_DUPLICATE.ROOT],
+const WORKSPACES_LIST_TO_RHP: Record = {
+ [SCREENS.WORKSPACES_LIST]: [SCREENS.WORKSPACE_DUPLICATE.SELECT_FEATURES, SCREENS.WORKSPACE_DUPLICATE.ROOT, SCREENS.WORKSPACES_VERIFY_DOMAIN, SCREENS.WORKSPACES_DOMAIN_VERIFIED],
};
export default WORKSPACES_LIST_TO_RHP;
diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts
index 5cea72f6439e8..60cca8d846bc8 100755
--- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts
+++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts
@@ -240,7 +240,6 @@ const WORKSPACE_TO_RHP: Partial['config'] = {
[SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT,
[SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route,
[SCREENS.REPORT_ADD_ATTACHMENT]: ROUTES.REPORT_ADD_ATTACHMENT.route,
- [SCREENS.PROFILE_AVATAR]: ROUTES.PROFILE_AVATAR.route,
+ [SCREENS.PROFILE_AVATAR]: {
+ path: ROUTES.PROFILE_AVATAR.route,
+ parse: {
+ accountID: Number,
+ },
+ },
[SCREENS.WORKSPACE_AVATAR]: ROUTES.WORKSPACE_AVATAR.route,
[SCREENS.REPORT_AVATAR]: ROUTES.REPORT_AVATAR.route,
[SCREENS.TRANSACTION_RECEIPT]: ROUTES.TRANSACTION_RECEIPT.route,
@@ -739,9 +744,6 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: {
path: ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.route,
},
- [SCREENS.WORKSPACE.COMPANY_CARDS_TRANSACTION_START_DATE]: {
- path: ROUTES.WORKSPACE_COMPANY_CARDS_TRANSACTION_START_DATE.route,
- },
[SCREENS.WORKSPACE.INVITE]: {
path: ROUTES.WORKSPACE_INVITE.route,
},
@@ -1114,11 +1116,6 @@ const config: LinkingOptions['config'] = {
[SCREENS.NEW_REPORT_WORKSPACE_SELECTION.ROOT]: ROUTES.NEW_REPORT_WORKSPACE_SELECTION.route,
},
},
- [SCREENS.RIGHT_MODAL.SET_DEFAULT_WORKSPACE]: {
- screens: {
- [SCREENS.SET_DEFAULT_WORKSPACE.ROOT]: ROUTES.SET_DEFAULT_WORKSPACE.route,
- },
- },
[SCREENS.RIGHT_MODAL.REPORT_DETAILS]: {
screens: {
[SCREENS.REPORT_DETAILS.ROOT]: ROUTES.REPORT_WITH_ID_DETAILS.route,
@@ -1254,6 +1251,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.DOMAIN_CARD.DOMAIN_CARD_REPORT_FRAUD]: {
path: ROUTES.SETTINGS_DOMAIN_CARD_REPORT_FRAUD.route,
},
+ [SCREENS.DOMAIN_CARD.DOMAIN_CARD_UPDATE_ADDRESS]: {
+ path: ROUTES.SETTINGS_DOMAIN_CARD_UPDATE_ADDRESS.route,
+ },
+ [SCREENS.DOMAIN_CARD.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: {
+ path: ROUTES.SETTINGS_DOMAIN_CARD_CONFIRM_MAGIC_CODE.route,
+ },
},
},
[SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: {
@@ -1381,6 +1384,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.STEP_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_AMOUNT.route,
[SCREENS.MONEY_REQUEST.STEP_CATEGORY]: ROUTES.MONEY_REQUEST_STEP_CATEGORY.route,
[SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.route,
+ [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.route,
[SCREENS.MONEY_REQUEST.STEP_CURRENCY]: ROUTES.MONEY_REQUEST_STEP_CURRENCY.route,
[SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route,
[SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route,
@@ -1421,6 +1425,7 @@ const config: LinkingOptions['config'] = {
path: ROUTES.SPLIT_EXPENSE_EDIT.route,
exact: true,
},
+ [SCREENS.SET_DEFAULT_WORKSPACE]: ROUTES.SET_DEFAULT_WORKSPACE.route,
},
},
[SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE]: {
@@ -1519,6 +1524,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.REFERRAL_DETAILS]: ROUTES.REFERRAL_DETAILS_MODAL.route,
},
},
+ [SCREENS.RIGHT_MODAL.REPORT_VERIFY_ACCOUNT]: {
+ screens: {
+ [SCREENS.REPORT_VERIFY_ACCOUNT]: ROUTES.REPORT_VERIFY_ACCOUNT.route,
+ [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.route,
+ },
+ },
[SCREENS.RIGHT_MODAL.TRAVEL]: {
screens: {
[SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS,
@@ -1541,7 +1552,9 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.RIGHT_MODAL.SEARCH_REPORT]: {
screens: {
+ [SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT]: ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT,
[SCREENS.SEARCH.REPORT_RHP]: ROUTES.SEARCH_REPORT.route,
+ [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.route,
[SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS.route,
[SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP,
[SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP,
@@ -1716,6 +1729,18 @@ const config: LinkingOptions['config'] = {
[SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route,
},
},
+ [SCREENS.RIGHT_MODAL.DOMAIN]: {
+ screens: {
+ [SCREENS.WORKSPACES_VERIFY_DOMAIN]: {
+ path: ROUTES.WORKSPACES_VERIFY_DOMAIN.route,
+ exact: true,
+ },
+ [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: {
+ path: ROUTES.WORKSPACES_DOMAIN_VERIFIED.route,
+ exact: true,
+ },
+ },
+ },
},
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 60348b78e4eaf..e29911da78079 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -83,6 +83,12 @@ type ConsoleNavigatorParamList = {
};
};
+type ReportVerifyAccountNavigatorParamList = {
+ [SCREENS.REPORT_VERIFY_ACCOUNT]: {
+ reportID: string;
+ };
+};
+
type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.SHARE_CODE]: undefined;
[SCREENS.SETTINGS.PROFILE.PRONOUNS]: undefined;
@@ -1291,6 +1297,18 @@ type SettingsNavigatorParamList = {
};
} & ReimbursementAccountNavigatorParamList;
+type DomainCardNavigatorParamList = {
+ [SCREENS.DOMAIN_CARD.DOMAIN_CARD_DETAIL]: {
+ cardID: string;
+ };
+ [SCREENS.DOMAIN_CARD.DOMAIN_CARD_REPORT_FRAUD]: {
+ cardID: string;
+ };
+ [SCREENS.DOMAIN_CARD.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: {
+ cardID: string;
+ };
+};
+
type TwoFactorAuthNavigatorParamList = {
[SCREENS.TWO_FACTOR_AUTH.ROOT]: {
// eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
@@ -1347,12 +1365,6 @@ type NewReportWorkspaceSelectionNavigatorParamList = {
};
};
-type SetDefaultWorkspaceNavigatorParamList = {
- [SCREENS.SET_DEFAULT_WORKSPACE.ROOT]: {
- navigateTo?: Routes;
- };
-};
-
type ReportDetailsNavigatorParamList = {
[SCREENS.REPORT_DETAILS.ROOT]: {
reportID: string;
@@ -1661,6 +1673,12 @@ type MoneyRequestNavigatorParamList = {
participantsAutoAssigned?: string;
backToReport?: string;
};
+ [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: {
+ action: IOUAction;
+ iouType: IOUType;
+ transactionID: string;
+ reportID: string;
+ };
[SCREENS.MONEY_REQUEST.STEP_SCAN]: {
action: IOUAction;
iouType: IOUType;
@@ -1802,6 +1820,9 @@ type MoneyRequestNavigatorParamList = {
backToReport?: string;
reportActionID?: string;
};
+ [SCREENS.SET_DEFAULT_WORKSPACE]: {
+ navigateTo?: Routes;
+ };
};
type WorkspaceConfirmationNavigatorParamList = {
@@ -2032,6 +2053,15 @@ type MergeTransactionNavigatorParamList = {
};
};
+type WorkspacesDomainModalNavigatorParamList = {
+ [SCREENS.WORKSPACES_VERIFY_DOMAIN]: {
+ accountID: number;
+ };
+ [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: {
+ accountID: number;
+ };
+};
+
type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.SETTINGS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: NavigatorScreenParams;
@@ -2039,8 +2069,8 @@ type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams;
[SCREENS.SETTINGS.SHARE_CODE]: undefined;
+ [SCREENS.RIGHT_MODAL.REPORT_VERIFY_ACCOUNT]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.NEW_REPORT_WORKSPACE_SELECTION]: NavigatorScreenParams;
- [SCREENS.RIGHT_MODAL.SET_DEFAULT_WORKSPACE]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_CHANGE_WORKSPACE]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: NavigatorScreenParams;
@@ -2080,6 +2110,7 @@ type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.SCHEDULE_CALL]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.DOMAIN]: NavigatorScreenParams;
};
type TravelNavigatorParamList = {
@@ -2203,12 +2234,6 @@ type WorkspaceSplitNavigatorParamList = {
// eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
backTo?: Routes;
};
- [SCREENS.WORKSPACE.COMPANY_CARDS_TRANSACTION_START_DATE]: {
- policyID: string;
- feed: string;
- // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
- backTo?: Routes;
- };
[SCREENS.WORKSPACE.PER_DIEM]: {
policyID: string;
};
@@ -2538,6 +2563,12 @@ type SearchReportParamList = {
// eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
backTo?: Routes;
};
+ [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: {
+ reportID: string;
+ };
+ [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: {
+ reportID: string;
+ };
[SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: {
/** ID of the transaction the page was opened for */
transactionID: string;
@@ -2708,6 +2739,7 @@ export type {
BackToParams,
DebugParamList,
DetailsNavigatorParamList,
+ DomainCardNavigatorParamList,
EditRequestNavigatorParamList,
EnablePaymentsNavigatorParamList,
ExplanationModalNavigatorParamList,
@@ -2730,10 +2762,10 @@ export type {
ProfileNavigatorParamList,
PublicScreensParamList,
ReferralDetailsNavigatorParamList,
+ ReportVerifyAccountNavigatorParamList,
ReimbursementAccountNavigatorParamList,
ReimbursementAccountEnterSignerInfoNavigatorParamList,
NewReportWorkspaceSelectionNavigatorParamList,
- SetDefaultWorkspaceNavigatorParamList,
ReportDescriptionNavigatorParamList,
ReportDetailsNavigatorParamList,
ReportChangeWorkspaceNavigatorParamList,
@@ -2781,4 +2813,5 @@ export type {
TestToolsModalModalNavigatorParamList,
MergeTransactionNavigatorParamList,
AttachmentModalScreensParamList,
+ WorkspacesDomainModalNavigatorParamList,
};
diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts
index 5fb699c463eb8..64cda4bdd0e71 100644
--- a/src/libs/NextStepUtils.ts
+++ b/src/libs/NextStepUtils.ts
@@ -1,15 +1,12 @@
import {format, setDate} from 'date-fns';
import {Str} from 'expensify-common';
-import Onyx from 'react-native-onyx';
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Beta, Policy, Report, ReportNextStep, TransactionViolations} from '@src/types/onyx';
+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 EmailUtils from './EmailUtils';
-import Permissions from './Permissions';
import {getLoginsByAccountIDs, getPersonalDetailsByIDs} from './PersonalDetailsUtils';
import {getApprovalWorkflow, getCorrectedAutoReportingFrequency, getReimburserAccountID} from './PolicyUtils';
import {
@@ -17,13 +14,12 @@ import {
getMoneyRequestSpendBreakdown,
getNextApproverAccountID,
getPersonalDetailsForAccountID,
- hasViolations as hasViolationsReportUtils,
isExpenseReport,
isInvoiceReport,
isPayer,
} from './ReportUtils';
-type BuildNextStepNewParams = {
+type BuildNextStepParams = {
report: OnyxEntry;
policy?: OnyxEntry;
currentUserAccountIDParam?: number;
@@ -36,43 +32,7 @@ type BuildNextStepNewParams = {
isReopen?: boolean;
};
-let currentUserAccountID = -1;
-let currentUserEmail = '';
-Onyx.connect({
- key: ONYXKEYS.SESSION,
- callback: (value) => {
- if (!value) {
- return;
- }
-
- currentUserAccountID = value?.accountID ?? CONST.DEFAULT_NUMBER_ID;
- currentUserEmail = value?.email ?? '';
- },
-});
-
-let allPolicies: OnyxCollection;
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.POLICY,
- waitForCollectionCallback: true,
- callback: (value) => (allPolicies = value),
-});
-
-let allBetas: OnyxEntry;
-Onyx.connect({
- key: ONYXKEYS.BETAS,
- callback: (value) => (allBetas = value),
-});
-
-let transactionViolations: OnyxCollection;
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
- waitForCollectionCallback: true,
- callback: (value) => {
- transactionViolations = value;
- },
-});
-
-function parseMessage(messages: Message[] | undefined) {
+function parseMessage(messages: Message[] | undefined, currentUserEmail: string) {
let nextStepHTML = '';
messages?.forEach((part, index) => {
const isEmail = Str.isValidEmail(part.text);
@@ -151,389 +111,11 @@ function buildOptimisticNextStepForStrictPolicyRuleViolations() {
return optimisticNextStep;
}
-/**
- * Please don't use this function anymore, let's use buildNextStepNew instead
- *
- * @param report
- * @param predictedNextStatus - a next expected status of the report
- * @param shouldFixViolations - whether to show `fix the issue` next step
- * @param isUnapprove - whether a report is being unapproved
- * @param isReopen - whether a report is being reopened
- * @returns nextStep
- */
-/**
- * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access.
- * All usages of this function should be replaced with useOnyx hook in React components.
- */
-function buildNextStep(
- report: OnyxEntry,
- predictedNextStatus: ValueOf,
- shouldFixViolations?: boolean,
- isUnapprove?: boolean,
- isReopen?: boolean,
-): ReportNextStep | null {
- if (!isExpenseReport(report)) {
- return null;
- }
-
- const {policyID = '', ownerAccountID = -1} = report ?? {};
- const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? ({} as Policy);
- const {harvesting, autoReportingOffset} = policy;
- const autoReportingFrequency = getCorrectedAutoReportingFrequency(policy);
- const hasViolations = hasViolationsReportUtils(report?.reportID, transactionViolations);
- const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas);
- const isInstantSubmitEnabled = autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT;
- const shouldShowFixMessage = hasViolations && isInstantSubmitEnabled && !isASAPSubmitBetaEnabled;
- const [policyOwnerPersonalDetails, ownerPersonalDetails] = getPersonalDetailsByIDs({
- accountIDs: [policy.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID, ownerAccountID],
- currentUserAccountID,
- shouldChangeUserDisplayName: true,
- });
- const isReportContainingTransactions =
- report &&
- ((report.total !== 0 && report.total !== undefined) ||
- (report.unheldTotal !== 0 && report.unheldTotal !== undefined) ||
- (report.unheldNonReimbursableTotal !== 0 && report.unheldNonReimbursableTotal !== undefined));
- const {reimbursableSpend} = getMoneyRequestSpendBreakdown(report);
-
- const ownerDisplayName = ownerPersonalDetails?.displayName ?? ownerPersonalDetails?.login ?? getDisplayNameForParticipant({accountID: ownerAccountID});
- const policyOwnerDisplayName = policyOwnerPersonalDetails?.displayName ?? policyOwnerPersonalDetails?.login ?? getDisplayNameForParticipant({accountID: policy.ownerAccountID});
- const nextApproverDisplayName = getNextApproverDisplayName(report, isUnapprove);
- const approverAccountID = getNextApproverAccountID(report, isUnapprove);
- const approvers = getLoginsByAccountIDs([approverAccountID ?? CONST.DEFAULT_NUMBER_ID]);
-
- const reimburserAccountID = getReimburserAccountID(policy);
- const hasValidAccount = !!policy?.achAccount?.accountNumber || policy.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES;
- const type: ReportNextStep['type'] = 'neutral';
- let optimisticNextStep: ReportNextStep | null;
-
- const nextStepPayExpense = {
- type,
- icon: CONST.NEXT_STEP.ICONS.HOURGLASS,
- message: [
- {
- text: 'Waiting for ',
- },
- ownerAccountID === -1 || !policy.ownerAccountID
- ? {
- text: 'an admin',
- }
- : {
- text: shouldShowFixMessage ? ownerDisplayName : policyOwnerDisplayName,
- type: 'strong',
- },
- {
- text: ' to ',
- },
- ...(shouldShowFixMessage ? [{text: 'fix the issues'}] : [{text: 'pay'}, {text: ' %expenses.'}]),
- ],
- };
-
- const noActionRequired = {
- icon: CONST.NEXT_STEP.ICONS.CHECKMARK,
- type,
- message: [
- {
- text: 'No further action required!',
- },
- ],
- };
-
- switch (predictedNextStatus) {
- // Generates an optimistic nextStep once a report has been opened
- case CONST.REPORT.STATUS_NUM.OPEN:
- if ((isASAPSubmitBetaEnabled && hasViolations && isInstantSubmitEnabled) || shouldFixViolations) {
- optimisticNextStep = {
- type,
- icon: CONST.NEXT_STEP.ICONS.HOURGLASS,
- message: [
- {
- text: 'Waiting for ',
- },
- {
- text: `${ownerDisplayName}`,
- type: 'strong',
- clickToCopyText: ownerAccountID === currentUserAccountID ? currentUserEmail : '',
- },
- {
- text: ' to ',
- },
- {
- text: 'fix the issues',
- },
- ],
- };
- break;
- }
- if (isReopen) {
- optimisticNextStep = {
- type,
- icon: CONST.NEXT_STEP.ICONS.HOURGLASS,
- message: [
- {
- text: 'Waiting for ',
- },
- {
- text: `${ownerDisplayName}`,
- type: 'strong',
- clickToCopyText: ownerAccountID === currentUserAccountID ? currentUserEmail : '',
- },
- {
- text: ' to ',
- },
- {
- text: 'submit',
- },
- {
- text: ' %expenses.',
- },
- ],
- };
- break;
- }
-
- // Self review
- optimisticNextStep = {
- type,
- icon: CONST.NEXT_STEP.ICONS.HOURGLASS,
- message: [
- {
- text: 'Waiting for ',
- },
- {
- text: `${ownerDisplayName}`,
- type: 'strong',
- clickToCopyText: ownerAccountID === currentUserAccountID ? currentUserEmail : '',
- },
- {
- text: ' to ',
- },
- {
- text: 'add',
- },
- {
- text: ' %expenses.',
- },
- ],
- };
-
- // Scheduled submit enabled
- if (harvesting?.enabled && autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL && isReportContainingTransactions) {
- optimisticNextStep.message = [
- {
- text: 'Waiting for ',
- },
- {
- text: `${ownerDisplayName}`,
- type: 'strong',
- clickToCopyText: ownerAccountID === currentUserAccountID ? currentUserEmail : '',
- },
- {
- text: `'s`,
- type: 'strong',
- },
- {
- text: ' %expenses to automatically submit',
- },
- ];
- let harvestingSuffix = '';
-
- if (autoReportingFrequency) {
- const currentDate = new Date();
- let autoSubmissionDate = '';
- let monthlyText = '';
-
- if (autoReportingOffset === CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH) {
- monthlyText = 'on the last day of the month';
- } else if (autoReportingOffset === CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH) {
- monthlyText = 'on the last business day of the month';
- } else if (autoReportingOffset !== undefined) {
- autoSubmissionDate = format(setDate(currentDate, autoReportingOffset), CONST.DATE.ORDINAL_DAY_OF_MONTH);
- }
-
- const harvestingSuffixes: Record, string> = {
- [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE]: 'later today',
- [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]: 'on Sunday',
- [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY]: 'on the 1st and 16th of each month',
- [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY]: autoSubmissionDate ? `on the ${autoSubmissionDate} of each month` : monthlyText,
- [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP]: 'at the end of their trip',
- [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT]: '',
- [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: '',
- };
-
- if (harvestingSuffixes[autoReportingFrequency]) {
- harvestingSuffix = `${harvestingSuffixes[autoReportingFrequency]}`;
- }
- }
-
- optimisticNextStep.message.push({
- text: ` ${harvestingSuffix}`,
- });
- }
-
- // Manual submission
- if (report?.total !== 0 && !harvesting?.enabled && autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL) {
- optimisticNextStep.message = [
- {
- text: 'Waiting for ',
- },
- {
- text: `${ownerDisplayName}`,
- type: 'strong',
- clickToCopyText: ownerAccountID === currentUserAccountID ? currentUserEmail : '',
- },
- {
- text: ' to ',
- },
- {
- text: 'submit',
- },
- {
- text: ' %expenses.',
- },
- ];
- }
-
- break;
-
- // Generates an optimistic nextStep once a report has been submitted
- case CONST.REPORT.STATUS_NUM.SUBMITTED: {
- if (policy.approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL) {
- optimisticNextStep = reimbursableSpend === 0 ? noActionRequired : nextStepPayExpense;
- break;
- }
- // Another owner
- optimisticNextStep = {
- type,
- icon: CONST.NEXT_STEP.ICONS.HOURGLASS,
- };
- // We want to show pending approval next step for cases where the policy has approvals enabled
- const policyApprovalMode = getApprovalWorkflow(policy);
- if ([CONST.POLICY.APPROVAL_MODE.BASIC, CONST.POLICY.APPROVAL_MODE.ADVANCED].some((approvalMode) => approvalMode === policyApprovalMode)) {
- optimisticNextStep.message = [
- {
- text: 'Waiting for ',
- },
- {
- text: nextApproverDisplayName,
- type: 'strong',
- clickToCopyText: approvers.at(0),
- },
- {
- text: ' to ',
- },
- {
- text: 'approve',
- },
- {
- text: ' %expenses.',
- },
- ];
- } else {
- optimisticNextStep.message = [
- {
- text: 'Waiting for ',
- },
- isPayer(
- {
- accountID: currentUserAccountID,
- email: currentUserEmail,
- },
- report,
- )
- ? {
- text: `you`,
- type: 'strong',
- }
- : {
- text: `an admin`,
- },
- {
- text: ' to ',
- },
- {
- text: 'pay',
- },
- {
- text: ' %expenses.',
- },
- ];
- }
-
- break;
- }
-
- // Generates an optimistic nextStep once a report has been closed for example in the case of Submit and Close approval flow
- case CONST.REPORT.STATUS_NUM.CLOSED:
- optimisticNextStep = noActionRequired;
-
- break;
-
- // Generates an optimistic nextStep once a report has been paid
- case CONST.REPORT.STATUS_NUM.REIMBURSED:
- optimisticNextStep = noActionRequired;
-
- break;
-
- // Generates an optimistic nextStep once a report has been approved
- case CONST.REPORT.STATUS_NUM.APPROVED:
- if (
- isInvoiceReport(report) ||
- !isPayer(
- {
- accountID: currentUserAccountID,
- email: currentUserEmail,
- },
- report,
- ) ||
- reimbursableSpend === 0
- ) {
- optimisticNextStep = noActionRequired;
-
- break;
- }
- // Self review
- optimisticNextStep = {
- type,
- icon: CONST.NEXT_STEP.ICONS.HOURGLASS,
- message: [
- {
- text: 'Waiting for ',
- },
- reimburserAccountID === -1
- ? {
- text: 'an admin',
- }
- : {
- text: getDisplayNameForParticipant({accountID: reimburserAccountID}),
- type: 'strong',
- },
- {
- text: ' to ',
- },
- {
- text: hasValidAccount ? 'pay' : 'finish setting up',
- },
- {
- text: hasValidAccount ? ' %expenses.' : ' a business bank account.',
- },
- ],
- };
- break;
-
- // Resets a nextStep
- default:
- optimisticNextStep = null;
- }
-
- return optimisticNextStep;
-}
-
/**
* Generates an optimistic nextStep based on a current report status and other properties.
* Need to rename this function and remove the buildNextStep function above after migrating to this function
*/
-function buildNextStepNew(params: BuildNextStepNewParams): ReportNextStep | null {
+function buildNextStep(params: BuildNextStepParams): ReportNextStep | null {
const {report, policy, currentUserAccountIDParam, currentUserEmailParam, hasViolations, isASAPSubmitBetaEnabled, predictedNextStatus, shouldFixViolations, isUnapprove, isReopen} =
params;
@@ -890,12 +472,4 @@ function buildNextStepNew(params: BuildNextStepNewParams): ReportNextStep | null
return optimisticNextStep;
}
-export {
- parseMessage,
- // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365)
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- buildNextStep,
- buildOptimisticNextStepForPreventSelfApprovalsEnabled,
- buildOptimisticNextStepForStrictPolicyRuleViolations,
- buildNextStepNew,
-};
+export {parseMessage, buildOptimisticNextStepForPreventSelfApprovalsEnabled, buildOptimisticNextStepForStrictPolicyRuleViolations, buildNextStep};
diff --git a/src/libs/NumberFormatUtils/index.ts b/src/libs/NumberFormatUtils/index.ts
index 02bb14b29eed7..cf04fa6eca566 100644
--- a/src/libs/NumberFormatUtils/index.ts
+++ b/src/libs/NumberFormatUtils/index.ts
@@ -1,9 +1,10 @@
+import intlPolyfill from '@libs/IntlPolyfill';
import memoize from '@libs/memoize';
import CONST from '@src/CONST';
import type Locale from '@src/types/onyx/Locale';
-import initPolyfill from './intlPolyfill';
-initPolyfill();
+// Polyfill the Intl API if locale data is not as expected
+intlPolyfill();
const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10, monitoringName: 'NumberFormatUtils'});
diff --git a/src/libs/NumberFormatUtils/intlPolyfill.ios.ts b/src/libs/NumberFormatUtils/intlPolyfill.ios.ts
deleted file mode 100644
index 4745284c0b61f..0000000000000
--- a/src/libs/NumberFormatUtils/intlPolyfill.ios.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import intlPolyfill from '@libs/IntlPolyfill';
-
-// On iOS, polyfills from `additionalSetup` are applied after memoization, which results in incorrect cache entry of `Intl.NumberFormat` (e.g. lacking `formatToParts` method).
-// To fix this, we need to apply the polyfill manually before memoization.
-// For further information, see: https://github.com/Expensify/App/pull/43868#issuecomment-2217637217
-const initPolyfill = () => {
- intlPolyfill();
-};
-
-export default initPolyfill;
diff --git a/src/libs/NumberFormatUtils/intlPolyfill.ts b/src/libs/NumberFormatUtils/intlPolyfill.ts
deleted file mode 100644
index 31fedd6a01b6a..0000000000000
--- a/src/libs/NumberFormatUtils/intlPolyfill.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-const initPolyfill = () => {};
-export default initPolyfill;
diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts
index 536c8cd522810..0c33383979836 100644
--- a/src/libs/OptionsListUtils/index.ts
+++ b/src/libs/OptionsListUtils/index.ts
@@ -109,6 +109,7 @@ import {
getReportOrDraftReport,
getReportPreviewMessage,
getReportSubtitlePrefix,
+ getUnreportedTransactionMessage,
getUpgradeWorkspaceMessage,
hasIOUWaitingOnCurrentUserBankAccount,
isArchivedNonExpenseReport,
@@ -316,12 +317,6 @@ Onyx.connect({
callback: (value) => (activePolicyID = value),
});
-let nvpDismissedProductTraining: OnyxEntry;
-Onyx.connect({
- key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING,
- callback: (value) => (nvpDismissedProductTraining = value),
-});
-
/**
* Returns the personal details for an array of accountIDs
* @returns keys of the object are emails, values are PersonalDetails objects.
@@ -781,6 +776,8 @@ function getLastMessageTextForReport({
lastMessageTextFromReport = Parser.htmlToText(getChangedApproverActionMessage(lastReportAction));
} else if (isMovedAction(lastReportAction)) {
lastMessageTextFromReport = Parser.htmlToText(getMovedActionMessage(lastReportAction, report));
+ } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION)) {
+ lastMessageTextFromReport = Parser.htmlToText(getUnreportedTransactionMessage());
}
// we do not want to show report closed in LHN for non archived report so use getReportLastMessage as fallback instead of lastMessageText from report
@@ -1890,7 +1887,7 @@ function prepareReportOptionsForDisplay(options: Array>, co
/**
* Whether user submitted already an expense or scanned receipt
*/
-function getIsUserSubmittedExpenseOrScannedReceipt(): boolean {
+function getIsUserSubmittedExpenseOrScannedReceipt(nvpDismissedProductTraining: OnyxEntry): boolean {
return !!nvpDismissedProductTraining?.[CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP];
}
@@ -1906,12 +1903,17 @@ function isManagerMcTestReport(report: SearchOption): boolean {
* based on dynamic business logic and feature flags.
* Centralizes restriction logic to avoid scattering conditions across the codebase.
*/
-function getRestrictedLogins(config: GetOptionsConfig, options: OptionList, canShowManagerMcTest: boolean): Record {
+function getRestrictedLogins(
+ config: GetOptionsConfig,
+ options: OptionList,
+ canShowManagerMcTest: boolean,
+ nvpDismissedProductTraining: OnyxEntry,
+): Record {
const userHasReportWithManagerMcTest = Object.values(options.reports).some((report) => isManagerMcTestReport(report));
return {
[CONST.EMAIL.MANAGER_MCTEST]:
!canShowManagerMcTest ||
- (getIsUserSubmittedExpenseOrScannedReceipt() && !userHasReportWithManagerMcTest) ||
+ (getIsUserSubmittedExpenseOrScannedReceipt(nvpDismissedProductTraining) && !userHasReportWithManagerMcTest) ||
!Permissions.isBetaEnabled(CONST.BETAS.NEWDOT_MANAGER_MCTEST, config.betas) ||
isCurrentUserMemberOfAnyPolicy(),
};
@@ -1923,6 +1925,7 @@ function getRestrictedLogins(config: GetOptionsConfig, options: OptionList, canS
function getValidOptions(
options: OptionList,
draftComments: OnyxCollection | undefined,
+ nvpDismissedProductTraining: OnyxEntry,
{
excludeLogins = {},
includeSelectedOptions = false,
@@ -1941,7 +1944,7 @@ function getValidOptions(
}: GetOptionsConfig = {},
countryCode: number = CONST.DEFAULT_COUNTRY_CODE,
): Options {
- const restrictedLogins = getRestrictedLogins(config, options, canShowManagerMcTest);
+ const restrictedLogins = getRestrictedLogins(config, options, canShowManagerMcTest, nvpDismissedProductTraining);
// Gather shared configs:
const loginsToExclude: Record = {
@@ -2135,6 +2138,7 @@ function getValidOptions(
type SearchOptionsConfig = {
options: OptionList;
+ nvpDismissedProductTraining: OnyxEntry;
draftComments: OnyxCollection;
betas?: Beta[];
isUsedInChatFinder?: boolean;
@@ -2154,6 +2158,7 @@ type SearchOptionsConfig = {
function getSearchOptions({
options,
draftComments,
+ nvpDismissedProductTraining,
betas,
isUsedInChatFinder = true,
includeReadOnly = true,
@@ -2171,6 +2176,7 @@ function getSearchOptions({
const optionList = getValidOptions(
options,
draftComments,
+ nvpDismissedProductTraining,
{
betas,
includeRecentReports,
@@ -2231,6 +2237,7 @@ type GetAttendeeOptionsParams = {
attendees: Attendee[];
recentAttendees: Attendee[];
draftComments: OnyxCollection;
+ nvpDismissedProductTraining: OnyxEntry;
includeOwnedWorkspaceChats: boolean;
includeP2P: boolean;
includeInvoiceRooms: boolean;
@@ -2245,6 +2252,7 @@ function getAttendeeOptions({
attendees,
recentAttendees,
draftComments,
+ nvpDismissedProductTraining,
includeOwnedWorkspaceChats = false,
includeP2P = true,
includeInvoiceRooms = false,
@@ -2282,6 +2290,7 @@ function getAttendeeOptions({
return getValidOptions(
{reports, personalDetails},
draftComments,
+ nvpDismissedProductTraining,
{
betas,
selectedOptions: attendees.map((attendee) => ({...attendee, login: attendee.email})),
@@ -2331,6 +2340,7 @@ function formatMemberForList(member: SearchOptionData): MemberForList {
*/
function getMemberInviteOptions(
personalDetails: Array>,
+ nvpDismissedProductTraining: OnyxEntry,
betas: Beta[] = [],
excludeLogins: Record = {},
includeSelectedOptions = false,
@@ -2339,6 +2349,7 @@ function getMemberInviteOptions(
return getValidOptions(
{personalDetails, reports: []},
undefined,
+ nvpDismissedProductTraining,
{
betas,
includeP2P: true,
diff --git a/src/libs/PaymentUtils.ts b/src/libs/PaymentUtils.ts
index 09f9b00bd1652..f404faefd44c2 100644
--- a/src/libs/PaymentUtils.ts
+++ b/src/libs/PaymentUtils.ts
@@ -5,6 +5,7 @@ import type {Merge, ValueOf} from 'type-fest';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import getBankIcon from '@components/Icon/BankIcons';
import type {ContinueActionParams, PaymentMethod as KYCPaymentMethod} from '@components/KYCWall/types';
+import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type {BankAccountMenuItem} from '@components/Search/types';
import type {ThemeStyles} from '@styles/index';
@@ -18,8 +19,6 @@ import type PaymentMethod from '@src/types/onyx/PaymentMethod';
import type {ACHAccount} from '@src/types/onyx/Policy';
import {setPersonalBankAccountContinueKYCOnSuccess} from './actions/BankAccounts';
import {approveMoneyRequest} from './actions/IOU';
-// eslint-disable-next-line @typescript-eslint/no-deprecated
-import {translateLocal} from './Localize';
import BankAccountModel from './models/BankAccount';
import Navigation from './Navigation/Navigation';
import {shouldRestrictUserBillableActions} from './SubscriptionUtils';
@@ -64,19 +63,21 @@ function hasExpensifyPaymentMethod(fundList: Record, bankAccountLi
return validBankAccount || (shouldIncludeDebitCard && validDebitCard);
}
-function getPaymentMethodDescription(accountType: AccountType, account: BankAccount['accountData'] | Fund['accountData'] | ACHAccount, bankCurrency?: string): string {
+function getPaymentMethodDescription(
+ accountType: AccountType,
+ account: BankAccount['accountData'] | Fund['accountData'] | ACHAccount,
+ translate: LocalizedTranslate,
+ bankCurrency?: string,
+): string {
if (account) {
if (accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && 'accountNumber' in account) {
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- return `${bankCurrency ? `${bankCurrency} ${CONST.DOT_SEPARATOR} ` : ''}${translateLocal('paymentMethodList.accountLastFour')} ${account.accountNumber?.slice(-4)}`;
+ return `${bankCurrency ? `${bankCurrency} ${CONST.DOT_SEPARATOR} ` : ''}${translate('paymentMethodList.accountLastFour')} ${account.accountNumber?.slice(-4)}`;
}
if (accountType === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT && 'accountNumber' in account) {
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- return `${translateLocal('paymentMethodList.accountLastFour')} ${account.accountNumber?.slice(-4)}`;
+ return `${translate('paymentMethodList.accountLastFour')} ${account.accountNumber?.slice(-4)}`;
}
if (accountType === CONST.PAYMENT_METHODS.DEBIT_CARD && 'cardNumber' in account) {
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- return `${translateLocal('paymentMethodList.cardLastFour')} ${account.cardNumber?.slice(-4)}`;
+ return `${translate('paymentMethodList.cardLastFour')} ${account.cardNumber?.slice(-4)}`;
}
}
return '';
@@ -85,7 +86,7 @@ function getPaymentMethodDescription(accountType: AccountType, account: BankAcco
/**
* Get the PaymentMethods list
*/
-function formatPaymentMethods(bankAccountList: Record, fundList: Record | Fund[], styles: ThemeStyles): PaymentMethod[] {
+function formatPaymentMethods(bankAccountList: Record, fundList: Record | Fund[], styles: ThemeStyles, translate: LocalizedTranslate): PaymentMethod[] {
const combinedPaymentMethods: PaymentMethod[] = [];
Object.values(bankAccountList).forEach((bankAccount) => {
@@ -101,7 +102,7 @@ function formatPaymentMethods(bankAccountList: Record, fund
});
combinedPaymentMethods.push({
...bankAccount,
- description: getPaymentMethodDescription(bankAccount?.accountType, bankAccount.accountData, bankAccount.bankCurrency),
+ description: getPaymentMethodDescription(bankAccount?.accountType, bankAccount.accountData, translate, bankAccount.bankCurrency),
icon,
iconSize,
iconHeight,
@@ -114,7 +115,7 @@ function formatPaymentMethods(bankAccountList: Record, fund
const {icon, iconSize, iconHeight, iconWidth, iconStyles} = getBankIcon({bankName: card?.accountData?.bank, isCard: true, styles});
combinedPaymentMethods.push({
...card,
- description: getPaymentMethodDescription(card?.accountType, card.accountData),
+ description: getPaymentMethodDescription(card?.accountType, card.accountData, translate),
icon,
iconSize,
iconHeight,
diff --git a/src/libs/PerDiemRequestUtils.ts b/src/libs/PerDiemRequestUtils.ts
index d0c8a423bc4d9..8749a00a68bfb 100644
--- a/src/libs/PerDiemRequestUtils.ts
+++ b/src/libs/PerDiemRequestUtils.ts
@@ -1,11 +1,10 @@
import {addDays, differenceInDays, differenceInMinutes, format, isSameDay, startOfDay} from 'date-fns';
import lodashSortBy from 'lodash/sortBy';
import type {OnyxEntry} from 'react-native-onyx';
+import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import CONST from '@src/CONST';
import type {Report, Transaction} from '@src/types/onyx';
import type {CustomUnit, Rate} from '@src/types/onyx/Policy';
-// eslint-disable-next-line @typescript-eslint/no-deprecated
-import {translateLocal} from './Localize';
import type {OptionTree, SectionBase} from './OptionsListUtils';
import {getPolicy} from './PolicyUtils';
import {isPolicyExpenseChat} from './ReportUtils';
@@ -85,12 +84,14 @@ function getDestinationListSections({
selectedOptions = [],
recentlyUsedDestinations = [],
maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
+ translate,
}: {
destinations: Rate[];
selectedOptions?: Destination[];
searchValue?: string;
recentlyUsedDestinations?: string[];
maxRecentReportsToShow?: number;
+ translate: LocalizedTranslate;
}): DestinationTreeSection[] {
const sortedDestinations: Destination[] = lodashSortBy(destinations, 'name').map((rate) => ({
name: rate.name ?? '',
@@ -128,6 +129,7 @@ function getDestinationListSections({
});
}
+ // eslint-disable-next-line unicorn/prefer-set-has
const selectedOptionRateIDs = selectedOptions.map((selectedOption) => selectedOption.rateID);
if (sortedDestinations.length < CONST.STANDARD_LIST_ITEM_LIMIT) {
@@ -155,8 +157,7 @@ function getDestinationListSections({
const data = getDestinationOptionTree(cutRecentlyUsedDestinations);
destinationSections.push({
// "Recent" section
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- title: translateLocal('common.recent'),
+ title: translate('common.recent'),
shouldShow: true,
data,
indexOffset: data.length,
@@ -166,8 +167,7 @@ function getDestinationListSections({
const data = getDestinationOptionTree(sortedDestinations);
destinationSections.push({
// "All" section when items amount more than the threshold
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- title: translateLocal('common.all'),
+ title: translate('common.all'),
shouldShow: true,
data,
indexOffset: data.length,
diff --git a/src/libs/PolicyDistanceRatesUtils.ts b/src/libs/PolicyDistanceRatesUtils.ts
index b0139e6765eb2..eef27ee44aa39 100644
--- a/src/libs/PolicyDistanceRatesUtils.ts
+++ b/src/libs/PolicyDistanceRatesUtils.ts
@@ -70,6 +70,7 @@ function buildOnyxDataForPolicyDistanceRateUpdates(policyID: string, customUnit:
const optimisticRates: Record> = {};
const successRates: Record> = {};
const failureRates: Record> = {};
+ // eslint-disable-next-line unicorn/prefer-set-has
const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
for (const rateID of Object.keys(customUnit.rates)) {
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index a7f4f98cccd47..cb9e3c55e27f4 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -30,7 +30,6 @@ import type {
Tenant,
} from '@src/types/onyx/Policy';
import type PolicyEmployee from '@src/types/onyx/PolicyEmployee';
-import type {SearchPolicy} from '@src/types/onyx/SearchResults';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {hasSynchronizationErrorMessage, isConnectionUnverified} from './actions/connections';
import {shouldShowQBOReimbursableExportDestinationAccountError} from './actions/connections/QuickbooksOnline';
@@ -99,7 +98,7 @@ function getActivePoliciesWithExpenseChatAndPerDiemEnabled(policies: OnyxCollect
/**
* Checks if the current user is an admin of the policy.
*/
-const isPolicyAdmin = (policy: OnyxInputOrEntry | SearchPolicy, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.ADMIN;
+const isPolicyAdmin = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.ADMIN;
/**
* Checks if we have any errors stored within the policy?.employeeList. Determines whether we should show a red brick road error or not.
@@ -253,7 +252,7 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, isConnecti
return undefined;
}
-function getPolicyRole(policy: OnyxInputOrEntry | SearchPolicy, currentUserLogin: string | undefined): string | undefined {
+function getPolicyRole(policy: OnyxInputOrEntry, currentUserLogin: string | undefined): string | undefined {
if (policy?.role) {
return policy.role;
}
@@ -544,7 +543,7 @@ function isPendingDeletePolicy(policy: OnyxEntry): boolean {
return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
}
-function isPaidGroupPolicy(policy: OnyxInputOrEntry | SearchPolicy): boolean {
+function isPaidGroupPolicy(policy: OnyxInputOrEntry): boolean {
return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE;
}
@@ -576,14 +575,14 @@ function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry | SearchPolicy): boolean {
+function isInstantSubmitEnabled(policy: OnyxInputOrEntry): boolean {
return policy?.autoReporting === true && policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT;
}
/**
* Checks if policy's scheduled submit / auto reporting frequency is not "instant".
*/
-function isDelayedSubmissionEnabled(policy: OnyxInputOrEntry | SearchPolicy): boolean {
+function isDelayedSubmissionEnabled(policy: OnyxInputOrEntry): boolean {
return policy?.autoReporting === true && policy?.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT;
}
@@ -596,7 +595,7 @@ function isDelayedSubmissionEnabled(policy: OnyxInputOrEntry | SearchPol
*
* Note that "daily" and "manual" only exist as options for the API, not in the database or Onyx.
*/
-function getCorrectedAutoReportingFrequency(policy: OnyxInputOrEntry | SearchPolicy): ValueOf | undefined {
+function getCorrectedAutoReportingFrequency(policy: OnyxInputOrEntry): ValueOf | undefined {
if (policy?.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE) {
return policy?.autoReportingFrequency;
}
@@ -613,7 +612,7 @@ function getCorrectedAutoReportingFrequency(policy: OnyxInputOrEntry | S
/**
* Checks if policy's approval mode is "optional", a.k.a. "Submit & Close"
*/
-function isSubmitAndClose(policy: OnyxInputOrEntry | SearchPolicy): boolean {
+function isSubmitAndClose(policy: OnyxInputOrEntry): boolean {
return policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL;
}
@@ -628,7 +627,7 @@ function isControlOnAdvancedApprovalMode(policy: OnyxInputOrEntry): bool
/**
* Checks if policy has Dynamic External Workflow enabled
*/
-function hasDynamicExternalWorkflow(policy: OnyxEntry | SearchPolicy): boolean {
+function hasDynamicExternalWorkflow(policy: OnyxEntry): boolean {
return policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.DYNAMICEXTERNAL;
}
@@ -712,7 +711,7 @@ function isPolicyFeatureEnabled(policy: OnyxEntry, featureName: PolicyFe
return !!policy?.[featureName];
}
-function getApprovalWorkflow(policy: OnyxEntry | SearchPolicy): ValueOf