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 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.2.42 + 9.2.46 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.2.42.0 + 9.2.46.0 FullStory OrgId 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.46 CFBundleVersion - 9.2.42.0 + 9.2.46.0 NSExtension NSExtensionPointIdentifier 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.46 CFBundleVersion - 9.2.42.0 + 9.2.46.0 NSExtension NSExtensionAttributes 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' && ( -