diff --git a/.eslintrc.js b/.eslintrc.js index 5451cfff6534..0661183101ab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -194,6 +194,7 @@ module.exports = { { selector: ['parameter', 'method'], format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allow', }, ], '@typescript-eslint/ban-types': [ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d34e4ebbf895..36b921570e7f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -103,9 +103,9 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases) - [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. - [ ] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account. -- [ ] If the PR modifies the form input styles: +- [ ] If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles: - [ ] I verified that all the inputs inside a form are aligned with each other. - - [ ] I added `Design` label so the design team can review the changes. + - [ ] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes. - [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page. - [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps. diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index e267769dc457..528a0a11498a 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -283,14 +283,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -325,11 +325,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -359,9 +359,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index dd2aef38e1ee..f042dbb38a91 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -395,14 +395,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -437,11 +437,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -471,9 +471,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index 82092be7e0eb..8e10f8b1d8b6 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -362,14 +362,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -404,11 +404,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -438,9 +438,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js index 1752ae62f86c..4441348a3c36 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js @@ -40,11 +40,8 @@ async function run() { // Next, we generate the checklist body let checklistBody = ''; - let checklistAssignees = []; if (shouldCreateNewDeployChecklist) { - const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber)); - checklistBody = issueBody; - checklistAssignees = issueAssignees; + checklistBody = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber)); } else { // Generate the updated PR list, preserving the previous state of `isVerified` for existing PRs const PRList = _.reduce( @@ -97,7 +94,7 @@ async function run() { } const didVersionChange = newVersionTag !== currentChecklistData.tag; - const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody( + checklistBody = await GithubUtils.generateStagingDeployCashBody( newVersionTag, _.pluck(PRList, 'url'), _.pluck(_.where(PRList, {isVerified: true}), 'url'), @@ -108,8 +105,6 @@ async function run() { didVersionChange ? false : currentChecklistData.isFirebaseChecked, didVersionChange ? false : currentChecklistData.isGHStatusChecked, ); - checklistBody = issueBody; - checklistAssignees = issueAssignees; } // Finally, create or update the checklist @@ -124,7 +119,7 @@ async function run() { ...defaultPayload, title: `Deploy Checklist: New Expensify ${format(new Date(), CONST.DATE_FORMAT_STRING)}`, labels: [CONST.LABELS.STAGING_DEPLOY], - assignees: [CONST.APPLAUSE_BOT].concat(checklistAssignees), + assignees: [CONST.APPLAUSE_BOT], }); console.log(`Successfully created new StagingDeployCash! 🎉 ${newChecklist.html_url}`); return newChecklist; diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 9c9a42709af0..154dacbdc3c3 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -49,11 +49,8 @@ async function run() { // Next, we generate the checklist body let checklistBody = ''; - let checklistAssignees = []; if (shouldCreateNewDeployChecklist) { - const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber)); - checklistBody = issueBody; - checklistAssignees = issueAssignees; + checklistBody = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber)); } else { // Generate the updated PR list, preserving the previous state of `isVerified` for existing PRs const PRList = _.reduce( @@ -106,7 +103,7 @@ async function run() { } const didVersionChange = newVersionTag !== currentChecklistData.tag; - const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody( + checklistBody = await GithubUtils.generateStagingDeployCashBody( newVersionTag, _.pluck(PRList, 'url'), _.pluck(_.where(PRList, {isVerified: true}), 'url'), @@ -117,8 +114,6 @@ async function run() { didVersionChange ? false : currentChecklistData.isFirebaseChecked, didVersionChange ? false : currentChecklistData.isGHStatusChecked, ); - checklistBody = issueBody; - checklistAssignees = issueAssignees; } // Finally, create or update the checklist @@ -133,7 +128,7 @@ async function run() { ...defaultPayload, title: `Deploy Checklist: New Expensify ${format(new Date(), CONST.DATE_FORMAT_STRING)}`, labels: [CONST.LABELS.STAGING_DEPLOY], - assignees: [CONST.APPLAUSE_BOT].concat(checklistAssignees), + assignees: [CONST.APPLAUSE_BOT], }); console.log(`Successfully created new StagingDeployCash! 🎉 ${newChecklist.html_url}`); return newChecklist; @@ -439,14 +434,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -481,11 +476,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -515,9 +510,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index e4f7634bd849..ea56ff5f4ebd 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -321,14 +321,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -363,11 +363,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -397,9 +397,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index f941c9524856..f272929d536a 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -377,14 +377,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -419,11 +419,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -453,9 +453,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index f4168af28802..b8d7d821d64e 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -329,14 +329,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -371,11 +371,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -405,9 +405,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/getReleaseBody/index.js b/.github/actions/javascript/getReleaseBody/index.js index 547aafe23038..cc1321ce5cd5 100644 --- a/.github/actions/javascript/getReleaseBody/index.js +++ b/.github/actions/javascript/getReleaseBody/index.js @@ -329,14 +329,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -371,11 +371,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -405,9 +405,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index 4938b5bb7745..8124c5795a5a 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -313,14 +313,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -355,11 +355,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -389,9 +389,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 2e6ab7e018dd..36cd0aaefe4a 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -478,14 +478,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -520,11 +520,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -554,9 +554,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 9dd23d68ca0a..329e0d3aad5d 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -388,14 +388,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -430,11 +430,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -464,9 +464,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 42196053f63f..6a5f89badb5e 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -283,14 +283,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -325,11 +325,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -359,9 +359,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 22335b36bd2b..322b529b89bf 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -283,14 +283,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -325,11 +325,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -359,9 +359,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index 239f20c9d258..ba188d3a2b86 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -283,14 +283,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -325,11 +325,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -359,9 +359,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js index e988167850ec..0cd407c78153 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.js @@ -250,14 +250,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', - // 'https://github.com/Expensify/App/pull/9642': 'mountiny' + // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], + // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = pr.merged_by.login; + map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); return map; }, {}, @@ -292,11 +292,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (merger, URL) => { - const mergerMention = `@${merger}`; + _.each(internalQAPRMap, (assignees, URL) => { + const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` - ${mergerMention}`; + issueBody += ` -${assigneeMentions}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -326,9 +326,7 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - const issueAssignees = _.values(internalQAPRMap); - const issue = {issueBody, issueAssignees}; - return issue; + return issueBody; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 8a47ea4bb220..fd814ad69a7c 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -183,7 +183,7 @@ jobs: run: npm run e2e-test-runner-build - name: Copy e2e code into zip folder - run: cp tests/e2e/dist/index.js zip/testRunner.js + run: cp tests/e2e/dist/index.js zip/testRunner.ts - name: Zip everything in the zip directory up run: zip -qr App.zip ./zip diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index 85fb866b05c4..9887943c77e0 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -69,9 +69,6 @@ jobs: uses: ./.github/actions/javascript/getGraphiteString - name: Send graphite data - env: - GRAPHITE_SERVER: ${{ vars.GRAPHITE_SERVER }} - GRAPHITE_PORT: ${{ vars.GRAPHITE_PORT }} # run only when merged to main if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' - run: echo -e "${{ steps.saveGraphiteString.outputs.GRAPHITE_STRING }}" | nc -q0 "$GRAPHITE_SERVER" "$GRAPHITE_PORT" + run: echo -e "${{ steps.saveGraphiteString.outputs.GRAPHITE_STRING }}" | nc -q0 stats.expensify.com 3003 diff --git a/README.md b/README.md index 72736b3fedb7..400260393bc1 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,79 @@ Our React Native Android app now uses the `Hermes` JS engine which requires your To make it easier to test things in web, we expose the Onyx object to the window, so you can easily do `Onyx.set('bla', 1)`. +---- + +# Release Profiler +Often, performance issue debugging occurs in debug builds, which can introduce errors from elements such as JS Garbage Collection, Hermes debug markers, or LLDB pauses. + +`react-native-release-profiler` facilitates profiling within release builds for accurate local problem-solving and broad performance analysis in production to spot regressions or collect extensive device data. Therefore, we will utilize the production build version + +### Getting Started with Source Maps +To accurately profile your application, generating source maps for Android and iOS is crucial. Here's how to enable them: +1. Enable source maps on Android +Ensure the following is set in your app's `android/app/build.gradle` file. + + ```jsx + project.ext.react = [ + enableHermes: true, + hermesFlagsRelease: ["-O", "-output-source-map"], // <-- here, plus whichever flag was required to set this away from default + ] + ``` + +2. Enable source maps on IOS +Within Xcode head to the build phase - `Bundle React Native code and images`. + + ```jsx + export SOURCEMAP_FILE="$(pwd)/../main.jsbundle.map" // <-- here; + + export NODE_BINARY=node + ../node_modules/react-native/scripts/react-native-xcode.sh + ``` +3. Install the necessary packages and CocoaPods dependencies: + ```jsx + npm i && npm run pod-install + ``` +7. Depending on the platform you are targeting, run your Android/iOS app in production mode. +8. Upon completion, the generated source map can be found at: + Android: `android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map` + IOS: `main.jsbundle.map` + +### Recording a Trace: +1. Ensure you have generated the source map as outlined above. +2. Launch the app in production mode. +2. Navigate to the feature you wish to profile. +3. Initiate the profiling session by tapping with four fingers to open the menu and selecting **`Use Profiling`**. +4. Close the menu and interact with the app. +5. After completing your interactions, tap with four fingers again and select to stop profiling. +6. You will be presented with a **`Share`** option to export the trace, which includes a trace file (`Profile.cpuprofile`) and build info (`AppInfo.json`). + +Build info: +```jsx +{ + appVersion: "1.0.0", + environment: "production", + platform: "IOS", + totalMemory: "3GB", + usedMemory: "300MB" +} +``` + +### How to symbolicate trace record: +1. You have two files: `AppInfo.json` and `Profile.cpuprofile` +2. Place the `Profile.cpuprofile` file at the root of your project. +3. If you have already generated a source map from the steps above for this branch, you can skip to the next step. Otherwise, obtain the app version from `AppInfo.json` switch to that branch and generate the source map as described. + +`IMPORTANT:` You should generate the source map from the same branch as the trace was recorded. + +4. Use the following commands to symbolicate the trace for Android and iOS, respectively: +Android: `npm run symbolicate-release:android` +IOS: `npm run symbolicate-release:ios` +5. A new file named `Profile_trace_for_-converted.json` will appear in your project's root folder. +6. Open this file in your tool of choice: + - SpeedScope ([https://www.speedscope.app](https://www.speedscope.app/)) + - Perfetto UI (https://ui.perfetto.dev/) + - Google Chrome's Tracing UI (chrome://tracing) + --- # App Structure and Conventions diff --git a/android/app/build.gradle b/android/app/build.gradle index f714ed005740..b792f7830ea4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001044904 - versionName "1.4.49-4" + versionCode 1001045103 + versionName "1.4.51-3" } flavorDimensions "default" @@ -181,7 +181,6 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("com.facebook.react:flipper-integration") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") diff --git a/assets/fonts/web/ExpensifyNewKansas-Medium.woff b/assets/fonts/web/ExpensifyNewKansas-Medium.woff index bd842c5ecb1d..9e4258763f58 100644 Binary files a/assets/fonts/web/ExpensifyNewKansas-Medium.woff and b/assets/fonts/web/ExpensifyNewKansas-Medium.woff differ diff --git a/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 b/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 index dba1df7e971e..1f65d0df0fcb 100644 Binary files a/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 and b/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 differ diff --git a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff index d3e7d9e82e15..5bab939ee71d 100644 Binary files a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff and b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff differ diff --git a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 index 94a0e04fa3b2..589edf3bc922 100644 Binary files a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 and b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 differ diff --git a/assets/images/simple-illustrations/simple-illustration__accounting.svg b/assets/images/simple-illustrations/simple-illustration__accounting.svg new file mode 100644 index 000000000000..f7634141e966 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__accounting.svg @@ -0,0 +1,32 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__car.svg b/assets/images/simple-illustrations/simple-illustration__car.svg new file mode 100644 index 000000000000..2d420be6c3a9 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__car.svg @@ -0,0 +1,25 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__coins.svg b/assets/images/simple-illustrations/simple-illustration__coins.svg new file mode 100644 index 000000000000..5350886402c6 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__coins.svg @@ -0,0 +1,26 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__pencil.svg b/assets/images/simple-illustrations/simple-illustration__pencil.svg new file mode 100644 index 000000000000..8d9f06991612 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__pencil.svg @@ -0,0 +1,20 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__workflows.svg b/assets/images/simple-illustrations/simple-illustration__workflows.svg index 47d30d54310f..b684c58126f7 100644 --- a/assets/images/simple-illustrations/simple-illustration__workflows.svg +++ b/assets/images/simple-illustrations/simple-illustration__workflows.svg @@ -1 +1,153 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/babel.config.js b/babel.config.js index 2a09d086dc5c..7e90fca1c9be 100644 --- a/babel.config.js +++ b/babel.config.js @@ -82,7 +82,7 @@ const metro = { }; /* - * We use Flipper, and react-native-performance to capture/monitor stats + * We use and react-native-performance to capture/monitor stats * By default is disabled in production as it adds small overhead * When CAPTURE_METRICS is set we're explicitly saying that we want to capture metrics * To enable the for release builds we add these aliases */ diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 170198987793..2fed8a477aab 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -18,7 +18,6 @@ const includeModules = [ '@react-native-picker', 'react-native-modal', 'react-native-gesture-handler', - 'react-native-flipper', 'react-native-google-places-autocomplete', 'react-native-qrcode-svg', 'react-native-view-shot', diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md index ab4b215516b9..4ff1f01b1475 100644 --- a/contributingGuides/REVIEWER_CHECKLIST.md +++ b/contributingGuides/REVIEWER_CHECKLIST.md @@ -51,9 +51,9 @@ - [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases) - [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. - [ ] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account. -- [ ] If the PR modifies the form input styles: +- [ ] If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles: - [ ] I verified that all the inputs inside a form are aligned with each other. - - [ ] I added `Design` label so the design team can review the changes. + - [ ] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes. - [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page. - [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps. - [ ] I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR. diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index c01243e777d5..3d0d16b00587 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -29,15 +29,15 @@ platforms: icon: /assets/images/bank-card.svg description: Find out how to connect Expensify to your financial institutions, track credit card transactions, and best practices for reconciling company cards. - - href: billing-and-subscriptions - title: Billing & Subscriptions + - href: expensify-billing + title: Expensify Billing icon: /assets/images/subscription-annual.svg - description: Here is where you can review Expensify's billing and subscription options, plan types, and payment methods. + description: Review Expensify's subscription options, plan types, and payment methods. - - href: expense-and-report-features - title: Expense & Report Features + - href: reports + title: Reports icon: /assets/images/money-receipt.svg - description: From enabling automatic expense auditing to tracking attendees, here is where you can review tips and tutorials to streamline expense management. + description: Set approval workflows and use Expensify’s automated report features. - href: expensify-card title: Expensify Card @@ -49,10 +49,10 @@ platforms: icon: /assets/images/handshake.svg description: Discover how to get the most out of Expensify as an ExpensifyApproved! accountant partner. Learn how to set up your clients, receive CPE credits, and take advantage of your partner discount. - - href: get-paid-back - title: Get Paid Back + - href: expenses + title: Expenses icon: /assets/images/money-into-wallet.svg - description: Whether you submit an expense report or an invoice, find out here how to ensure a smooth and timely payback process every time. + description: Learn more about expense tracking and submission. - href: insights-and-custom-reporting title: Insights & Custom Reporting @@ -64,20 +64,25 @@ platforms: icon: /assets/images/workflow.svg description: Enhance Expensify’s capabilities by integrating it with your accounting or HR software. Here is where you can learn more about creating a synchronized financial management ecosystem. - - href: manage-employees-and-report-approvals - title: Manage Employees & Report Approvals + - href: copilots-and-delegates + title: Copilots & Delegates icon: /assets/images/envelope-receipt.svg - description: Master the art of overseeing employees and reports by utilizing Expensify’s automation features and approval workflows. + description: Assign Copilots and delegate report approvals. - href: send-payments title: Send Payments icon: /assets/images/send-money.svg description: Uncover step-by-step guidance on sending direct reimbursements to employees, paying an invoice to a vendor, and utilizing third-party payment options. - - href: workspace-and-domain-settings - title: Workspace & Domain Settings + - href: workspaces + title: Workspaces icon: /assets/images/shield.svg - description: Discover how to set up and manage workspace, define user permissions, and implement compliance rules to maintain a secure and compliant financial management landscape. + description: Configure rules, settings, and limits for your company’s spending. + + - href: domains + title: Domains + icon: /assets/images/domains.svg + description: Claim and verify your company’s domain to access additional management and security features. - href: new-expensify title: New Expensify diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index ea18acef7c23..ec0f76801bc7 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -508,6 +508,7 @@ button { .info { padding: 12px; + margin-bottom: 20px; border-radius: 8px; background-color: $color-highlightBG; color: $color-text; diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows.md b/docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows.md rename to docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md b/docs/articles/expensify-classic/copilots-and-delegates/Approving-Reports.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md rename to docs/articles/expensify-classic/copilots-and-delegates/Approving-Reports.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members.md b/docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members.md rename to docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md b/docs/articles/expensify-classic/copilots-and-delegates/Removing-Members.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md rename to docs/articles/expensify-classic/copilots-and-delegates/Removing-Members.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md b/docs/articles/expensify-classic/copilots-and-delegates/User-Roles.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md rename to docs/articles/expensify-classic/copilots-and-delegates/User-Roles.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md b/docs/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md rename to docs/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate.md diff --git a/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md b/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md new file mode 100644 index 000000000000..bb4b21547892 --- /dev/null +++ b/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md @@ -0,0 +1,50 @@ +--- +title: Claim and verify a domain +description: Grant specific employee permissions by claiming a private domain and verifying it in Expensify +--- +
+ +If you have a private domain (like yourcompany.com), you can add the domain to your Expensify account. Claiming a domain allows you to grant specific permissions to accounts that include the domain in their email address (for example, if your domain is yourcompany.com, anyone who signs up under this domain—like yourname@yourcompany.com—will have these domain rules applied to their account). + +Claiming a domain also allows you to: +* Import and reconcile company credit cards and Expensify Cards +* Add company credit card and Expensify Card rules and restrictions + +Once you verify your domain, you’ll be able to: +* Assign delegates for employees who are on vacation +* Delete employee Expensify accounts +* Enable SAML / SSO settings for secure log in + +{% include info.html %} +You can claim and verify private domains only. Public domains (like gmail.com) cannot be used to create a domain. +{% include end-info.html %} + +# Step 1: Claim domain + +
    +
  1. Hover over Settings, then click Domains.
  2. +
  3. Click New Domain.
  4. +
  5. Enter your domain name (e.g., yourcompany.com).
  6. +
  7. Click Submit.
  8. +
+ +# Step 2: Verify domain ownership + +{% include info.html %} +To complete this step, you must have a Control workspace, and you’ll need access to your domain provider account (GoDaddy, Wix, GSuite, etc.). If you don’t verify the domain, you will still have access to the domain to add and manage credit card expenses and domain admins, but you will not be able to invite members, add groups, use domain reporting tools, set delegates for employees on vacation, or enable SAML SSO. For more guidance on how to complete this process for a specific provider, check the provider’s website.{% include end-info.html %} + +
    +
  1. Log in to your DNS service provider (which may be the website you purchased the domain from or that currently hosts the domain, like NameCheap, GoDaddy, DNSMadeEasy, or Amazon Route53. You may need to contact your company’s IT department if your domain is managed internally).
  2. +
  3. Find the page for DNS records, which might be labeled as DNS Management or Zone File Editor.
  4. +
  5. Add a new TXT record and set the value as 532F6180D8.
  6. +
  7. Save your changes.
  8. +
  9. In Expensify, click the Domain Members tab and click Verify.
  10. +
+ +After successful verification, an email will be sent to all members of the Expensify domain to inform them that their accounts will be under domain control (i.e. the rules set for the domain will affect their account). + +# Add another domain + +To add an additional domain, you’ll have to first add your email address that is connected with your domain as your [primary or secondary email] (https://help.expensify.com/articles/expensify-classic/settings/account-settings/Change-or-add-email-address) (for example, if your domain is yourcompany.com, then you want to add and verify your email address @yourcompany.com as your primary or secondary email address). Then you can complete the steps above to add the domain. + +
\ No newline at end of file diff --git a/docs/articles/expensify-classic/domains/Create-A-Group.md b/docs/articles/expensify-classic/domains/Create-A-Group.md new file mode 100644 index 000000000000..fb70faffa27e --- /dev/null +++ b/docs/articles/expensify-classic/domains/Create-A-Group.md @@ -0,0 +1,26 @@ +--- +title: Create a group +description: How to set different rules for different members of your domain +--- +
+ +To set different domain rules for different members, you can place them into groups. For example, many organizations create different groups for employees and managers since they generally need different domain permissions. + +To create a group, + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Click the **Groups** tab on the left. +4. Click **Create Group**. +5. Select all of the group settings and permissions. + - **Permission Group Name**: Enter a name for the group + - **Default Group**: Determine if new domain members will be automatically added to this group. + - **Strictly enforce expense workspace rules**: Determine if all expense rules must be met before people in this group can submit a report. + - **Restrict primary login selection**: Determine if members of this group will be restricted from using a personal email address to access their Expensify account. + - **Restrict expense workspace creation/removal**: Determine if members of this group will be allowed to create new workspaces. + - **Preferred workspace**: Determine if this group will automatically have their expenses and reports posted to a specific workspace. + - **Set preferred workspace to**: If preferred workspace is enabled, select which workspace members of this group will have set as their preferred workspace. + - **Expensify Card Preferred Workspace**: If preferred workspace is enabled, determine if Expensify Card transactions for this group will be posted to the preferred workspace listed for the Expensify Card instead of the preferred workspace listed in the above settings. +6. Click **Save**. + +
\ No newline at end of file diff --git a/docs/articles/expensify-classic/get-paid-back/Distance-Tracking.md b/docs/articles/expensify-classic/expenses/Distance-Tracking.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/Distance-Tracking.md rename to docs/articles/expensify-classic/expenses/Distance-Tracking.md diff --git a/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md b/docs/articles/expensify-classic/expenses/Per-Diem-Expenses.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md rename to docs/articles/expensify-classic/expenses/Per-Diem-Expenses.md diff --git a/docs/articles/expensify-classic/get-paid-back/Referral-Program.md b/docs/articles/expensify-classic/expenses/Referral-Program.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/Referral-Program.md rename to docs/articles/expensify-classic/expenses/Referral-Program.md diff --git a/docs/articles/expensify-classic/get-paid-back/Trips.md b/docs/articles/expensify-classic/expenses/Trips.md similarity index 96% rename from docs/articles/expensify-classic/get-paid-back/Trips.md rename to docs/articles/expensify-classic/expenses/Trips.md index ccfbe1592291..04f95c96eb44 100644 --- a/docs/articles/expensify-classic/get-paid-back/Trips.md +++ b/docs/articles/expensify-classic/expenses/Trips.md @@ -34,6 +34,6 @@ To view details about your past or upcoming trips, follow these steps within the If you received your receipt in an email that is not associated with your Expensify account, you can add this email as a [secondary login](https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#how-to-add-a-secondary-login) to directly forward the receipt into your account. ## How do I upload Trip receipts that were not sent to me by email? -If your trip receipt was not sent to you by email, you can manually upload the receipt to your account. Check out this resource for more information on [manually uploading receipts](https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts#manually-upload). +If your trip receipt was not sent to you by email, you can manually upload the receipt to your account. Check out this resource for more information on [manually uploading receipts](https://help.expensify.com/articles/expensify-classic/expenses/expenses/Upload-Receipts#manually-upload). {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md b/docs/articles/expensify-classic/expenses/expenses/Apply-Tax.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md rename to docs/articles/expensify-classic/expenses/expenses/Apply-Tax.md diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md b/docs/articles/expensify-classic/expenses/expenses/Create-Expenses.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md rename to docs/articles/expensify-classic/expenses/expenses/Create-Expenses.md diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md b/docs/articles/expensify-classic/expenses/expenses/Merge-Expenses.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md rename to docs/articles/expensify-classic/expenses/expenses/Merge-Expenses.md diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md b/docs/articles/expensify-classic/expenses/expenses/Upload-Receipts.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md rename to docs/articles/expensify-classic/expenses/expenses/Upload-Receipts.md diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md b/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md rename to docs/articles/expensify-classic/expenses/reports/Create-A-Report.md diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md b/docs/articles/expensify-classic/expenses/reports/Reimbursements.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md rename to docs/articles/expensify-classic/expenses/reports/Reimbursements.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Annual-Subscription.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription.md rename to docs/articles/expensify-classic/expensify-billing/Annual-Subscription.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md rename to docs/articles/expensify-classic/expensify-billing/Billing-Overview.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md b/docs/articles/expensify-classic/expensify-billing/Billing-Owner.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md rename to docs/articles/expensify-classic/expensify-billing/Billing-Owner.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md rename to docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md b/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md rename to docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Individual-Subscription.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md rename to docs/articles/expensify-classic/expensify-billing/Individual-Subscription.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Pay-Per-Use-Subscription.md similarity index 93% rename from docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md rename to docs/articles/expensify-classic/expensify-billing/Pay-Per-Use-Subscription.md index 326ce7fe33ab..fac605ada1bd 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md +++ b/docs/articles/expensify-classic/expensify-billing/Pay-Per-Use-Subscription.md @@ -5,7 +5,7 @@ description: Learn more about your pay-per-use subscription. # Overview Pay-per-use is a billing option for people who prefer to use Expensify month to month or on an as-needed basis. On a pay-per-use subscription, you will only pay for active users in that given month. -**We recommend this billing setup for companies that use Expensify a few months out of the year**. If you have expenses to manage for more than 6 out of 12 months, an [**Annual Subscription**](https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription) may better suit your needs. +**We recommend this billing setup for companies that use Expensify a few months out of the year**. If you have expenses to manage for more than 6 out of 12 months, an [**Annual Subscription**](https://help.expensify.com/articles/expensify-classic/expensify-billing/Annual-Subscription) may better suit your needs. # How to start a pay-per-use subscription 1. Create a Group Workspace if you haven’t already by going to **Settings > Workspaces > Group > New Workspace** diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/expensify-billing/Receipt-Breakdown.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md rename to docs/articles/expensify-classic/expensify-billing/Receipt-Breakdown.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md b/docs/articles/expensify-classic/expensify-billing/Tax-Exempt.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md rename to docs/articles/expensify-classic/expensify-billing/Tax-Exempt.md diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md index 9940535e1fad..724745f458ef 100644 --- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -38,6 +38,10 @@ If you need to cancel your Expensify Card and cannot access the website or mobil It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account. +# Card Expiration Date + +If you notice that your card expiration date is soon, it's time for a new Expensify card. Expensify will automatically input a notification in your account's Home (Inbox) tab. This notice will ask you to input your address, but this is more if you have changed your address since your card was issued to you. You can ignore it and do nothing; the new Expensify card will ship to your address on file. The new Expensify card will have a new, unique card number and will not be associated with the old one. + {% include faq-begin.md %} ## What if I haven’t received my card after multiple weeks? diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md index aa63aa3c38bd..42d06d45fa87 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md @@ -114,7 +114,7 @@ For an efficient company, we recommend setting up [Scheduled Submit](https://hel - You’ll notice *Scheduled Submit* is located directly under *Report Basics* - Choose *Daily* -Between Expensify's SmartScan technology, automatic categorization, and [DoubleCheck](https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports) features, your employees shouldn't need to do anything more than swipe their Expensify Card or take a photo of their receipt. +Between Expensify's SmartScan technology, automatic categorization, and [DoubleCheck](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approving-Reports) features, your employees shouldn't need to do anything more than swipe their Expensify Card or take a photo of their receipt. Expenses with violations will stay behind for the employee to fix, while expenses that are “in-workspace” will move into an approver’s queue to mitigate any potential for delays. Scheduled Submit will ensure all expenses are submitted automatically for approval. @@ -147,10 +147,10 @@ You only need to do this once: you are fully set up for not only reimbursing exp ## Step 9: Invite employees and set an approval workflow *Select an Approval Mode* -We recommend you select *Advanced Approval* as your Approval Mode to set up a middle-management layer of approval. If you have a single layer of approval, we recommend selecting [Submit & Approve](https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows). But if *Advanced Approval* is your jam, keep reading! +We recommend you select *Advanced Approval* as your Approval Mode to set up a middle-management layer of approval. If you have a single layer of approval, we recommend selecting [Submit & Approve](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows). But if *Advanced Approval* is your jam, keep reading! *Import your employees in bulk via CSV* -Given the amount of employees you have, it’s best you import employees in bulk via CSV. You can learn more about using a CSV file to bulk upload employees with *Advanced Approval [here](https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows)* +Given the amount of employees you have, it’s best you import employees in bulk via CSV. You can learn more about using a CSV file to bulk upload employees with *Advanced Approval [here](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows)* ![Bulk import your employees](https://help.expensify.com/assets/images/playbook-impoort-employees.png){:width="100%"} diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md index b9e06db13bfc..30d3b3e7732c 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md @@ -22,7 +22,7 @@ If you don't already have one, go to *[new.expensify.com](https://new.expensify. There are three paid policies; Individual, Collect, and Control, but for your needs we recommend the Control Policy for the following reasons: - You can cap spend on certain expense types, and set compliance controls so Expensify’s built-in Concierge Audit Tracking can detect violations on your behalf -- As a growing business with VC-funding, the Control plan will scale with you as your team grows and you start to introduce more sophisticated [approval workflows](https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows) (see Step 8 below). +- As a growing business with VC-funding, the Control plan will scale with you as your team grows and you start to introduce more sophisticated [approval workflows](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows) (see Step 8 below). To create your Control Policy: diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md index 8f8d3cfc3dea..4db5ad1f27b9 100644 --- a/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md @@ -12,7 +12,7 @@ First, you’ll need to add these two tags to your Workspace: 1) Number of Internal Attendees 2) Number of External Attendees -These tags must be named exactly as written above, ensuring there are no extra spaces at the beginning or at the end. You’ll need to set the tags to be numbers 00 - 10 or whatever number you wish to go up to (up to the maximum number of attendees you would expect at any one time), one tag per number i.e. “01”, “02”, “03” etc. These tags can be added in addition to those that are pulled in from your accounting solution. Follow these [instructions](https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Tags) to add tags. +These tags must be named exactly as written above, ensuring there are no extra spaces at the beginning or at the end. You’ll need to set the tags to be numbers 00 - 10 or whatever number you wish to go up to (up to the maximum number of attendees you would expect at any one time), one tag per number i.e. “01”, “02”, “03” etc. These tags can be added in addition to those that are pulled in from your accounting solution. Follow these [instructions](https://help.expensify.com/articles/expensify-classic/workspaces/Tags) to add tags. ## Add Payroll Code Go to **Settings > Workspaces > Group > _Workspace Name_ > Categories** and within the categories you wish to track FBT against, select **Edit Category** and add the code “TAG”: diff --git a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md b/docs/articles/expensify-classic/reports/Attendee-Tracking.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md rename to docs/articles/expensify-classic/reports/Attendee-Tracking.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/Currency.md b/docs/articles/expensify-classic/reports/Currency.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Currency.md rename to docs/articles/expensify-classic/reports/Currency.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md b/docs/articles/expensify-classic/reports/Expense-Rules.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md rename to docs/articles/expensify-classic/reports/Expense-Rules.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md b/docs/articles/expensify-classic/reports/Expense-Types.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md rename to docs/articles/expensify-classic/reports/Expense-Types.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md b/docs/articles/expensify-classic/reports/Report-Audit-Log-and-Comments.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md rename to docs/articles/expensify-classic/reports/Report-Audit-Log-and-Comments.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md b/docs/articles/expensify-classic/reports/The-Expenses-Page.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md rename to docs/articles/expensify-classic/reports/The-Expenses-Page.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md b/docs/articles/expensify-classic/reports/The-Reports-Page.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md rename to docs/articles/expensify-classic/reports/The-Reports-Page.md diff --git a/docs/articles/expensify-classic/settings/Preferences.md b/docs/articles/expensify-classic/settings/Preferences.md deleted file mode 100644 index 8131cd0e80c5..000000000000 --- a/docs/articles/expensify-classic/settings/Preferences.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Account Preferences -description: Expensify Account Preferences ---- - -# Overview -Customize your Expensify experience by updating your account details and preferences. Here you can update your profile picture, adjust contact preferences, and perform other actions to personalize your account. - -# How to manage contact preferences -To edit your notification preferences or unsubscribe from Expensify updates: -- On the web, navigate to **Settings > Account > Preferences** -Scroll down to find the ‘Contact Preferences’ section. To stop receiving a specific type of email, uncheck the corresponding box. - -# How to set your time zone -Wherever you are, we'll time-stamp your report actions according to your local time. This helps you keep track of when submissions or approvals occurred. Setting your time zone in Expensify is simple and ensures accurate time-stamping for your report actions, especially in the comments section of the expense report you're reviewing. - -To set your time zone: -Navigate to **Settings > Account > Preferences > Scroll down to Time Zone** - -![ExpensifyHelp_Timezone]({{site.url}}/assets/images/ExpensifyHelp_Timezone.png){:width="100%"} - - **Note:** To set your time zone automatically based on your location, tick the box that says **Set my time zone automatically**. - -If you prefer to set your time zone manually, leave the box unticked and select your time zone from the searchable list of locations. - -When you add a comment to a report, all the report actions will be time-stamped in your local time. Adjusting your time zone to the appropriate location makes tracking and understanding submission and approval times much easier. - -![ExpensifyHelp_Time]({{site.url}}/assets/images/ExpensifyHelp_Time.png){:width="100%"} diff --git a/docs/articles/expensify-classic/settings/account-settings/Set-time-zone.md b/docs/articles/expensify-classic/settings/account-settings/Set-time-zone.md new file mode 100644 index 000000000000..7d4842f936b9 --- /dev/null +++ b/docs/articles/expensify-classic/settings/account-settings/Set-time-zone.md @@ -0,0 +1,21 @@ +--- +title: Set time zone +description: Set your time zone in Expensify +--- +
+ +You can manually set your time zone or allow Expensify to automatically set your time zone based on your location. + +{% include info.html %} +Some actions you take in Expensify are timestamped. To ensure the most accurate time is captured, you’ll want to make sure your time zone matches your current location. If you select the automatic time zone option, your time zone will automatically reflect your current location. If you manually set your time zone, you must manually update the time zone when traveling. +{% include end-info.html %} + +*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* + +1. Hover over Settings, then click **Account**. +2. Click the **Preferences** tab. +3. Scroll down to the Time Zone section and select your time zone preferences. + - Automatic time zone: To allow Expensify to automatically set your time zone based on your location, select the Set my time zone automatically checkbox. + - Manual time zone: To manually select your time zone, select your time zone from the list. + +
diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md b/docs/articles/expensify-classic/workspaces/Budgets.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md rename to docs/articles/expensify-classic/workspaces/Budgets.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md b/docs/articles/expensify-classic/workspaces/Categories.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md rename to docs/articles/expensify-classic/workspaces/Categories.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md b/docs/articles/expensify-classic/workspaces/Domains-Overview.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md rename to docs/articles/expensify-classic/workspaces/Domains-Overview.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md b/docs/articles/expensify-classic/workspaces/Expenses.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md rename to docs/articles/expensify-classic/workspaces/Expenses.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Invoicing.md b/docs/articles/expensify-classic/workspaces/Invoicing.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Invoicing.md rename to docs/articles/expensify-classic/workspaces/Invoicing.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md b/docs/articles/expensify-classic/workspaces/Per-Diem.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md rename to docs/articles/expensify-classic/workspaces/Per-Diem.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md b/docs/articles/expensify-classic/workspaces/Reimbursement.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md rename to docs/articles/expensify-classic/workspaces/Reimbursement.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md b/docs/articles/expensify-classic/workspaces/SAML-SSO.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md rename to docs/articles/expensify-classic/workspaces/SAML-SSO.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md b/docs/articles/expensify-classic/workspaces/Tags.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md rename to docs/articles/expensify-classic/workspaces/Tags.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Currency.md b/docs/articles/expensify-classic/workspaces/reports/Currency.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/reports/Currency.md rename to docs/articles/expensify-classic/workspaces/reports/Currency.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Report-Fields-And-Titles.md b/docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/reports/Report-Fields-And-Titles.md rename to docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Scheduled-Submit.md b/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/reports/Scheduled-Submit.md rename to docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/tax-tracking.md b/docs/articles/expensify-classic/workspaces/tax-tracking.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/tax-tracking.md rename to docs/articles/expensify-classic/workspaces/tax-tracking.md diff --git a/docs/assets/images/domains.svg b/docs/assets/images/domains.svg new file mode 100644 index 000000000000..3a3c95604b79 --- /dev/null +++ b/docs/assets/images/domains.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/expensify-classic/hubs/expense-and-report-features/index.html b/docs/expensify-classic/hubs/copilots-and-delegates/index.html similarity index 58% rename from docs/expensify-classic/hubs/expense-and-report-features/index.html rename to docs/expensify-classic/hubs/copilots-and-delegates/index.html index 44afa4b18b51..a2147a6a61ae 100644 --- a/docs/expensify-classic/hubs/expense-and-report-features/index.html +++ b/docs/expensify-classic/hubs/copilots-and-delegates/index.html @@ -1,6 +1,6 @@ --- layout: default -title: Expense & Report Settings +title: Copilots & Delegates --- {% include hub.html %} \ No newline at end of file diff --git a/docs/expensify-classic/hubs/get-paid-back/index.html b/docs/expensify-classic/hubs/domains/index.html similarity index 69% rename from docs/expensify-classic/hubs/get-paid-back/index.html rename to docs/expensify-classic/hubs/domains/index.html index 1f84c1510b92..fd3b4727cce7 100644 --- a/docs/expensify-classic/hubs/get-paid-back/index.html +++ b/docs/expensify-classic/hubs/domains/index.html @@ -1,6 +1,6 @@ --- layout: default -title: Get Paid Back +title: Domains --- {% include hub.html %} \ No newline at end of file diff --git a/docs/expensify-classic/hubs/get-paid-back/expenses.html b/docs/expensify-classic/hubs/expenses/expenses.html similarity index 100% rename from docs/expensify-classic/hubs/get-paid-back/expenses.html rename to docs/expensify-classic/hubs/expenses/expenses.html diff --git a/docs/expensify-classic/hubs/workspace-and-domain-settings/index.html b/docs/expensify-classic/hubs/expenses/index.html similarity index 58% rename from docs/expensify-classic/hubs/workspace-and-domain-settings/index.html rename to docs/expensify-classic/hubs/expenses/index.html index ffd514fcb6fa..66aa2c74ac57 100644 --- a/docs/expensify-classic/hubs/workspace-and-domain-settings/index.html +++ b/docs/expensify-classic/hubs/expenses/index.html @@ -1,6 +1,6 @@ --- layout: default -title: Policy And Domain Settings +title: Expenses --- {% include hub.html %} \ No newline at end of file diff --git a/docs/expensify-classic/hubs/get-paid-back/reports.html b/docs/expensify-classic/hubs/expenses/reports.html similarity index 100% rename from docs/expensify-classic/hubs/get-paid-back/reports.html rename to docs/expensify-classic/hubs/expenses/reports.html diff --git a/docs/expensify-classic/hubs/manage-employees-and-report-approvals/index.html b/docs/expensify-classic/hubs/reports/index.html similarity index 51% rename from docs/expensify-classic/hubs/manage-employees-and-report-approvals/index.html rename to docs/expensify-classic/hubs/reports/index.html index 788e445ebc91..627274fc2391 100644 --- a/docs/expensify-classic/hubs/manage-employees-and-report-approvals/index.html +++ b/docs/expensify-classic/hubs/reports/index.html @@ -1,6 +1,6 @@ --- layout: default -title: Manage Employees And Report Approvals +title: Reports --- {% include hub.html %} \ No newline at end of file diff --git a/docs/expensify-classic/hubs/workspaces/index.html b/docs/expensify-classic/hubs/workspaces/index.html new file mode 100644 index 000000000000..436c9fcfecb1 --- /dev/null +++ b/docs/expensify-classic/hubs/workspaces/index.html @@ -0,0 +1,6 @@ +--- +layout: default +title: Workspaces +--- + +{% include hub.html %} \ No newline at end of file diff --git a/docs/expensify-classic/hubs/workspace-and-domain-settings/reports.html b/docs/expensify-classic/hubs/workspaces/reports.html similarity index 100% rename from docs/expensify-classic/hubs/workspace-and-domain-settings/reports.html rename to docs/expensify-classic/hubs/workspaces/reports.html diff --git a/docs/redirects.csv b/docs/redirects.csv index 097c0ad2679e..df4e2a45dce3 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -1,5 +1,5 @@ sourceURL,targetURL -https://community.expensify.com/discussion/5634/deep-dive-how-long-will-it-take-for-me-to-receive-my-reimbursement,https://help.expensify.com/articles/expensify-classic/get-paid-back/reports/Reimbursements +https://community.expensify.com/discussion/5634/deep-dive-how-long-will-it-take-for-me-to-receive-my-reimbursement,https://help.expensify.com/articles/expensify-classic/expenses/reports/Reimbursements https://community.expensify.com/discussion/4925/how-to-dispute-an-expensify-card-transaction,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction https://community.expensify.com/discussion/5184/faq-how-am-i-protected-from-fraud-using-the-expensify-card,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction https://community.expensify.com/discussion/4887/deep-dive-understanding-your-expensify-card-statement,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements @@ -17,14 +17,14 @@ https://community.expensify.com/discussion/5802/deep-dive-understanding-math-and https://community.expensify.com/discussion/5796/deep-dive-user-level-formula,https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates https://community.expensify.com/discussion/4750/how-to-create-a-custom-export,https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates https://community.expensify.com/discussion/4642/how-to-export-reports-to-a-custom-template,https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates -https://community.expensify.com/discussion/5648/deep-dive-policy-users-and-roles,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles -https://community.expensify.com/discussion/5740/deep-dive-what-expense-information-is-available-based-on-role,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles -https://community.expensify.com/discussion/4472/how-to-set-or-edit-a-user-role,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles -https://community.expensify.com/discussion/5655/deep-dive-what-is-a-vacation-delegate,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate -https://community.expensify.com/discussion/5194/how-to-assign-a-vacation-delegate-for-an-employee-through-domains,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate -https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vacation-delegate-from-account-settings,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate +https://community.expensify.com/discussion/5648/deep-dive-policy-users-and-roles,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles +https://community.expensify.com/discussion/5740/deep-dive-what-expense-information-is-available-based-on-role,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles +https://community.expensify.com/discussion/4472/how-to-set-or-edit-a-user-role,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles +https://community.expensify.com/discussion/5655/deep-dive-what-is-a-vacation-delegate,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate +https://community.expensify.com/discussion/5194/how-to-assign-a-vacation-delegate-for-an-employee-through-domains,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate +https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vacation-delegate-from-account-settings,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate https://community.expensify.com/discussion/5274/how-to-set-up-an-adp-indirect-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP -https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/get-paid-back/Distance-Tracking +https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/expenses/Distance-Tracking https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings @@ -54,14 +54,15 @@ https://help.expensify.com/articles/expensify-classic/getting-started/Employees, https://help.expensify.com/articles/expensify-classic/getting-started/Using-The-App,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace https://help.expensify.com/articles/expensify-classic/getting-started/support/Expensify-Support,https://use.expensify.com/support https://help.expensify.com/articles/expensify-classic/getting-started/Plan-Types,https://use.expensify.com/ -https://help.expensify.com/articles/new-expensify/payments/Referral-Program,https://help.expensify.com/articles/expensify-classic/get-paid-back/Referral-Program +https://help.expensify.com/articles/new-expensify/payments/Referral-Program,https://help.expensify.com/articles/expensify-classic/expenses/Referral-Program https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://help.expensify.com/articles/expensify-classic/account-settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://help.expensify.com/articles/expensify-classic/getting-started/Individual-Users,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself -https://help.expensify.com/articles/expensify-classic/getting-started/Invite-Members,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members -https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription,https://use.expensify.com/ -https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription,https://use.expensify.com/ -https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/getting-started/Invite-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members +https://help.expensify.com/articles/expensify-classic/expensify-billing/Annual-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/expensify-billing/Pay-Per-Use-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/expensify-billing/Individual-Subscription,https://use.expensify.com/ https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://help.expensify.com/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager,https://use.expensify.com/support diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index 643c81bd0b9c..2c5350cec2aa 100644 Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg index 8a5170cfe697..bae3cd9f3e21 100644 Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index acfc4d933954..e39542ef0303 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -543,13 +543,10 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Plaid/LinkKit.framework/LinkKit", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); @@ -557,13 +554,10 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); @@ -613,13 +607,10 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Plaid/LinkKit.framework/LinkKit", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); @@ -627,13 +618,10 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 712909df6b61..ab5d359a5460 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.49 + 1.4.51 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.49.4 + 1.4.51.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index b1a4aa336ab8..ca9200c78376 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.49 + 1.4.51 CFBundleSignature ???? CFBundleVersion - 1.4.49.4 + 1.4.51.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index c90bc159062b..f20b520b1480 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.49 + 1.4.51 CFBundleVersion - 1.4.49.4 + 1.4.51.3 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index aa87c3e295f3..83c21797bd0a 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -26,17 +26,6 @@ setup_permissions([ 'LocationWhenInUse' ]) -# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. -# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded -# -# To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` -# ```js -# module.exports = { -# dependencies: { -# ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), -# ``` -flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled(['DebugProduction', 'DebugDevelopment', 'DebugAdHoc']) - linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green @@ -86,11 +75,6 @@ target 'NewExpensify' do use_react_native!( :path => config[:reactNativePath], - # Enables Flipper. - # - # Note that if you have use_frameworks! enabled, Flipper will not work and - # you should disable the next line. - :flipper_configuration => flipper_config, # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c93dfba50f5a..d0007ec51668 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -30,7 +30,6 @@ PODS: - boost (1.83.0) - BVLinearGradient (2.8.1): - React-Core - - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) - EXAV (13.10.4): - ExpoModulesCore @@ -127,62 +126,6 @@ PODS: - FirebaseInstallations (~> 8.0) - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - Flipper (0.201.0): - - Flipper-Folly (~> 2.6) - - Flipper-Boost-iOSX (1.76.0.1.11) - - Flipper-DoubleConversion (3.2.0.1) - - Flipper-Fmt (7.1.7) - - Flipper-Folly (2.6.10): - - Flipper-Boost-iOSX - - Flipper-DoubleConversion - - Flipper-Fmt (= 7.1.7) - - Flipper-Glog - - libevent (~> 2.1.12) - - OpenSSL-Universal (= 1.1.1100) - - Flipper-Glog (0.5.0.5) - - Flipper-PeerTalk (0.0.4) - - FlipperKit (0.201.0): - - FlipperKit/Core (= 0.201.0) - - FlipperKit/Core (0.201.0): - - Flipper (~> 0.201.0) - - FlipperKit/CppBridge - - FlipperKit/FBCxxFollyDynamicConvert - - FlipperKit/FBDefines - - FlipperKit/FKPortForwarding - - SocketRocket (~> 0.6.0) - - FlipperKit/CppBridge (0.201.0): - - Flipper (~> 0.201.0) - - FlipperKit/FBCxxFollyDynamicConvert (0.201.0): - - Flipper-Folly (~> 2.6) - - FlipperKit/FBDefines (0.201.0) - - FlipperKit/FKPortForwarding (0.201.0): - - CocoaAsyncSocket (~> 7.6) - - Flipper-PeerTalk (~> 0.0.4) - - FlipperKit/FlipperKitHighlightOverlay (0.201.0) - - FlipperKit/FlipperKitLayoutHelpers (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutTextSearchable - - FlipperKit/FlipperKitLayoutIOSDescriptors (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutHelpers - - FlipperKit/FlipperKitLayoutPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutHelpers - - FlipperKit/FlipperKitLayoutIOSDescriptors - - FlipperKit/FlipperKitLayoutTextSearchable - - FlipperKit/FlipperKitLayoutTextSearchable (0.201.0) - - FlipperKit/FlipperKitNetworkPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitReactPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitUserDefaultsPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/SKIOSNetworkPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitNetworkPlugin - fmt (6.2.1) - glog (0.3.5) - GoogleAppMeasurement (8.8.0): @@ -284,7 +227,6 @@ PODS: - onfido-react-native-sdk (10.6.0): - Onfido (~> 29.6.0) - React - - OpenSSL-Universal (1.1.1100) - Plaid (4.7.0) - PromisesObjC (2.3.1) - RCT-Folly (2022.05.16.00): @@ -1198,6 +1140,10 @@ PODS: - React - React-callinvoker - React-Core + - react-native-release-profiler (0.1.6): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core - react-native-render-html (6.3.1): - React-Core - react-native-safe-area-context (4.8.2): @@ -1443,6 +1389,8 @@ PODS: - glog - RCT-Folly (= 2022.05.16.00) - React-Core + - RNShare (10.0.2): + - React-Core - RNSound (0.11.2): - React-Core - RNSound/Core (= 0.11.2) @@ -1480,32 +1428,11 @@ DEPENDENCIES: - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - - Flipper (= 0.201.0) - - Flipper-Boost-iOSX (= 1.76.0.1.11) - - Flipper-DoubleConversion (= 3.2.0.1) - - Flipper-Fmt (= 7.1.7) - - Flipper-Folly (= 2.6.10) - - Flipper-Glog (= 0.5.0.5) - - Flipper-PeerTalk (= 0.0.4) - - FlipperKit (= 0.201.0) - - FlipperKit/Core (= 0.201.0) - - FlipperKit/CppBridge (= 0.201.0) - - FlipperKit/FBCxxFollyDynamicConvert (= 0.201.0) - - FlipperKit/FBDefines (= 0.201.0) - - FlipperKit/FKPortForwarding (= 0.201.0) - - FlipperKit/FlipperKitHighlightOverlay (= 0.201.0) - - FlipperKit/FlipperKitLayoutPlugin (= 0.201.0) - - FlipperKit/FlipperKitLayoutTextSearchable (= 0.201.0) - - FlipperKit/FlipperKitNetworkPlugin (= 0.201.0) - - FlipperKit/FlipperKitReactPlugin (= 0.201.0) - - FlipperKit/FlipperKitUserDefaultsPlugin (= 0.201.0) - - FlipperKit/SKIOSNetworkPlugin (= 0.201.0) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) - lottie-react-native (from `../node_modules/lottie-react-native`) - "onfido-react-native-sdk (from `../node_modules/@onfido/react-native-sdk`)" - - OpenSSL-Universal (= 1.1.1100) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) @@ -1514,7 +1441,6 @@ DEPENDENCIES: - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - React-Codegen (from `build/generated/ios`) - React-Core (from `../node_modules/react-native/`) - - React-Core/DevSupport (from `../node_modules/react-native/`) - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) @@ -1546,6 +1472,7 @@ DEPENDENCIES: - react-native-performance (from `../node_modules/react-native-performance`) - react-native-plaid-link-sdk (from `../node_modules/react-native-plaid-link-sdk`) - react-native-quick-sqlite (from `../node_modules/react-native-quick-sqlite`) + - react-native-release-profiler (from `../node_modules/react-native-release-profiler`) - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) @@ -1591,6 +1518,7 @@ DEPENDENCIES: - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) + - RNShare (from `../node_modules/react-native-share`) - RNSound (from `../node_modules/react-native-sound`) - RNSVG (from `../node_modules/react-native-svg`) - VisionCamera (from `../node_modules/react-native-vision-camera`) @@ -1602,7 +1530,6 @@ SPEC REPOS: - AirshipFrameworkProxy - AirshipServiceExtension - AppAuth - - CocoaAsyncSocket - Firebase - FirebaseABTesting - FirebaseAnalytics @@ -1612,14 +1539,6 @@ SPEC REPOS: - FirebaseInstallations - FirebasePerformance - FirebaseRemoteConfig - - Flipper - - Flipper-Boost-iOSX - - Flipper-DoubleConversion - - Flipper-Fmt - - Flipper-Folly - - Flipper-Glog - - Flipper-PeerTalk - - FlipperKit - fmt - GoogleAppMeasurement - GoogleDataTransport @@ -1639,7 +1558,6 @@ SPEC REPOS: - MapboxMobileEvents - nanopb - Onfido - - OpenSSL-Universal - Plaid - PromisesObjC - SDWebImage @@ -1751,6 +1669,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-plaid-link-sdk" react-native-quick-sqlite: :path: "../node_modules/react-native-quick-sqlite" + react-native-release-profiler: + :path: "../node_modules/react-native-release-profiler" react-native-render-html: :path: "../node_modules/react-native-render-html" react-native-safe-area-context: @@ -1841,6 +1761,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" + RNShare: + :path: "../node_modules/react-native-share" RNSound: :path: "../node_modules/react-native-sound" RNSVG: @@ -1857,7 +1779,6 @@ SPEC CHECKSUMS: AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 boost: d3f49c53809116a5d38da093a8aa78bf551aed09 BVLinearGradient: 421743791a59d259aec53f4c58793aad031da2ca - CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 EXAV: 09a4d87fa6b113fbb0ada3aade6799f78271cb44 Expo: 1e3bcf9dd99de57a636127057f6b488f0609681a @@ -1874,14 +1795,6 @@ SPEC CHECKSUMS: FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b - Flipper: c7a0093234c4bdd456e363f2f19b2e4b27652d44 - Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c - Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30 - Flipper-Fmt: 60cbdd92fc254826e61d669a5d87ef7015396a9b - Flipper-Folly: 584845625005ff068a6ebf41f857f468decd26b3 - Flipper-Glog: 70c50ce58ddaf67dc35180db05f191692570f446 - Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9 - FlipperKit: 37525a5d056ef9b93d1578e04bc3ea1de940094f fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 @@ -1905,7 +1818,6 @@ SPEC CHECKSUMS: nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Onfido: c52e797b10cc9e6d29ba91996cb62e501000bfdd onfido-react-native-sdk: 4e7f0a7a986ed93cb906d2e0b67a6aab9202de0b - OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c Plaid: 431ef9be5314a1345efb451bc5e6b067bfb3b4c6 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 @@ -1945,6 +1857,7 @@ SPEC CHECKSUMS: react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886 react-native-plaid-link-sdk: df1618a85a615d62ff34e34b76abb7a56497fbc1 react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 + react-native-release-profiler: 86f2004d5f8c4fff17d90a5580513519a685d7ae react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: 0ee144a6170530ccc37a0fd9388e28d06f516a89 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 @@ -1990,6 +1903,7 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 RNReanimated: 3850671fd0c67051ea8e1e648e8c3e86bf3a28eb RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 + RNShare: 859ff710211285676b0bcedd156c12437ea1d564 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 @@ -1999,8 +1913,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 0a6794d1974aed5d653d0d0cb900493e2583e35a - Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 + Yoga: 13c8ef87792450193e117976337b8527b49e8c03 -PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 +PODFILE CHECKSUM: a431c146e1501391834a2f299a74093bac53b530 COCOAPODS: 1.13.0 diff --git a/jest.config.js b/jest.config.js index 5b36e44c7581..13645e720c8e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,7 +23,7 @@ module.exports = { }, testEnvironment: 'jsdom', setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], - setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'], + setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.ts'], cacheDirectory: '/.jest-cache', moduleNameMapper: { '\\.(lottie)$': '/__mocks__/fileMock.ts', diff --git a/jest/setup.ts b/jest/setup.ts index 11b0d77ed7ac..488e3e36a1d3 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -47,3 +47,7 @@ jest.mock('react-native-sound', () => { return SoundMock; }); + +jest.mock('react-native-share', () => ({ + default: jest.fn(), +})); diff --git a/package-lock.json b/package-lock.json index 2986136f8a98..109ff3b862d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.49-4", + "version": "1.4.51-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.49-4", + "version": "1.4.51-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -52,7 +52,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -64,7 +64,7 @@ "lodash": "4.17.21", "lottie-react-native": "6.4.1", "mapbox-gl": "^2.15.0", - "onfido-sdk-ui": "13.6.1", + "onfido-sdk-ui": "14.15.0", "patch-package": "^8.0.0", "process": "^0.11.10", "prop-types": "^15.7.2", @@ -107,9 +107,11 @@ "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", "react-native-reanimated": "^3.7.2", + "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", + "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", "react-native-tab-view": "^3.5.2", @@ -227,7 +229,6 @@ "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", "react-native-clean-project": "^4.0.0-alpha4.0", - "react-native-performance-flipper-reporter": "^2.0.0", "react-test-renderer": "18.2.0", "reassure": "^0.10.1", "setimmediate": "^1.0.5", @@ -7172,16 +7173,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/@mediapipe/face_detection": { - "version": "0.4.1646425229", - "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz", - "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg==" - }, - "node_modules/@mediapipe/face_mesh": { - "version": "0.4.1633559619", - "resolved": "https://registry.npmjs.org/@mediapipe/face_mesh/-/face_mesh-0.4.1633559619.tgz", - "integrity": "sha512-Vc8cdjxS5+O2gnjWH9KncYpUCVXT0h714KlWAsyqJvJbIgUJBqpppbIx8yWcAzBDxm/5cYSuBI5p5ySIPxzcEg==" - }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -7513,153 +7504,6 @@ "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", "license": "MIT" }, - "node_modules/@onfido/active-video-capture": { - "version": "0.28.6", - "resolved": "https://registry.npmjs.org/@onfido/active-video-capture/-/active-video-capture-0.28.6.tgz", - "integrity": "sha512-RFUeKaOSjj/amPp6VzhVkq/7kIkutEnnttT9n5KDeD3Vx8a09KD3a/xvxdQppveHlDAYsdBP6LrJwSSpjXiprg==", - "dependencies": { - "@mediapipe/face_detection": "^0.4.1646425229", - "@mediapipe/face_mesh": "^0.4.1633559619", - "@onfido/castor": "^2.2.2", - "@onfido/castor-icons": "^2.12.0", - "@tensorflow-models/face-detection": "^1.0.1", - "@tensorflow-models/face-landmarks-detection": "^1.0.2", - "@tensorflow/tfjs-backend-wasm": "3.20.0", - "@tensorflow/tfjs-backend-webgl": "3.20.0", - "@tensorflow/tfjs-converter": "3.20.0", - "@tensorflow/tfjs-core": "3.20.0", - "preact": "10.11.3", - "react-webcam": "^7.2.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow-models/face-landmarks-detection": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@tensorflow-models/face-landmarks-detection/-/face-landmarks-detection-1.0.5.tgz", - "integrity": "sha512-54XJPi8g29/MknJ33ZBrLsEzr9kw/dJtrJMMD3xrCrnRlfFQPIKQ5PI2Wml55Fz2p4U2hemzBB0/H+S94JddIQ==", - "dependencies": { - "rimraf": "^3.0.2" - }, - "peerDependencies": { - "@mediapipe/face_mesh": "~0.4.0", - "@tensorflow-models/face-detection": "~1.0.0", - "@tensorflow/tfjs-backend-webgl": "^3.12.0", - "@tensorflow/tfjs-converter": "^3.12.0", - "@tensorflow/tfjs-core": "^3.12.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.20.0.tgz", - "integrity": "sha512-gf075YaBLwSAAiUwa0D4GvYyUBhbJ1BVSivUNQmUfGKvIr2lIhF0qstBr033YTc3lhkbFSHEEPAHh/EfpqyjXQ==", - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-wasm": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-wasm/-/tfjs-backend-wasm-3.20.0.tgz", - "integrity": "sha512-k+sDcrcPtGToLjKRffgtSqlcN4MC6g4hXWRarZfgvvyvFqpxVfVqrGYHGTirXdN47sKYhmcTSMvbM2quGaaQnA==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.20.0", - "@types/emscripten": "~0.0.34" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.20.0.tgz", - "integrity": "sha512-SucbyQ08re3HvRgVfarRtKFIjNM4JvIAzcXmw4vaE/HrCtPEePkGO1VrmfQoN470EdUmGiwgqAjoyBvM2VOlVg==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.20.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@types/webgl2": "0.0.6", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-converter": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.20.0.tgz", - "integrity": "sha512-8EIYqtQwvSYw9GFNW2OFU8Qnl/FQF/kKAsQJoORYaZ419WJo+FIZWbAWDtCpJSAgkgoHH1jYWgV9H313cVmqxg==", - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-core": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.20.0.tgz", - "integrity": "sha512-L16JyVA4a8jFJXFgB9/oYZxcGq/GfLypt5dMVTyedznARZZ9SiY/UMMbo3IKl9ZylG1dOVVTpjzV3EvBYfeJXw==", - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@webgpu/types": "0.1.16", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@webgpu/types": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz", - "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==" - }, - "node_modules/@onfido/castor": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@onfido/castor/-/castor-2.3.0.tgz", - "integrity": "sha512-FkydkjedS6b2g3SqgZMYnVRZvUs/MkaEuXXJWG9+LNc7DMFT1K8smOnNuHzkiM3cJhXL6yAADdKE0mg+ZIrucQ==", - "dependencies": { - "@onfido/castor-tokens": "^1.0.0-beta.6", - "csstype": "^3.1.1" - }, - "peerDependencies": { - "@onfido/castor-icons": ">=1.0.0" - } - }, - "node_modules/@onfido/castor-icons": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@onfido/castor-icons/-/castor-icons-2.22.0.tgz", - "integrity": "sha512-7OnCvu5xqVWcBLqovZyb99NP0oHw7sjkVYXZhi438i0U6Pgecrhu/14Gc/IN/kvgDxWj9qmiYdd0qdjNaVckrQ==", - "peerDependencies": { - "react": ">=17 || ^16.14 || ^15.7 || ^0.14.10" - } - }, - "node_modules/@onfido/castor-tokens": { - "version": "1.0.0-beta.6", - "resolved": "https://registry.npmjs.org/@onfido/castor-tokens/-/castor-tokens-1.0.0-beta.6.tgz", - "integrity": "sha512-MfwuSlNdM0Ay0cI3LLyqZGsHW0e1Y1R/0IdQKVU575PdWQx1Q/538aOZMo/a3/oSW0pMEgfOm+mNqPx057cvWA==" - }, - "node_modules/@onfido/opencv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@onfido/opencv/-/opencv-2.1.1.tgz", - "integrity": "sha512-Bwo0YsZrrdm+p5hpNFZ7yrqNVWJxOUbQW9aWDEUtkDWUL+nX2RHIR6F4lBGVmbqnG24anadS/+nEvy80SwD3tQ==", - "dependencies": { - "mirada": "^0.0.15" - } - }, "node_modules/@onfido/react-native-sdk": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-10.6.0.tgz", @@ -10003,78 +9847,6 @@ "join-component": "^1.1.0" } }, - "node_modules/@sentry/browser": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/core": "7.11.1", - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/core": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/hub": "7.11.1", - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/core/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/hub": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/hub/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/types": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, "node_modules/@shopify/flash-list": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.6.3.tgz", @@ -10153,11 +9925,6 @@ "@sinonjs/commons": "^2.0.0" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" - }, "node_modules/@storybook/addon-a11y": { "version": "6.5.10", "dev": true, @@ -19521,88 +19288,6 @@ "node": ">=10" } }, - "node_modules/@tensorflow-models/face-detection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tensorflow-models/face-detection/-/face-detection-1.0.2.tgz", - "integrity": "sha512-anjSxy3MnZdTiVluOEQZeaFWM30IPswFM+SltX6wseXKja/AbrHYqamGNZKUylAs2JAyudq+xqTRPS+nA2ourg==", - "dependencies": { - "rimraf": "^3.0.2", - "tslib": "2.4.0" - }, - "peerDependencies": { - "@mediapipe/face_detection": "~0.4.0", - "@tensorflow/tfjs-backend-webgl": "^4.4.0", - "@tensorflow/tfjs-converter": "^4.4.0", - "@tensorflow/tfjs-core": "^4.4.0" - } - }, - "node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.16.0.tgz", - "integrity": "sha512-bQFu7FTUgqgss1AwnqSwQ1f02IPrfLLc2lLn5pyyVrS6Ex7zA6Y4YkfktqoJSRE6LlRZv3vxSriUGE1avRe4qQ==", - "peer": true, - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.16.0.tgz", - "integrity": "sha512-cIGZWuY892iwTRokbDj3qsLi0AlpQn+U7rzB1mddhHrWr9kBXrrnAvIq0h2aiFzRFNePWUcsbgK+HmYG32kosg==", - "peer": true, - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "4.16.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-converter": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.16.0.tgz", - "integrity": "sha512-gd8dHl9tqEPQOHZLAUza713nKr42rpvUXrtm7yUhk10THvJT6TXe9Q2AJKmni8J3vfR+ghsCh77F8D4RbShx1Q==", - "peer": true, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-core": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.16.0.tgz", - "integrity": "sha512-MarAtO+Up6wA8pI9QDpQOwwJgb/imYMN++tsoaalyOEE9+B5HS4lQldxDJKXO8Frf4DyXf4FItJktEXaiPfRHw==", - "peer": true, - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.7.0", - "@types/seedrandom": "^2.4.28", - "@webgpu/types": "0.1.38", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { - "version": "2019.7.3", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", - "peer": true - }, "node_modules/@testing-library/jest-native": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-native/-/jest-native-5.4.1.tgz", @@ -20092,11 +19777,6 @@ "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", "dev": true }, - "node_modules/@types/emscripten": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-0.0.34.tgz", - "integrity": "sha512-QSb9ojDincskc+uKMI0KXp8e1NALFINCrMlp8VGKGcTSxeEyRTTKyjWw75NYrCZHUsVEEEpr1tYHpbtaC++/sQ==" - }, "node_modules/@types/eslint": { "version": "8.4.6", "license": "MIT", @@ -20341,11 +20021,6 @@ "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", "dev": true }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, "node_modules/@types/mapbox-gl": { "version": "2.7.13", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.13.tgz", @@ -20418,11 +20093,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/offscreencanvas": { - "version": "2019.3.0", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", - "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" - }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -20580,11 +20250,6 @@ "version": "0.16.2", "license": "MIT" }, - "node_modules/@types/seedrandom": { - "version": "2.4.34", - "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", - "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==" - }, "node_modules/@types/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", @@ -20684,16 +20349,6 @@ "dev": true, "optional": true }, - "node_modules/@types/webgl-ext": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", - "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" - }, - "node_modules/@types/webgl2": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", - "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" - }, "node_modules/@types/webpack": { "version": "4.41.32", "dev": true, @@ -21710,12 +21365,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webgpu/types": { - "version": "0.1.38", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", - "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", - "peer": true - }, "node_modules/@webpack-cli/configtest": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", @@ -24144,12 +23793,6 @@ "bluebird": "^3.5.5" } }, - "node_modules/blueimp-load-image": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/blueimp-load-image/-/blueimp-load-image-2.29.0.tgz", - "integrity": "sha512-psm81GlZ0ffKxVT0QN9dvhpzXMv1KxgXSg8ars0XGAcEGsTwFT2IPo59HDXlw4Lo2oImdPzwrwkliZSiLLUpIw==", - "license": "MIT" - }, "node_modules/blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", @@ -28264,10 +27907,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dompurify": { - "version": "2.3.10", - "license": "(MPL-2.0 OR Apache-2.0)" - }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -28786,46 +28425,6 @@ "objectorarray": "^1.0.5" } }, - "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -28866,12 +28465,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/enumerate-devices": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/enumerate-devices/-/enumerate-devices-1.1.1.tgz", - "integrity": "sha512-8zDbrc7ocusTL1ZGmvgy0cTwdyCaM7sGZoYLRmnWJalLQzmftDtce+uDU91gafOTo9MCtgjSIxyMv/F4+Hcchw==", - "license": "MIT" - }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", @@ -30268,12 +29861,6 @@ "node": ">=6" } }, - "node_modules/eventemitter2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-2.2.2.tgz", - "integrity": "sha512-AmQ734LWUB9Iyk+2WIU3Z8iRhdL1XQihEE0iF/QC5Xp11zST0Z5tn5jRHa/PgIld2QIPSCys3CREqOQLUhNvkw==", - "license": "MIT" - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -30510,8 +30097,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", - "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", + "integrity": "sha512-v6UnN9yAW6p2996Fvd4AZnMRnisVfjg6ijWzUQue/6JsjSY+MW10oP74hSjD6x32fRrNmMctjy6d5a79bQFdPA==", "license": "MIT", "dependencies": { "classnames": "2.5.0", @@ -31350,14 +30937,6 @@ "url": "https://opencollective.com/ramda" } }, - "node_modules/file-type": { - "version": "12.4.2", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", - "integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==", - "engines": { - "node": ">=8" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -32933,19 +32512,6 @@ "node": ">= 8" } }, - "node_modules/history": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.5.1.tgz", - "integrity": "sha512-gfHeJeYeMzFtos61gdA1AloO0hGXPF2Yum+2FRdJvlylYQOz51OnT1zuwg9UYst1BRrONhcAh3Nmsg9iblgl6g==", - "license": "MIT", - "dependencies": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "resolve-pathname": "^2.0.0", - "value-equal": "^0.2.0", - "warning": "^3.0.0" - } - }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -37306,13 +36872,6 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" }, - "node_modules/js-cookie": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -38326,11 +37885,6 @@ "node": ">=6" } }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -39945,22 +39499,6 @@ "node": ">= 8" } }, - "node_modules/mirada": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/mirada/-/mirada-0.0.15.tgz", - "integrity": "sha512-mbm4c+wjBVcmUzHRLv/TfOAq+iy03D24KwGxx8H+NSXkD5EOZV9zFWbVxTvZCc9XwR0FIUhryU/kQm12SMSQ3g==", - "dependencies": { - "buffer": "^5.4.3", - "cross-fetch": "^3.0.4", - "file-type": "^12.3.0", - "misc-utils-of-mine-generic": "^0.2.31" - } - }, - "node_modules/misc-utils-of-mine-generic": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/misc-utils-of-mine-generic/-/misc-utils-of-mine-generic-0.2.45.tgz", - "integrity": "sha512-WsG2zYiui2cdEbHF2pXmJfnjHb4zL+cy+PaYcLgIpMju98hwX89VbjlvGIfamCfEodbQ0qjCEvD3ocgkCXfMOQ==" - }, "node_modules/mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -40972,41 +40510,9 @@ } }, "node_modules/onfido-sdk-ui": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-13.6.1.tgz", - "integrity": "sha512-EcFqTN9uaVINRUttSdt6ySUBlfg25dE9f2yxxXVUmrM9a4M1luv+aICej1zE3vRZPFEuFJ9mqJZQUTYo0YMFyg==", - "dependencies": { - "@onfido/active-video-capture": "^0.28.2", - "@onfido/opencv": "^2.0.0", - "@sentry/browser": "^7.2.0", - "blueimp-load-image": "~2.29.0", - "classnames": "~2.2.5", - "core-js": "^3.21.1", - "deepmerge": "^4.2.2", - "dompurify": "^2.2.6", - "enumerate-devices": "^1.1.1", - "eventemitter2": "~2.2.2", - "history": "~4.5.1", - "hoist-non-react-statics": "^3.3.2", - "js-cookie": "^3.0.1", - "pdfobject": "^2.2.7", - "preact": "10.11.3", - "redux": "^4.0.5", - "socket.io-client": "^4.2.0", - "supports-webp": "~1.0.3", - "uuid": "^8.3.2", - "visibilityjs": "~1.2.4", - "xstate": "^4.33.6" - }, - "bin": { - "migrate_locales": "scripts/migrate_locales.js" - } - }, - "node_modules/onfido-sdk-ui/node_modules/classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "license": "MIT" + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-14.15.0.tgz", + "integrity": "sha512-4Z+tnH6pQjK4SyazlzJq17NXO8AnhGcwEACbA3PVbAo90LBpGu1WAZ1r6VidlxFr/oPbu6sg/hisYvfXiqOtTg==" }, "node_modules/open": { "version": "8.4.2", @@ -41856,10 +41362,6 @@ "canvas": "^2.11.2" } }, - "node_modules/pdfobject": { - "version": "2.2.8", - "license": "MIT" - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -42246,15 +41748,6 @@ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" }, - "node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -43420,16 +42913,6 @@ "react-native-reanimated": ">=2.8.0" } }, - "node_modules/react-native-flipper": { - "version": "0.159.0", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": ">0.62.0" - } - }, "node_modules/react-native-fs": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz", @@ -43662,17 +43145,6 @@ "react-native": "*" } }, - "node_modules/react-native-performance-flipper-reporter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-native-performance-flipper-reporter/-/react-native-performance-flipper-reporter-2.0.0.tgz", - "integrity": "sha512-ccOgq99eK3OvrNNhpJDC4ydNk/1JGgWZPo2FLrPDLUHXAR4EcE9cUAtb46oGOpvHk5ZOb5aEDofc/CS9OEGcag==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react-native-flipper": "*", - "react-native-performance": "*" - } - }, "node_modules/react-native-permissions": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.9.3.tgz", @@ -43762,6 +43234,33 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/react-native-release-profiler": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/react-native-release-profiler/-/react-native-release-profiler-0.1.6.tgz", + "integrity": "sha512-kSAPYjO3PDzV4xbjgj2NoiHtL7EaXmBira/WOcyz6S7mz1MVBoF0Bj74z5jAZo6BoBJRKqmQWI4ep+m0xvoF+g==", + "dependencies": { + "@react-native-community/cli": "^12.2.1", + "commander": "^11.1.0" + }, + "bin": { + "react-native-release-profiler": "lib/commonjs/cli.js" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-release-profiler/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/react-native-render-html": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-6.3.1.tgz", @@ -43831,6 +43330,14 @@ "react-native": "*" } }, + "node_modules/react-native-share": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.0.2.tgz", + "integrity": "sha512-EZs4MtsyauAI1zP8xXT1hIFB/pXOZJNDCKcgCpEfTZFXgCUzz8MDVbI1ocP2hA59XHRSkqAQdbJ0BFTpjxOBlg==", + "engines": { + "node": ">=16" + } + }, "node_modules/react-native-sound": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.11.2.tgz", @@ -45796,12 +45303,6 @@ "node": ">=8" } }, - "node_modules/resolve-pathname": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==", - "license": "MIT" - }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -46170,6 +45671,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true, "license": "MIT" }, "node_modules/select": { @@ -47068,32 +46570,6 @@ "node": ">=0.10.0" } }, - "node_modules/socket.io-client": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", - "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -48154,12 +47630,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/supports-webp": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/supports-webp/-/supports-webp-1.0.7.tgz", - "integrity": "sha512-ZlqT+sCgZKcykOLrk8DYR4t3Em+nyVSHpiV3q7uzOutLwKIYU23n88KibCLw3FzM4NCQeRorvZ55AV/77lQyOQ==", - "license": "MIT" - }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", @@ -50348,12 +49818,6 @@ "builtins": "^1.0.3" } }, - "node_modules/value-equal": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.2.1.tgz", - "integrity": "sha512-yRL36Xb2K/HmFT5Fe3M86S7mu4+a12/3l7uytUh6eNPPjP77ldPBvsAvmnWff39sXn55naRMZN8LZWRO8PWaeQ==", - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -50421,12 +49885,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/visibilityjs": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/visibilityjs/-/visibilityjs-1.2.8.tgz", - "integrity": "sha512-Y+aL3OUX88b+/VSmkmC2ApuLbf0grzbNLpCfIDSw3BzTU6PqcPsdgIOaw8b+eZoy+DdQqnVN3y/Evow9vQq9Ig==", - "license": "MIT" - }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", @@ -50488,15 +49946,6 @@ "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", "license": "MIT" }, - "node_modules/warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==", - "license": "BSD-3-Clause", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -51930,22 +51379,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/xstate": { - "version": "4.37.2", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/xstate" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 487e345e28dd..45c898e0ab70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.49-4", + "version": "1.4.51-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -50,13 +50,15 @@ "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", - "test:e2e": "ts-node tests/e2e/testRunner.js --config ./config.local.ts", - "test:e2e:dev": "ts-node tests/e2e/testRunner.js --config ./config.dev.js", + "symbolicate-release:ios": "scripts/release-profile.js --platform=ios", + "symbolicate-release:android": "scripts/release-profile.js --platform=android", + "test:e2e": "ts-node tests/e2e/testRunner.ts --config ./config.local.ts", + "test:e2e:dev": "ts-node tests/e2e/testRunner.ts --config ./config.dev.ts", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.ts", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", - "e2e-test-runner-build": "ncc build tests/e2e/testRunner.js -o tests/e2e/dist/" + "e2e-test-runner-build": "ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/" }, "dependencies": { "@dotlottie/react-player": "^1.6.3", @@ -101,7 +103,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -113,7 +115,7 @@ "lodash": "4.17.21", "lottie-react-native": "6.4.1", "mapbox-gl": "^2.15.0", - "onfido-sdk-ui": "13.6.1", + "onfido-sdk-ui": "14.15.0", "patch-package": "^8.0.0", "process": "^0.11.10", "prop-types": "^15.7.2", @@ -155,10 +157,12 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", + "react-native-release-profiler": "^0.1.6", "react-native-reanimated": "^3.7.2", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", + "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", "react-native-tab-view": "^3.5.2", @@ -276,7 +280,6 @@ "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", "react-native-clean-project": "^4.0.0-alpha4.0", - "react-native-performance-flipper-reporter": "^2.0.0", "react-test-renderer": "18.2.0", "reassure": "^0.10.1", "setimmediate": "^1.0.5", diff --git a/patches/@react-native-community+cli-platform-ios+12.3.0.patch b/patches/@react-native-community+cli-platform-ios+12.3.0.patch index cfae504e44fa..e54ab17c43dd 100644 --- a/patches/@react-native-community+cli-platform-ios+12.3.0.patch +++ b/patches/@react-native-community+cli-platform-ios+12.3.0.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb -index 82f537c..f5e2cda 100644 +index 82f537c..df441e2 100644 --- a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb +++ b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb @@ -12,7 +12,7 @@ @@ -19,7 +19,7 @@ index 82f537c..f5e2cda 100644 if (!config) json = [] -@@ -36,10 +35,30 @@ def use_native_modules!(config = nil) +@@ -36,9 +35,24 @@ def use_native_modules!(config = nil) config = JSON.parse(json.join("\n")) end @@ -42,11 +42,5 @@ index 82f537c..f5e2cda 100644 + end + packages = config["dependencies"] -+ -+ if (ENV["NO_FLIPPER"]) -+ packages = {**packages, "react-native-flipper" => {"platforms" => {"ios" => nil}}} -+ end -+ found_pods = [] - packages.each do |package_name, package| diff --git a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch b/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch index 877521094cd4..c65ebbb98007 100644 --- a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch +++ b/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch @@ -43,7 +43,7 @@ index 7558eb3..b7bb75e 100644 }) : STATE_TRANSITIONING_OR_BELOW_TOP; } + -+ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Settings_Root') && isScreenActive !== STATE_ON_TOP; ++ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Workspace_Initial') && isScreenActive !== STATE_ON_TOP; + const { headerShown = true, diff --git a/patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch similarity index 100% rename from patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch rename to patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch diff --git a/patches/react-native-reanimated+3.7.1.patch b/patches/react-native-reanimated+3.7.2.patch similarity index 100% rename from patches/react-native-reanimated+3.7.1.patch rename to patches/react-native-reanimated+3.7.2.patch diff --git a/scripts/release-profile.js b/scripts/release-profile.js new file mode 100755 index 000000000000..0f96232bcdca --- /dev/null +++ b/scripts/release-profile.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +const fs = require('fs'); +const {execSync} = require('child_process'); + +// Function to parse command-line arguments into a key-value object +function parseCommandLineArguments() { + const args = process.argv.slice(2); // Skip node and script paths + const argsMap = {}; + args.forEach((arg) => { + const [key, value] = arg.split('='); + if (key.startsWith('--')) { + argsMap[key.substring(2)] = value; + } + }); + return argsMap; +} + +// Function to find .cpuprofile files in the current directory +function findCpuProfileFiles() { + const files = fs.readdirSync(process.cwd()); + // eslint-disable-next-line rulesdir/prefer-underscore-method + return files.filter((file) => file.endsWith('.cpuprofile')); +} + +const argsMap = parseCommandLineArguments(); + +// Determine sourcemapPath based on the platform flag passed +let sourcemapPath; +if (argsMap.platform === 'ios') { + sourcemapPath = 'main.jsbundle.map'; +} else if (argsMap.platform === 'android') { + sourcemapPath = 'android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map'; +} else { + console.error('Please specify the platform using --platform=ios or --platform=android'); + process.exit(1); +} + +// Attempt to find .cpuprofile files +const cpuProfiles = findCpuProfileFiles(); +if (cpuProfiles.length === 0) { + console.error('No .cpuprofile files found in the root directory.'); + process.exit(1); +} else if (cpuProfiles.length > 1) { + console.error('Multiple .cpuprofile files found. Please specify which one to use by placing only one .cpuprofile in the root or specifying the filename as an argument.'); + process.exit(1); +} else { + // Construct the command + const cpuprofileName = cpuProfiles[0]; + const command = `npx react-native-release-profiler --local ${cpuprofileName} --sourcemap-path ${sourcemapPath}`; + + console.log(`Executing: ${command}`); + + // Execute the command + try { + const output = execSync(command, {stdio: 'inherit'}); + console.log(output.toString()); + } catch (error) { + console.error(`Error executing command: ${error}`); + process.exit(1); + } +} diff --git a/src/CONST.ts b/src/CONST.ts index ce2029c78713..d4fbd0ff6ef3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -98,6 +98,8 @@ const CONST = { AVATAR_MAX_WIDTH_PX: 4096, AVATAR_MAX_HEIGHT_PX: 4096, + LOGO_MAX_SCALE: 1.5, + BREADCRUMB_TYPE: { ROOT: 'root', STRONG: 'strong', @@ -1169,6 +1171,7 @@ const CONST = { MISSING_FIELD: 'Missing required additional details fields', WRONG_ANSWERS: 'Wrong answers', ONFIDO_FIXABLE_ERROR: 'Onfido returned a fixable error', + ONFIDO_USER_CONSENT_DENIED: 'user_consent_denied', // KBA stands for Knowledge Based Answers (requiring us to show Idology questions) KBA_NEEDED: 'KBA needed', @@ -1495,8 +1498,6 @@ const CONST = { ALPHABETIC_AND_LATIN_CHARS: /^[\p{Script=Latin} ]*$/u, NON_ALPHABETIC_AND_NON_LATIN_CHARS: /[^\p{Script=Latin}]/gu, ACCENT_LATIN_CHARS: /[\u00C0-\u017F]/g, - INVALID_DISPLAY_NAME_LHN: /[^\p{L}\p{N}\u00C0-\u017F\s-]/gu, - INVALID_DISPLAY_NAME_ONLY_LHN: /^[^\p{L}\p{N}\u00C0-\u017F]$/gu, POSITIVE_INTEGER: /^\d+$/, PO_BOX: /\b[P|p]?(OST|ost)?\.?\s*[O|o|0]?(ffice|FFICE)?\.?\s*[B|b][O|o|0]?[X|x]?\.?\s+[#]?(\d+)\b/, ANY_VALUE: /^.+$/, @@ -1660,6 +1661,8 @@ const CONST = { LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, + TAG_NAME_LIMIT: 256, + TITLE_CHARACTER_LIMIT: 100, DESCRIPTION_LIMIT: 500, @@ -3161,7 +3164,7 @@ const CONST = { SHARE_CODE: 'shareCode', }, REVENUE: 250, - LEARN_MORE_LINK: 'https://help.expensify.com/articles/new-expensify/get-paid-back/Referral-Program', + LEARN_MORE_LINK: 'https://help.expensify.com/articles/new-expensify/expenses/Referral-Program', LINK: 'https://join.my.expensify.com', }, diff --git a/src/Expensify.tsx b/src/Expensify.tsx index f822862ec434..5681be838ca8 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -183,12 +183,12 @@ function Expensify({ // If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report Linking.getInitialURL().then((url) => { setInitialUrl(url); - Report.openReportFromDeepLink(url ?? '', isAuthenticated); + Report.openReportFromDeepLink(url ?? ''); }); // Open chat report from a deep link (only mobile native) Linking.addEventListener('url', (state) => { - Report.openReportFromDeepLink(state.url, isAuthenticated); + Report.openReportFromDeepLink(state.url); }); return () => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bb1766f40e1f..8c48cbad561f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -216,6 +216,9 @@ const ONYXKEYS = { /** Is the test tools modal open? */ IS_TEST_TOOLS_MODAL_OPEN: 'isTestToolsModalOpen', + /** Is app in profiling mode */ + APP_PROFILING_IN_PROGRESS: 'isProfilingInProgress', + /** Stores information about active wallet transfer amount, selectedAccountID, status, etc */ WALLET_TRANSFER: 'walletTransfer', @@ -329,6 +332,8 @@ const ONYXKEYS = { WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate', WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft', + WORKSPACE_TAG_CREATE_FORM: 'workspaceTagCreate', + WORKSPACE_TAG_CREATE_FORM_DRAFT: 'workspaceTagCreateDraft', WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft', WORKSPACE_DESCRIPTION_FORM: 'workspaceDescriptionForm', WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft', @@ -402,6 +407,8 @@ const ONYXKEYS = { EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft', EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm', EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft', + POLICY_TAG_NAME_FORM: 'policyTagNameForm', + POLICY_TAG_NAME_FORM_DRAFT: 'policyTagNameFormDraft', }, } as const; @@ -411,6 +418,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm; + [ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM]: FormTypes.WorkspaceTagCreateForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; @@ -447,6 +455,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; + [ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm; }; type OnyxFormDraftValuesMapping = { @@ -553,6 +562,7 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_LOADING_PAYMENT_METHODS]: boolean; [ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean; + [ONYXKEYS.APP_PROFILING_IN_PROGRESS]: boolean; [ONYXKEYS.IS_LOADING_APP]: boolean; [ONYXKEYS.IS_SWITCHING_TO_OLD_DOT]: boolean; [ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 856a6fb89a3e..defb945ba8c2 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -450,128 +450,148 @@ const ROUTES = { WORKSPACE_NEW: 'workspace/new', WORKSPACE_NEW_ROOM: 'workspace/new-room', WORKSPACE_INITIAL: { - route: 'workspace/:policyID', - getRoute: (policyID: string) => `workspace/${policyID}` as const, + route: 'settings/workspaces/:policyID', + getRoute: (policyID: string) => `settings/workspaces/${policyID}` as const, }, WORKSPACE_INVITE: { - route: 'workspace/:policyID/invite', - getRoute: (policyID: string) => `workspace/${policyID}/invite` as const, + route: 'settings/workspaces/:policyID/invite', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/invite` as const, }, WORKSPACE_INVITE_MESSAGE: { - route: 'workspace/:policyID/invite-message', - getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const, + route: 'settings/workspaces/:policyID/invite-message', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/invite-message` as const, }, WORKSPACE_PROFILE: { - route: 'workspace/:policyID/profile', - getRoute: (policyID: string) => `workspace/${policyID}/profile` as const, + route: 'settings/workspaces/:policyID/profile', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile` as const, }, WORKSPACE_PROFILE_CURRENCY: { - route: 'workspace/:policyID/profile/currency', - getRoute: (policyID: string) => `workspace/${policyID}/profile/currency` as const, + route: 'settings/workspaces/:policyID/profile/currency', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/currency` as const, }, WORKSPACE_PROFILE_NAME: { - route: 'workspace/:policyID/profile/name', - getRoute: (policyID: string) => `workspace/${policyID}/profile/name` as const, + route: 'settings/workspaces/:policyID/profile/name', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/name` as const, }, WORKSPACE_PROFILE_DESCRIPTION: { - route: 'workspace/:policyID/profile/description', - getRoute: (policyID: string) => `workspace/${policyID}/profile/description` as const, + route: 'settings/workspaces/:policyID/profile/description', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/description` as const, }, WORKSPACE_PROFILE_SHARE: { - route: 'workspace/:policyID/profile/share', - getRoute: (policyID: string) => `workspace/${policyID}/profile/share` as const, + route: 'settings/workspaces/:policyID/profile/share', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/share` as const, }, WORKSPACE_AVATAR: { - route: 'workspace/:policyID/avatar', - getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const, + route: 'settings/workspaces/:policyID/avatar', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/avatar` as const, }, WORKSPACE_JOIN_USER: { - route: 'workspace/:policyID/join', - getRoute: (policyID: string, inviterEmail: string) => `workspace/${policyID}/join?email=${inviterEmail}` as const, + route: 'settings/workspaces/:policyID/join', + getRoute: (policyID: string, inviterEmail: string) => `settings/workspaces/${policyID}/join?email=${inviterEmail}` as const, }, WORKSPACE_SETTINGS_CURRENCY: { - route: 'workspace/:policyID/settings/currency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, + route: 'settings/workspaces/:policyID/settings/currency', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/currency` as const, }, WORKSPACE_WORKFLOWS: { - route: 'workspace/:policyID/workflows', - getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const, + route: 'settings/workspaces/:policyID/workflows', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/workflows` as const, + }, + WORKSPACE_WORKFLOWS_PAYER: { + route: 'workspace/:policyID/settings/workflows/payer', + getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/payer` as const, }, WORKSPACE_WORKFLOWS_APPROVER: { - route: 'workspace/:policyID/settings/workflows/approver', - getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/approver` as const, + route: 'settings/workspaces/:policyID/settings/workflows/approver', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/approver` as const, }, WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY: { - route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency` as const, + route: 'settings/workspaces/:policyID/settings/workflows/auto-reporting-frequency', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/auto-reporting-frequency` as const, }, WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET: { - route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency/monthly-offset', - getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency/monthly-offset` as const, + route: 'settings/workspaces/:policyID/settings/workflows/auto-reporting-frequency/monthly-offset', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/auto-reporting-frequency/monthly-offset` as const, }, WORKSPACE_CARD: { - route: 'workspace/:policyID/card', - getRoute: (policyID: string) => `workspace/${policyID}/card` as const, + route: 'settings/workspaces/:policyID/card', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/card` as const, }, WORKSPACE_REIMBURSE: { - route: 'workspace/:policyID/reimburse', - getRoute: (policyID: string) => `workspace/${policyID}/reimburse` as const, + route: 'settings/workspaces/:policyID/reimburse', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/reimburse` as const, }, WORKSPACE_RATE_AND_UNIT: { - route: 'workspace/:policyID/rateandunit', - getRoute: (policyID: string) => `workspace/${policyID}/rateandunit` as const, + route: 'settings/workspaces/:policyID/rateandunit', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit` as const, }, WORKSPACE_RATE_AND_UNIT_RATE: { - route: 'workspace/:policyID/rateandunit/rate', - getRoute: (policyID: string) => `workspace/${policyID}/rateandunit/rate` as const, + route: 'settings/workspaces/:policyID/rateandunit/rate', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit/rate` as const, }, WORKSPACE_RATE_AND_UNIT_UNIT: { - route: 'workspace/:policyID/rateandunit/unit', - getRoute: (policyID: string) => `workspace/${policyID}/rateandunit/unit` as const, + route: 'settings/workspaces/:policyID/rateandunit/unit', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit/unit` as const, }, WORKSPACE_BILLS: { - route: 'workspace/:policyID/bills', - getRoute: (policyID: string) => `workspace/${policyID}/bills` as const, + route: 'settings/workspaces/:policyID/bills', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/bills` as const, }, WORKSPACE_INVOICES: { - route: 'workspace/:policyID/invoices', - getRoute: (policyID: string) => `workspace/${policyID}/invoices` as const, + route: 'settings/workspaces/:policyID/invoices', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/invoices` as const, }, WORKSPACE_TRAVEL: { - route: 'workspace/:policyID/travel', - getRoute: (policyID: string) => `workspace/${policyID}/travel` as const, + route: 'settings/workspaces/:policyID/travel', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/travel` as const, }, WORKSPACE_MEMBERS: { - route: 'workspace/:policyID/members', - getRoute: (policyID: string) => `workspace/${policyID}/members` as const, + route: 'settings/workspaces/:policyID/members', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const, }, WORKSPACE_CATEGORIES: { - route: 'workspace/:policyID/categories', - getRoute: (policyID: string) => `workspace/${policyID}/categories` as const, + route: 'settings/workspaces/:policyID/categories', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const, }, WORKSPACE_CATEGORY_SETTINGS: { - route: 'workspace/:policyID/categories/:categoryName', - getRoute: (policyID: string, categoryName: string) => `workspace/${policyID}/categories/${encodeURI(categoryName)}` as const, + route: 'settings/workspaces/:policyID/categories/:categoryName', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURI(categoryName)}` as const, }, WORKSPACE_CATEGORIES_SETTINGS: { - route: 'workspace/:policyID/categories/settings', - getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, + route: 'settings/workspaces/:policyID/categories/settings', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const, + }, + WORKSPACE_MORE_FEATURES: { + route: 'workspace/:policyID/more-features', + getRoute: (policyID: string) => `workspace/${policyID}/more-features` as const, }, WORKSPACE_CATEGORY_CREATE: { - route: 'workspace/:policyID/categories/new', - getRoute: (policyID: string) => `workspace/${policyID}/categories/new` as const, + route: 'settings/workspaces/:policyID/categories/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const, }, WORKSPACE_TAGS: { - route: 'workspace/:policyID/tags', - getRoute: (policyID: string) => `workspace/${policyID}/tags` as const, + route: 'settings/workspaces/:policyID/tags', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const, + }, + WORKSPACE_TAG_CREATE: { + route: 'settings/workspaces/:policyID/tags/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/new` as const, + }, + WORKSPACE_TAGS_SETTINGS: { + route: 'settings/workspaces/:policyID/tags/settings', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/settings` as const, + }, + WORKSPACE_EDIT_TAGS: { + route: 'settings/workspaces/:policyID/tags/edit', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/edit` as const, }, WORKSPACE_MEMBER_DETAILS: { - route: 'workspace/:policyID/members/:accountID', - getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo), + route: 'settings/workspaces/:policyID/members/:accountID', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/members/${accountID}`, backTo), }, WORKSPACE_MEMBER_ROLE_SELECTION: { - route: 'workspace/:policyID/members/:accountID/role-selection', - getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo), + route: 'settings/workspaces/:policyID/members/:accountID/role-selection', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/members/${accountID}/role-selection`, backTo), }, WORKSPACE_DISTANCE_RATES: { route: 'workspace/:policyID/distance-rates', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8546f543b77a..4db5fd9115a5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -22,6 +22,7 @@ const SCREENS = { VALIDATE_LOGIN: 'ValidateLogin', UNLINK_LOGIN: 'UnlinkLogin', SETTINGS_CENTRAL_PANE: 'SettingsCentralPane', + WORKSPACES_CENTRAL_PANE: 'WorkspacesCentralPane', SETTINGS: { ROOT: 'Settings_Root', SHARE_CODE: 'Settings_Share_Code', @@ -216,8 +217,12 @@ const SCREENS = { INVITE_MESSAGE: 'Workspace_Invite_Message', CATEGORIES: 'Workspace_Categories', TAGS: 'Workspace_Tags', + TAGS_SETTINGS: 'Tags_Settings', + TAGS_EDIT: 'Tags_Edit', + TAG_CREATE: 'Tag_Create', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', + WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', @@ -227,6 +232,7 @@ const SCREENS = { CATEGORY_CREATE: 'Category_Create', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', + MORE_FEATURES: 'Workspace_More_Features', MEMBER_DETAILS: 'Workspace_Member_Details', MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection', DISTANCE_RATES: 'Distance_Rates', diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 083c8340baa6..9713e40136a2 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -4,6 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {getCurrentUserAccountID} from '@libs/actions/Report'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -31,7 +32,8 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report?.ownerAccountID ?? 0]); + const actorPersonalDetails = personalDetails?.[reportClosedAction?.actorAccountID ?? 0]; + let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(actorPersonalDetails); let oldDisplayName: string | undefined; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { @@ -56,6 +58,7 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} displayName: `${displayName}`, oldDisplayName: `${oldDisplayName}`, policyName: `${policyName}`, + shouldUseYou: actorPersonalDetails?.accountID === getCurrentUserAccountID(), }) : translate(`reportArchiveReasons.${archiveReason}`); diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 2f80af7f572a..1ed7b6d188a0 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -446,7 +446,7 @@ function AttachmentModal({ onSelected: () => downloadAttachment(), }); } - if (TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction) && canEditReceipt) { + if (TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction) && canEditReceipt && !TransactionUtils.hasMissingSmartscanFields(transaction)) { menuItems.push({ icon: Expensicons.Trashcan, text: translate('receipt.deleteReceipt'), diff --git a/src/components/AvatarSkeleton.tsx b/src/components/AvatarSkeleton.tsx index 0887830aa07a..a6781448c3ba 100644 --- a/src/components/AvatarSkeleton.tsx +++ b/src/components/AvatarSkeleton.tsx @@ -1,21 +1,24 @@ import React from 'react'; import {Circle} from 'react-native-svg'; import useTheme from '@hooks/useTheme'; +import variables from '@styles/variables'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; function AvatarSkeleton() { const theme = useTheme(); + const skeletonCircleRadius = variables.componentSizeSmall / 2; + return ( ); diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index 2fd733d4b072..42b91b3d2d71 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import * as UserUtils from '@libs/UserUtils'; +import CONST from '@src/CONST'; import Avatar from './Avatar'; import AvatarSkeleton from './AvatarSkeleton'; import * as Expensicons from './Icon/Expensicons'; @@ -33,6 +34,7 @@ function AvatarWithIndicator({source, tooltipText = '', fallbackIcon = Expensico ) : ( <> diff --git a/src/components/BaseMiniContextMenuItem.tsx b/src/components/BaseMiniContextMenuItem.tsx index 7bed44cd8f13..6e1a1e0fd229 100644 --- a/src/components/BaseMiniContextMenuItem.tsx +++ b/src/components/BaseMiniContextMenuItem.tsx @@ -32,13 +32,20 @@ type BaseMiniContextMenuItemProps = { * Whether the button should be in the active state */ isDelayButtonStateComplete: boolean; + /** + * Can be used to control the click event, and for example whether or not to lose focus from the composer when pressing the item + */ + shouldPreventDefaultFocusOnPress?: boolean; }; /** * Component that renders a mini context menu item with a * pressable. Also renders a tooltip when hovering the item. */ -function BaseMiniContextMenuItem({tooltipText, onPress, children, isDelayButtonStateComplete = true}: BaseMiniContextMenuItemProps, ref: ForwardedRef) { +function BaseMiniContextMenuItem( + {tooltipText, onPress, children, isDelayButtonStateComplete = true, shouldPreventDefaultFocusOnPress = true}: BaseMiniContextMenuItemProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); return ( @@ -64,7 +71,9 @@ function BaseMiniContextMenuItem({tooltipText, onPress, children, isDelayButtonS } // Prevent text input blur on left click - event.preventDefault(); + if (shouldPreventDefaultFocusOnPress) { + event.preventDefault(); + } }} accessibilityLabel={tooltipText} role={CONST.ROLE.BUTTON} diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 34bc3f7e30c8..e5eb09691eba 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; -import {View} from 'react-native'; +import {PixelRatio, View} from 'react-native'; import LogoComponent from '@assets/images/expensify-wordmark.svg'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -36,7 +36,7 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { const theme = useTheme(); const styles = useThemeStyles(); const [primaryBreadcrumb, secondaryBreadcrumb] = breadcrumbs; - + const fontScale = PixelRatio.getFontScale() > CONST.LOGO_MAX_SCALE ? CONST.LOGO_MAX_SCALE : PixelRatio.getFontScale(); return ( {primaryBreadcrumb.type === CONST.BREADCRUMB_TYPE.ROOT ? ( @@ -47,8 +47,8 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { contentFit="contain" src={LogoComponent} fill={theme.text} - width={variables.lhnLogoWidth} - height={variables.lhnLogoHeight} + width={variables.lhnLogoWidth * fontScale} + height={variables.lhnLogoHeight * fontScale} /> } shouldShowEnvironmentBadge diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index fcf1baaa6aed..798369292958 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -15,7 +15,7 @@ type WorkspaceDistanceRatesBulkActionType = DeepValueOf = { value: TValueType; text: string; - icon: IconAsset; + icon?: IconAsset; iconWidth?: number; iconHeight?: number; iconDescription?: string; @@ -58,7 +58,7 @@ type ButtonWithDropdownMenuProps = { anchorAlignment?: AnchorAlignment; /* ref for the button */ - buttonRef: RefObject; + buttonRef?: RefObject; /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ enterKeyEventListenerPriority?: number; diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 7e7720b57a6e..5445816f067b 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -127,3 +127,5 @@ function Checkbox( Checkbox.displayName = 'Checkbox'; export default forwardRef(Checkbox); + +export type {CheckboxProps}; diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index 7f05b45bca30..17c5097b8154 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -25,13 +25,13 @@ type ConfirmedRoutePropsOnyxProps = { type ConfirmedRouteProps = ConfirmedRoutePropsOnyxProps & { /** Transaction that stores the distance request data */ - transaction: Transaction; + transaction: OnyxEntry; }; function ConfirmedRoute({mapboxAccessToken, transaction}: ConfirmedRouteProps) { const {isOffline} = useNetwork(); - const {route0: route} = transaction.routes ?? {}; - const waypoints = transaction.comment?.waypoints ?? {}; + const {route0: route} = transaction?.routes ?? {}; + const waypoints = transaction?.comment?.waypoints ?? {}; const coordinates = route?.geometry?.coordinates ?? []; const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index d6c8fd973983..b80d6a138c9e 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -44,6 +44,8 @@ type ContextMenuItemProps = { /** Styles to apply to ManuItem wrapper */ wrapperStyle?: StyleProp; + + shouldPreventDefaultFocusOnPress?: boolean; }; type ContextMenuItemHandle = { @@ -63,6 +65,7 @@ function ContextMenuItem( isFocused = false, shouldLimitWidth = true, wrapperStyle, + shouldPreventDefaultFocusOnPress = true, }: ContextMenuItemProps, ref: ForwardedRef, ) { @@ -94,6 +97,7 @@ function ContextMenuItem( tooltipText={itemText} onPress={triggerPressAndUpdateSuccess} isDelayButtonStateComplete={!isThrottledButtonActive} + shouldPreventDefaultFocusOnPress={shouldPreventDefaultFocusOnPress} > {({hovered, pressed}) => ( { - const yearsList = searchText === '' ? years : years.filter((year) => year.text.includes(searchText)); + const yearsList = searchText === '' ? years : years.filter((year) => year.text?.includes(searchText)); return { headerMessage: !yearsList.length ? translate('common.noResultsFound') : '', sections: [{data: yearsList.sort((a, b) => b.value - a.value), indexOffset: 0}], diff --git a/src/components/DragAndDrop/Provider/types.ts b/src/components/DragAndDrop/Provider/types.ts index b4394056cac5..57d0fb47c637 100644 --- a/src/components/DragAndDrop/Provider/types.ts +++ b/src/components/DragAndDrop/Provider/types.ts @@ -8,7 +8,7 @@ type DragAndDropProviderProps = { isDisabled?: boolean; /** Indicate that users are dragging file or not */ - setIsDraggingOver: (value: boolean) => void; + setIsDraggingOver?: (value: boolean) => void; }; type SetOnDropHandlerCallback = (event: DragEvent) => void; diff --git a/src/components/EReceipt.tsx b/src/components/EReceipt.tsx index 183d88ba1c6a..40f5d242d005 100644 --- a/src/components/EReceipt.tsx +++ b/src/components/EReceipt.tsx @@ -105,3 +105,4 @@ export default withOnyx({ key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, }, })(EReceipt); +export type {EReceiptProps, EReceiptOnyxProps}; diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index f5f9b5fc5f06..023dcc16e696 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -114,3 +114,4 @@ export default withOnyx({ key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, }, })(EReceiptThumbnail); +export type {EReceiptThumbnailProps, EReceiptThumbnailOnyxProps}; diff --git a/src/components/FlatList/index.android.tsx b/src/components/FlatList/index.android.tsx index 1246367d29e8..863930203863 100644 --- a/src/components/FlatList/index.android.tsx +++ b/src/components/FlatList/index.android.tsx @@ -1,7 +1,7 @@ import {useFocusEffect} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useContext} from 'react'; -import type {FlatListProps, NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; +import type {FlatListProps} from 'react-native'; import {FlatList} from 'react-native'; import {ActionListContext} from '@pages/home/ReportScreenContext'; @@ -22,9 +22,6 @@ function CustomFlatList(props: FlatListProps, ref: ForwardedRef) } }, [scrollPosition?.offset, ref]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const onMomentumScrollEnd = useCallback((event: NativeSyntheticEvent) => setScrollPosition({offset: event.nativeEvent.contentOffset.y}), []); - useFocusEffect( useCallback(() => { onScreenFocus(); @@ -35,8 +32,10 @@ function CustomFlatList(props: FlatListProps, ref: ForwardedRef) // eslint-disable-next-line react/jsx-props-no-spreading {...props} - onScroll={props.onScroll} - onMomentumScrollEnd={onMomentumScrollEnd} + onScroll={(event) => props.onScroll?.(event)} + onMomentumScrollEnd={(event) => { + setScrollPosition({offset: event.nativeEvent.contentOffset.y}); + }} ref={ref} /> ); diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 88938f31cd79..b9c52ad397ec 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -104,13 +104,13 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo }; return ( - - - + + + { fabPressable.current = el ?? null; @@ -136,9 +136,9 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo /> - - - + + + ); } diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index ad09b68a5f39..ee3b3607401e 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -394,3 +394,5 @@ export default withOnyx({ key: (props) => `${props.formID}Draft` as any, }, })(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; + +export type {FormProviderProps}; diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 9968bb0e0772..270d476f4f79 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -120,3 +120,5 @@ function FormAlertWithSubmitButton({ FormAlertWithSubmitButton.displayName = 'FormAlertWithSubmitButton'; export default FormAlertWithSubmitButton; + +export type {FormAlertWithSubmitButtonProps}; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js b/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js deleted file mode 100644 index 2432d1b1748c..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; -import {Animated} from 'react-native'; - -const propTypes = { - /** GrowlNotification content */ - children: PropTypes.node.isRequired, - - /** GrowlNotification Y postion, required to show or hide with fling animation */ - translateY: PropTypes.instanceOf(Animated.Value).isRequired, -}; - -export default propTypes; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.js deleted file mode 100644 index ccc404d415d7..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useThemeStyles from '@hooks/useThemeStyles'; -import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; - -const propTypes = { - ...growlNotificationContainerPropTypes, - ...windowDimensionsPropTypes, -}; - -function GrowlNotificationContainer(props) { - const styles = useThemeStyles(); - return ( - - {props.children} - - ); -} - -GrowlNotificationContainer.propTypes = propTypes; -GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; - -export default withWindowDimensions(GrowlNotificationContainer); diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js deleted file mode 100644 index 207033f8fac2..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; - -const propTypes = { - ...growlNotificationContainerPropTypes, -}; - -function GrowlNotificationContainer(props) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const insets = useSafeAreaInsets; - - return ( - - {props.children} - - ); -} - -GrowlNotificationContainer.propTypes = propTypes; -GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; - -export default GrowlNotificationContainer; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx new file mode 100644 index 000000000000..efd143c9487c --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import {Animated} from 'react-native'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type GrowlNotificationContainerProps from './types'; + +function GrowlNotificationContainer({children, translateY}: GrowlNotificationContainerProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const insets = useSafeAreaInsets(); + + return {children}; +} + +GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; + +export default GrowlNotificationContainer; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx new file mode 100644 index 000000000000..3bbd0303906d --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import {Animated} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type GrowlNotificationContainerProps from './types'; + +function GrowlNotificationContainer({children, translateY}: GrowlNotificationContainerProps) { + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + + return ( + + {children} + + ); +} + +GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; + +export default GrowlNotificationContainer; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/types.ts b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts new file mode 100644 index 000000000000..91a48437dbd9 --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts @@ -0,0 +1,8 @@ +import type {Animated} from 'react-native'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type GrowlNotificationContainerProps = ChildrenProps & { + translateY: Animated.Value; +}; + +export default GrowlNotificationContainerProps; diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.tsx similarity index 82% rename from src/components/GrowlNotification/index.js rename to src/components/GrowlNotification/index.tsx index ed0dd302f705..d0846dcf7a42 100644 --- a/src/components/GrowlNotification/index.js +++ b/src/components/GrowlNotification/index.tsx @@ -1,6 +1,8 @@ +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {Animated, View} from 'react-native'; import {Directions, Gesture, GestureDetector} from 'react-native-gesture-handler'; +import type {SvgProps} from 'react-native-svg'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Pressables from '@components/Pressable'; @@ -8,6 +10,7 @@ import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Growl from '@libs/Growl'; +import type {GrowlRef} from '@libs/Growl'; import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; import GrowlNotificationContainer from './GrowlNotificationContainer'; @@ -16,15 +19,29 @@ const INACTIVE_POSITION_Y = -255; const PressableWithoutFeedback = Pressables.PressableWithoutFeedback; -function GrowlNotification(_, ref) { +function GrowlNotification(_: unknown, ref: ForwardedRef) { const translateY = useRef(new Animated.Value(INACTIVE_POSITION_Y)).current; const [bodyText, setBodyText] = useState(''); const [type, setType] = useState('success'); - const [duration, setDuration] = useState(); + const [duration, setDuration] = useState(); const theme = useTheme(); const styles = useThemeStyles(); - const types = { + type GrowlIconTypes = Record< + /** String representing the growl type, all type strings + * for growl notifications are stored in CONST.GROWL + */ + string, + { + /** Expensicon for the page */ + icon: React.FC; + + /** Color for the icon (should be from theme) */ + iconColor: string; + } + >; + + const types: GrowlIconTypes = { [CONST.GROWL.SUCCESS]: { icon: Expensicons.Checkmark, iconColor: theme.success, @@ -46,7 +63,7 @@ function GrowlNotification(_, ref) { * @param {String} type * @param {Number} duration */ - const show = useCallback((text, growlType, growlDuration) => { + const show = useCallback((text: string, growlType: string, growlDuration: number) => { setBodyText(text); setType(growlType); setDuration(growlDuration); @@ -61,7 +78,6 @@ function GrowlNotification(_, ref) { (val = INACTIVE_POSITION_Y) => { Animated.spring(translateY, { toValue: val, - duration: 80, useNativeDriver, }).start(); }, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index 5d8c0f6ef81e..0327b6bc6f56 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -1,3 +1,4 @@ +import Str from 'expensify-common/lib/str'; import cloneDeep from 'lodash/cloneDeep'; import isEmpty from 'lodash/isEmpty'; import React from 'react'; @@ -65,10 +66,11 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona // We need to remove the LTR unicode and leading @ from data as it is not part of the login displayNameOrLogin = tnodeClone.data.replace(CONST.UNICODE.LTR, '').slice(1); // We need to replace tnode.data here because we will pass it to TNodeChildrenRenderer below - asMutable(tnodeClone).data = tnodeClone.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID)); + asMutable(tnodeClone).data = tnodeClone.data.replace(displayNameOrLogin, Str.removeSMSDomain(getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID))); accountID = PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])?.[0]; navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin); + displayNameOrLogin = Str.removeSMSDomain(displayNameOrLogin); } else { // If neither an account ID or email is provided, don't render anything return null; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 25532107016f..68c445cc944c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -55,3 +55,5 @@ function Header({title = '', subtitle = '', textStyles = [], shouldShowEnvironme Header.displayName = 'Header'; export default Header; + +export type {HeaderProps}; diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 3a1c35d46c94..21f3e9a3b605 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {Keyboard, StyleSheet, View} from 'react-native'; +import Avatar from '@components/Avatar'; import AvatarWithDisplayName from '@components/AvatarWithDisplayName'; import Header from '@components/Header'; import Icon from '@components/Icon'; @@ -32,7 +33,8 @@ function HeaderWithBackButton({ onThreeDotsButtonPress = () => {}, report = null, policy, - shouldShowAvatarWithDisplay = false, + policyAvatar, + shouldShowReportAvatarWithDisplay = false, shouldShowBackButton = true, shouldShowBorderBottom = false, shouldShowCloseButton = false, @@ -58,6 +60,7 @@ function HeaderWithBackButton({ shouldOverlay = false, singleExecution = (func) => func, shouldNavigateToTopMostReport = false, + style, }: HeaderWithBackButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -81,6 +84,7 @@ function HeaderWithBackButton({ shouldShowBorderBottom && styles.borderBottom, shouldShowBackButton && styles.pl2, shouldOverlay && StyleSheet.absoluteFillObject, + style, ]} > @@ -118,7 +122,15 @@ function HeaderWithBackButton({ additionalStyles={[styles.mr2]} /> )} - {shouldShowAvatarWithDisplay ? ( + {policyAvatar && ( + + )} + {shouldShowReportAvatarWithDisplay ? ( & { /** Data to display a step counter in the header */ stepCounter?: StepCounterParams; - /** Whether we should show an avatar */ - shouldShowAvatarWithDisplay?: boolean; + /** Whether we should show a report avatar */ + shouldShowReportAvatarWithDisplay?: boolean; /** Parent report, if provided it will override props.report for AvatarWithDisplay */ parentReport?: OnyxEntry; @@ -101,6 +103,9 @@ type HeaderWithBackButtonProps = Partial & { /** The report's policy, if we're showing the details for a report and need info about it for AvatarWithDisplay */ policy?: OnyxEntry; + /** Policies, if we're showing the details for a report and need participant details for AvatarWithDisplay */ + personalDetails?: OnyxCollection; + /** Single execution function to prevent concurrent navigation actions */ singleExecution?: (action: Action) => Action; @@ -118,6 +123,12 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should overlay the 3 dots menu */ shouldOverlayDots?: boolean; + + /** Policy avatar to display in the header */ + policyAvatar?: Icon; + + /** Additional styles to add to the component */ + style?: StyleProp; }; export type {ThreeDotsMenuItem}; diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 58cefb1877ce..28d1d53ed60c 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -29,13 +29,16 @@ import TadaYellow from '@assets/images/product-illustrations/tada--yellow.svg'; import TeleScope from '@assets/images/product-illustrations/telescope.svg'; import ThreeLeggedLaptopWoman from '@assets/images/product-illustrations/three_legged_laptop_woman.svg'; import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg'; +import Accounting from '@assets/images/simple-illustrations/simple-illustration__accounting.svg'; import Approval from '@assets/images/simple-illustrations/simple-illustration__approval.svg'; import BankArrow from '@assets/images/simple-illustrations/simple-illustration__bank-arrow.svg'; import BigRocket from '@assets/images/simple-illustrations/simple-illustration__bigrocket.svg'; import PinkBill from '@assets/images/simple-illustrations/simple-illustration__bill.svg'; import CarIce from '@assets/images/simple-illustrations/simple-illustration__car-ice.svg'; +import Car from '@assets/images/simple-illustrations/simple-illustration__car.svg'; import ChatBubbles from '@assets/images/simple-illustrations/simple-illustration__chatbubbles.svg'; import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg'; +import Coins from '@assets/images/simple-illustrations/simple-illustration__coins.svg'; import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg'; import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg'; import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg'; @@ -59,6 +62,7 @@ import MoneyIntoWallet from '@assets/images/simple-illustrations/simple-illustra import MoneyWings from '@assets/images/simple-illustrations/simple-illustration__moneywings.svg'; import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__opensafe.svg'; import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg'; +import Pencil from '@assets/images/simple-illustrations/simple-illustration__pencil.svg'; import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; @@ -148,6 +152,10 @@ export { Workflows, ThreeLeggedLaptopWoman, House, + Accounting, + Car, + Coins, + Pencil, Tag, CarIce, }; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 1420a6abe189..e3d226a17999 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -1,17 +1,24 @@ import React from 'react'; import {StyleSheet, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as UserUtils from '@libs/UserUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BankAccountList, FundList, LoginList, UserWallet, WalletTerms} from '@src/types/onyx'; +import type {BankAccountList, FundList, LoginList, Policy, PolicyMembers, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; type CheckingMethod = () => boolean; type IndicatorOnyxProps = { + /** The employee list of all policies (coming from Onyx) */ + allPolicyMembers: OnyxCollection; + + /** All the user's policies (from Onyx via withFullPolicy) */ + policies: OnyxCollection; + /** List of bank accounts */ bankAccountList: OnyxEntry; @@ -21,6 +28,9 @@ type IndicatorOnyxProps = { /** The user's wallet (coming from Onyx) */ userWallet: OnyxEntry; + /** Bank account attached to free plan */ + reimbursementAccount: OnyxEntry; + /** Information about the user accepting the terms for payments */ walletTerms: OnyxEntry; @@ -30,16 +40,25 @@ type IndicatorOnyxProps = { type IndicatorProps = IndicatorOnyxProps; -function Indicator({bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) { +function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) { const theme = useTheme(); const styles = useThemeStyles(); + // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and + // those should be cleaned out before doing any error checking + const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)); + const cleanAllPolicyMembers = Object.fromEntries(Object.entries(allPolicyMembers ?? {}).filter(([, policyMembers]) => !!policyMembers)); + // All of the error & info-checking methods are put into an array. This is so that using _.some() will return // early as soon as the first error / info condition is returned. This makes the checks very efficient since // we only care if a single error / info condition exists anywhere. const errorCheckingMethods: CheckingMethod[] = [ () => Object.keys(userWallet?.errors ?? {}).length > 0, () => PaymentMethods.hasPaymentMethodError(bankAccountList, fundList), + () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError), + () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError), + () => Object.values(cleanAllPolicyMembers).some(PolicyUtils.hasPolicyMemberError), + () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0, () => !!loginList && UserUtils.hasLoginListError(loginList), // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) @@ -58,9 +77,19 @@ function Indicator({bankAccountList, fundList, userWallet, walletTerms, loginLis Indicator.displayName = 'Indicator'; export default withOnyx({ + allPolicyMembers: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, bankAccountList: { key: ONYXKEYS.BANK_ACCOUNT_LIST, }, + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, fundList: { key: ONYXKEYS.FUND_LIST, }, diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 0549e19c2eb4..e28400505280 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -6,11 +6,6 @@ import FlatList from '@components/FlatList'; const WINDOW_SIZE = 15; const AUTOSCROLL_TO_TOP_THRESHOLD = 128; -const maintainVisibleContentPosition = { - minIndexForVisible: 0, - autoscrollToTopThreshold: AUTOSCROLL_TO_TOP_THRESHOLD, -}; - function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { return ( (props: FlatListProps, ref: ForwardedRef ); diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 93eac30d5477..27f424ad1b70 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,8 +1,9 @@ import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {memo, useCallback, useMemo} from 'react'; +import React, {memo, useCallback} from 'react'; import {StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import withCurrentReportID from '@components/withCurrentReportID'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -27,10 +28,10 @@ function LHNOptionsList({ preferredLocale = CONST.LOCALES.DEFAULT, personalDetails = {}, transactions = {}, + currentReportID = '', draftComments = {}, transactionViolations = {}, onFirstItemRendered = () => {}, - reportIDsWithErrors = {}, }: LHNOptionsListProps) { const styles = useThemeStyles(); const {canUseViolations} = usePermissions(); @@ -62,7 +63,6 @@ function LHNOptionsList({ const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); const lastReportAction = sortedReportActions[0]; - const reportErrors = reportIDsWithErrors[reportID] ?? {}; // Get the transaction for the last report action let lastReportActionTransactionID = ''; @@ -84,18 +84,18 @@ function LHNOptionsList({ lastReportActionTransaction={lastReportActionTransaction} receiptTransactions={transactions} viewMode={optionMode} - isFocused={!shouldDisableFocusOptions} + isFocused={!shouldDisableFocusOptions && reportID === currentReportID} onSelectRow={onSelectRow} preferredLocale={preferredLocale} comment={itemComment} transactionViolations={transactionViolations} canUseViolations={canUseViolations} onLayout={onLayoutItem} - reportErrors={reportErrors} /> ); }, [ + currentReportID, draftComments, onSelectRow, optionMode, @@ -109,12 +109,9 @@ function LHNOptionsList({ transactionViolations, canUseViolations, onLayoutItem, - reportIDsWithErrors, ], ); - const extraData = useMemo(() => [reportActions, reports, policy, personalDetails], [reportActions, reports, policy, personalDetails]); - return ( @@ -135,31 +132,33 @@ function LHNOptionsList({ LHNOptionsList.displayName = 'LHNOptionsList'; -export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - policy: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - draftComments: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, -})(memo(LHNOptionsList)); +export default withCurrentReportID( + withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + reportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + }, + policy: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, + draftComments: { + key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, + })(memo(LHNOptionsList)), +); export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 9b22b50b64fe..a18d5a8ec1ec 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,6 +1,5 @@ import {deepEqual} from 'fast-equals'; import React, {useEffect, useMemo, useRef} from 'react'; -import useCurrentReportID from '@hooks/useCurrentReportID'; import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import * as Report from '@userActions/Report'; @@ -29,12 +28,9 @@ function OptionRowLHNData({ lastReportActionTransaction = {}, transactionViolations, canUseViolations, - reportErrors, ...propsToForward }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; - const currentReportIDValue = useCurrentReportID(); - const isReportFocused = isFocused && currentReportIDValue?.currentReportID === reportID; const optionItemRef = useRef(); @@ -44,11 +40,11 @@ function OptionRowLHNData({ // Note: ideally we'd have this as a dependent selector in onyx! const item = SidebarUtils.getOptionData({ report: fullReport, + reportActions, personalDetails, preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, policy, parentReportAction, - reportErrors, hasViolations: !!hasViolations, }); if (deepEqual(item, optionItemRef.current)) { @@ -73,7 +69,6 @@ function OptionRowLHNData({ transactionViolations, canUseViolations, receiptTransactions, - reportErrors, ]); useEffect(() => { @@ -88,7 +83,7 @@ function OptionRowLHNData({ ); diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 4ca30358f9b1..58bea97f04c9 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -3,10 +3,10 @@ import type {RefObject} from 'react'; import type {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; -import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; type OptionMode = ValueOf; @@ -58,12 +58,9 @@ type CustomLHNOptionsListProps = { /** Callback to fire when the list is laid out */ onFirstItemRendered: () => void; - - /** Report IDs with errors mapping to their corresponding error objects */ - reportIDsWithErrors: Record; }; -type LHNOptionsListProps = CustomLHNOptionsListProps & LHNOptionsListOnyxProps; +type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps; type OptionRowLHNDataProps = { /** Whether row should be focused */ @@ -116,9 +113,6 @@ type OptionRowLHNDataProps = { /** Callback to execute when the OptionList lays out */ onLayout?: (event: LayoutChangeEvent) => void; - - /** The report errors */ - reportErrors: OnyxCommon.Errors | undefined; }; type OptionRowLHNProps = { diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx index 5c672cf7cab6..08e7613dc7a9 100644 --- a/src/components/Lottie/index.tsx +++ b/src/components/Lottie/index.tsx @@ -4,6 +4,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef} from 'react'; import {View} from 'react-native'; import type DotLottieAnimation from '@components/LottieAnimations/types'; +import useAppState from '@hooks/useAppState'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,6 +13,7 @@ type Props = { } & Omit; function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef) { + const appState = useAppState(); const styles = useThemeStyles(); const [isError, setIsError] = React.useState(false); @@ -19,8 +21,10 @@ function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef; } diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 2520520fd467..74fec2c606af 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -20,6 +20,7 @@ import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; +import {usePersonalDetails} from './OnyxProvider'; import SettlementButton from './SettlementButton'; type PaymentType = DeepValueOf; @@ -40,10 +41,11 @@ type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & { report: OnyxTypes.Report; /** The policy tied to the money request report */ - policy: OnyxTypes.Policy; + policy: OnyxEntry; }; function MoneyReportHeader({session, policy, chatReport, nextStep, report: moneyRequestReport}: MoneyReportHeaderProps) { + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); @@ -79,8 +81,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !policy.harvesting?.enabled, - [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled], + () => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled, + [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled], ); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)]; @@ -95,11 +97,12 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money return ( Navigation.goBack(undefined, false, true)} // Shows border if no buttons or next steps are showing below the header diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.tsx similarity index 55% rename from src/components/MoneyRequestConfirmationList.js rename to src/components/MoneyRequestConfirmationList.tsx index 51a9148a3e6a..2bad8322c8d9 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,18 +1,18 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; -import {isEmpty} from 'lodash'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {ValueOf} from 'type-fest'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import type {DefaultMileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; @@ -23,250 +23,261 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import {policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {AllRoutes} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import categoryPropTypes from './categoryPropTypes'; +import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; import FormHelpMessage from './FormHelpMessage'; import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import optionPropTypes from './optionPropTypes'; import OptionsSelector from './OptionsSelector'; import ReceiptEmptyState from './ReceiptEmptyState'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; -import tagPropTypes from './tagPropTypes'; import Text from './Text'; -import transactionPropTypes from './transactionPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; -const propTypes = { +type IouType = ValueOf; + +type MoneyRequestConfirmationListOnyxProps = { + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; + + /** Collection of tags attached to a policy */ + policyTags: OnyxEntry; + + /** The policy of the report */ + policy: OnyxEntry; + + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + iou: OnyxEntry; + + /** The session of the logged in user */ + session: OnyxEntry; + + /** Unit and rate used for if the money request is a distance request */ + mileageRate: OnyxEntry; +}; +type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { /** Callback to inform parent modal of success */ - onConfirm: PropTypes.func, + onConfirm?: (selectedParticipants: Participant[]) => void; /** Callback to parent modal to send money */ - onSendMoney: PropTypes.func, + onSendMoney?: (paymentMethod: IouType | PaymentMethodType | undefined) => void; /** Callback to inform a participant is selected */ - onSelectParticipant: PropTypes.func, + onSelectParticipant?: (option: Participant) => void; /** Should we request a single or multiple participant selection from user */ - hasMultipleParticipants: PropTypes.bool.isRequired, + hasMultipleParticipants: boolean; /** IOU amount */ - iouAmount: PropTypes.number.isRequired, + iouAmount: number; /** IOU comment */ - iouComment: PropTypes.string, + iouComment?: string; /** IOU currency */ - iouCurrencyCode: PropTypes.string, + iouCurrencyCode?: string; /** IOU type */ - iouType: PropTypes.string, + iouType?: IouType; /** IOU date */ - iouCreated: PropTypes.string, + iouCreated?: string; /** IOU merchant */ - iouMerchant: PropTypes.string, + iouMerchant?: string; /** IOU Category */ - iouCategory: PropTypes.string, + iouCategory?: string; /** IOU Tag */ - iouTag: PropTypes.string, + iouTag?: string; /** IOU isBillable */ - iouIsBillable: PropTypes.bool, + iouIsBillable?: boolean; /** Callback to toggle the billable state */ - onToggleBillable: PropTypes.func, + onToggleBillable?: (isOn: boolean) => void; /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired, + selectedParticipants: Participant[]; /** Payee of the money request with login */ - payeePersonalDetails: optionPropTypes, + payeePersonalDetails?: OnyxTypes.PersonalDetails; /** Can the participants be modified or not */ - canModifyParticipants: PropTypes.bool, + canModifyParticipants?: boolean; /** Should the list be read only, and not editable? */ - isReadOnly: PropTypes.bool, + isReadOnly?: boolean; /** Depending on expense report or personal IOU report, respective bank account route */ - bankAccountRoute: PropTypes.string, - - ...withCurrentUserPersonalDetailsPropTypes, - - /** Current user session */ - session: PropTypes.shape({ - email: PropTypes.string.isRequired, - }), + bankAccountRoute?: AllRoutes; /** The policyID of the request */ - policyID: PropTypes.string, + policyID?: string; /** The reportID of the request */ - reportID: PropTypes.string, + reportID?: string; /** File path of the receipt */ - receiptPath: PropTypes.string, + receiptPath?: string; /** File name of the receipt */ - receiptFilename: PropTypes.string, + receiptFilename?: string; /** List styles for OptionsSelector */ - listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + listStyles?: StyleProp; /** ID of the transaction that represents the money request */ - transactionID: PropTypes.string, + transactionID?: string; /** Transaction that represents the money request */ - transaction: transactionPropTypes, - - /** Unit and rate used for if the money request is a distance request */ - mileageRate: PropTypes.shape({ - /** Unit used to represent distance */ - unit: PropTypes.oneOf([CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]), - - /** Rate used to calculate the distance request amount */ - rate: PropTypes.number, - - /** The currency of the rate */ - currency: PropTypes.string, - }), + transaction?: OnyxEntry; /** Whether the money request is a distance request */ - isDistanceRequest: PropTypes.bool, + isDistanceRequest?: boolean; /** Whether the money request is a scan request */ - isScanRequest: PropTypes.bool, + isScanRequest?: boolean; /** Whether we're editing a split bill */ - isEditingSplitBill: PropTypes.bool, + isEditingSplitBill?: boolean; /** Whether we should show the amount, date, and merchant fields. */ - shouldShowSmartScanFields: PropTypes.bool, + shouldShowSmartScanFields?: boolean; /** A flag for verifying that the current report is a sub-report of a workspace chat */ - isPolicyExpenseChat: PropTypes.bool, + isPolicyExpenseChat?: boolean; - /* Onyx Props */ - /** Collection of categories attached to a policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), - - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, + /** Whether smart scan failed */ + hasSmartScanFailed?: boolean; - /* Onyx Props */ - /** The policy of the report */ - policy: policyPropTypes.policy, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, + /** The ID of the report action */ + reportActionID?: string; }; -const defaultProps = { - onConfirm: () => {}, - onSendMoney: () => {}, - onSelectParticipant: () => {}, - iouType: CONST.IOU.TYPE.REQUEST, - iouCategory: '', - iouTag: '', - iouIsBillable: false, - onToggleBillable: () => {}, - payeePersonalDetails: null, - canModifyParticipants: false, - isReadOnly: false, - bankAccountRoute: '', - session: { - email: null, +function MoneyRequestConfirmationList({ + transaction = null, + onSendMoney, + onConfirm, + onSelectParticipant, + iouType = CONST.IOU.TYPE.REQUEST, + isScanRequest = false, + iouAmount, + policyCategories, + mileageRate, + isDistanceRequest = false, + policy, + isPolicyExpenseChat = false, + iouCategory = '', + shouldShowSmartScanFields = true, + isEditingSplitBill, + policyTags, + iouCurrencyCode, + iouMerchant, + hasMultipleParticipants, + selectedParticipants: selectedParticipantsProp, + payeePersonalDetails: payeePersonalDetailsProp, + iou = { + id: '', + amount: 0, + currency: CONST.CURRENCY.USD, + comment: '', + merchant: '', + category: '', + tag: '', + billable: false, + created: '', + participants: [], + receiptPath: '', }, - policyID: '', - reportID: '', - ...withCurrentUserPersonalDetailsDefaultProps, - receiptPath: '', - receiptFilename: '', - listStyles: [], - policy: {}, - policyCategories: {}, - policyTags: {}, - transactionID: '', - transaction: {}, - mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'}, - isDistanceRequest: false, - isScanRequest: false, - shouldShowSmartScanFields: true, - isPolicyExpenseChat: false, - iou: iouDefaultProps, -}; - -function MoneyRequestConfirmationList(props) { + canModifyParticipants: canModifyParticipantsProp = false, + session, + isReadOnly = false, + bankAccountRoute = '', + policyID = '', + reportID = '', + receiptPath = '', + iouComment, + receiptFilename = '', + listStyles, + iouCreated, + iouIsBillable = false, + onToggleBillable, + iouTag = '', + transactionID = '', + hasSmartScanFailed, + reportActionID, +}: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); - // Destructure functions from props to pass it as a dependecy to useCallback/useMemo hooks. - // Prop functions pass props itself as a "this" value to the function which means they change every time props change. - const {onSendMoney, onConfirm, onSelectParticipant} = props; const {translate, toLocaleDigit} = useLocalize(); - const transaction = props.transaction; const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST; - const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT; - const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND; + const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; + const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const isTypeSend = iouType === CONST.IOU.TYPE.SEND; const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isSplitBill); - const isSplitWithScan = isSplitBill && props.isScanRequest; + const isSplitWithScan = isSplitBill && isScanRequest; - const {unit, rate, currency} = props.mileageRate; - const distance = lodashGet(transaction, 'routes.route0.distance', 0); - const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0; - const taxRates = lodashGet(props.policy, 'taxRates', {}); + const {unit, rate, currency} = mileageRate ?? { + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + rate: 0, + currency: CONST.CURRENCY.USD, + }; + const distance = transaction?.routes?.route0.distance ?? 0; + const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; + const taxRates = policy?.taxRates; // A flag for showing the categories field - const shouldShowCategories = props.isPolicyExpenseChat && (props.iouCategory || OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories))); + const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); // Do not hide fields in case of send money request - const shouldShowAllFields = props.isDistanceRequest || shouldExpandFields || !props.shouldShowSmartScanFields || isTypeSend || props.isEditingSplitBill; + const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan; - const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !props.isDistanceRequest && !isSplitWithScan; + const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !isDistanceRequest && !isSplitWithScan; - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(props.policyTags), [props.policyTags]); + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for showing the tags field - const shouldShowTags = props.isPolicyExpenseChat && (props.iouTag || OptionsListUtils.hasEnabledTags(policyTagLists)); + const shouldShowTags = isPolicyExpenseChat && (!!iouTag || OptionsListUtils.hasEnabledTags(policyTagLists)); // A flag for showing tax fields - tax rate and tax amount - const shouldShowTax = props.isPolicyExpenseChat && lodashGet(props.policy, 'tax.trackingEnabled', props.policy.isTaxTrackingEnabled); + const shouldShowTax = isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled); // A flag for showing the billable field - const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true); + const shouldShowBillable = !(policy?.disabledFields?.defaultBillable ?? true); const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithPendingRoute = props.isDistanceRequest && (!hasRoute || !rate); + const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate); const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, - props.isDistanceRequest ? currency : props.iouCurrencyCode, + shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount, + isDistanceRequest ? currency : iouCurrencyCode, ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); - const defaultTaxKey = taxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; - const taxRateTitle = (props.transaction.taxRate && props.transaction.taxRate.text) || defaultTaxName; + const defaultTaxKey = taxRates?.defaultExternalID; + const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) ?? ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing is not working when a left hand side value is '' + const taxRateTitle = transaction?.taxRate?.text || defaultTaxName; const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); @@ -274,67 +285,65 @@ function MoneyRequestConfirmationList(props) { const [didConfirm, setDidConfirm] = useState(false); const [didConfirmSplit, setDidConfirmSplit] = useState(false); - const shouldDisplayFieldError = useMemo(() => { - if (!props.isEditingSplitBill) { + const shouldDisplayFieldError: boolean = useMemo(() => { + if (!isEditingSplitBill) { return false; } - return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); - }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); + return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); + }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); - const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const shouldDisplayMerchantError = props.isPolicyExpenseChat && shouldDisplayFieldError && isMerchantEmpty; + const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; + const shouldDisplayMerchantError = isPolicyExpenseChat && shouldDisplayFieldError && isMerchantEmpty; useEffect(() => { if (shouldDisplayFieldError && didConfirmSplit) { setFormError('iou.error.genericSmartscanFailureMessage'); return; } - if (shouldDisplayFieldError && props.hasSmartScanFailed) { + if (shouldDisplayFieldError && hasSmartScanFailed) { setFormError('iou.receiptScanningFailed'); return; } // reset the form error whenever the screen gains or loses focus setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, props.hasSmartScanFailed, didConfirmSplit]); + }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]); useEffect(() => { if (!shouldCalculateDistanceAmount) { return; } - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate); + const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0); IOU.setMoneyRequestAmount(amount); }, [shouldCalculateDistanceAmount, distance, rate, unit]); /** * Returns the participants with amount - * @param {Array} participants - * @returns {Array} */ const getParticipantsWithAmount = useCallback( - (participantsList) => { - const iouAmount = IOUUtils.calculateAmount(participantsList.length, props.iouAmount, props.iouCurrencyCode); + (participantsList: Participant[]): Participant[] => { + const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participantsList, - props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(iouAmount, props.iouCurrencyCode) : '', + iouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '', ); }, - [props.iouAmount, props.iouCurrencyCode], + [iouAmount, iouCurrencyCode], ); // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again - if (props.isEditingSplitBill && didConfirm) { + if (isEditingSplitBill && didConfirm) { setDidConfirm(false); } - const splitOrRequestOptions = useMemo(() => { + const splitOrRequestOptions: Array> = useMemo(() => { let text; - if (isSplitBill && props.iouAmount === 0) { + if (isSplitBill && iouAmount === 0) { text = translate('iou.split'); - } else if ((props.receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { + } else if ((!!receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { text = translate('iou.request'); - if (props.iouAmount !== 0) { + if (iouAmount !== 0) { text = translate('iou.requestAmount', {amount: formattedAmount}); } } else { @@ -344,34 +353,34 @@ function MoneyRequestConfirmationList(props) { return [ { text: text[0].toUpperCase() + text.slice(1), - value: props.iouType, + value: iouType, }, ]; - }, [isSplitBill, isTypeRequest, props.iouType, props.iouAmount, props.receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); + }, [isSplitBill, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); - const selectedParticipants = useMemo(() => _.filter(props.selectedParticipants, (participant) => participant.selected), [props.selectedParticipants]); - const payeePersonalDetails = useMemo(() => props.payeePersonalDetails || props.currentUserPersonalDetails, [props.payeePersonalDetails, props.currentUserPersonalDetails]); - const canModifyParticipants = !props.isReadOnly && props.canModifyParticipants && props.hasMultipleParticipants; + const selectedParticipants: Participant[] = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); + const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); + const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; const shouldDisablePaidBySection = canModifyParticipants; - const optionSelectorSections = useMemo(() => { + const optionSelectorSections: OptionsListUtils.CategorySection[] = useMemo(() => { const sections = []; - const unselectedParticipants = _.filter(props.selectedParticipants, (participant) => !participant.selected); - if (props.hasMultipleParticipants) { + const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); + if (hasMultipleParticipants) { const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants); + let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; if (!canModifyParticipants) { - formattedParticipantsList = _.map(formattedParticipantsList, (participant) => ({ + formattedParticipantsList = formattedParticipantsList.map((participant) => ({ ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), + isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), })); } - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, props.iouAmount, props.iouCurrencyCode, true); + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( payeePersonalDetails, - props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, props.iouCurrencyCode) : '', + iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', ); sections.push( @@ -390,9 +399,9 @@ function MoneyRequestConfirmationList(props) { }, ); } else { - const formattedSelectedParticipants = _.map(props.selectedParticipants, (participant) => ({ + const formattedSelectedParticipants = selectedParticipantsProp.map((participant) => ({ ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), + isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), })); sections.push({ title: translate('common.to'), @@ -403,27 +412,27 @@ function MoneyRequestConfirmationList(props) { } return sections; }, [ - props.selectedParticipants, - props.hasMultipleParticipants, - props.iouAmount, - props.iouCurrencyCode, - getParticipantsWithAmount, selectedParticipants, + hasMultipleParticipants, + iouAmount, + iouCurrencyCode, + getParticipantsWithAmount, + selectedParticipantsProp, payeePersonalDetails, translate, shouldDisablePaidBySection, canModifyParticipants, ]); - const selectedOptions = useMemo(() => { - if (!props.hasMultipleParticipants) { + const selectedOptions: Array = useMemo(() => { + if (!hasMultipleParticipants) { return []; } return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, props.hasMultipleParticipants, payeePersonalDetails]); + }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]); useEffect(() => { - if (!props.isDistanceRequest) { + if (!isDistanceRequest) { return; } @@ -432,31 +441,27 @@ function MoneyRequestConfirmationList(props) { When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page. In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending. */ - IOU.setMoneyRequestPendingFields(props.transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); + IOU.setMoneyRequestPendingFields(transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); - IOU.setMoneyRequestMerchant(props.transactionID, distanceMerchant, false); - }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]); + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? CONST.CURRENCY.USD, translate, toLocaleDigit); + IOU.setMoneyRequestMerchant(transactionID, distanceMerchant, false); + }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transactionID]); - /** - * @param {Object} option - */ const selectParticipant = useCallback( - (option) => { + (option: Participant) => { // Return early if selected option is currently logged in user. - if (option.accountID === props.session.accountID) { + if (option.accountID === session?.accountID) { return; } - onSelectParticipant(option); + onSelectParticipant?.(option); }, - [props.session.accountID, onSelectParticipant], + [session?.accountID, onSelectParticipant], ); /** * Navigate to report details or profile of selected user - * @param {Object} option */ - const navigateToReportOrUserDetail = (option) => { + const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => { if (option.accountID) { const activeRoute = Navigation.getActiveRouteWithoutParams(); @@ -466,19 +471,16 @@ function MoneyRequestConfirmationList(props) { } }; - /** - * @param {String} paymentMethod - */ const confirm = useCallback( - (paymentMethod) => { - if (_.isEmpty(selectedParticipants)) { + (paymentMethod: IouType | PaymentMethodType | undefined) => { + if (!selectedParticipants.length) { return; } - if (props.iouCategory && props.iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { + if (iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { setFormError('iou.error.invalidCategoryLength'); return; } - if (props.iouType === CONST.IOU.TYPE.SEND) { + if (iouType === CONST.IOU.TYPE.SEND) { if (!paymentMethod) { return; } @@ -486,45 +488,45 @@ function MoneyRequestConfirmationList(props) { setDidConfirm(true); Log.info(`[IOU] Sending money via: ${paymentMethod}`); - onSendMoney(paymentMethod); + onSendMoney?.(paymentMethod); } else { // validate the amount for distance requests - const decimals = CurrencyUtils.getCurrencyDecimals(props.iouCurrencyCode); - if (props.isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(props.iouAmount), decimals)) { + const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); + if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { setFormError('common.error.invalidAmount'); return; } - if (props.isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { + if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { setDidConfirmSplit(true); return; } setDidConfirm(true); - onConfirm(selectedParticipants); + onConfirm?.(selectedParticipants); } }, [ selectedParticipants, onSendMoney, onConfirm, - props.isEditingSplitBill, - props.iouType, - props.isDistanceRequest, - props.iouCategory, + isEditingSplitBill, + iouType, + isDistanceRequest, + iouCategory, isDistanceRequestWithPendingRoute, - props.iouCurrencyCode, - props.iouAmount, + iouCurrencyCode, + iouAmount, transaction, ], ); const footerContent = useMemo(() => { - if (props.isReadOnly) { + if (isReadOnly) { return; } - const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; + const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; const shouldDisableButton = selectedParticipants.length === 0 || shouldDisplayMerchantError; const button = shouldShowSettlementButton ? ( @@ -533,10 +535,10 @@ function MoneyRequestConfirmationList(props) { isDisabled={shouldDisableButton} onPress={confirm} enablePaymentsRoute={ROUTES.IOU_SEND_ENABLE_PAYMENTS} - addBankAccountRoute={props.bankAccountRoute} + addBankAccountRoute={bankAccountRoute} addDebitCardRoute={ROUTES.IOU_SEND_ADD_DEBIT_CARD} - currency={props.iouCurrencyCode} - policyID={props.policyID} + currency={iouCurrencyCode} + policyID={policyID} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} kycWallAnchorAlignment={{ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, @@ -554,16 +556,16 @@ function MoneyRequestConfirmationList(props) { success pressOnEnter isDisabled={shouldDisableButton} + // eslint-disable-next-line @typescript-eslint/naming-convention onPress={(_event, value) => confirm(value)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} enterKeyEventListenerPriority={1} /> ); - return ( <> - {!_.isEmpty(formError) && ( + {!!formError.length && ( ); }, [ - props.isReadOnly, - props.iouType, - props.bankAccountRoute, - props.iouCurrencyCode, - props.policyID, + isReadOnly, + iouType, + bankAccountRoute, + iouCurrencyCode, + policyID, selectedParticipants.length, shouldDisplayMerchantError, confirm, @@ -589,8 +591,9 @@ function MoneyRequestConfirmationList(props) { ]); const {image: receiptImage, thumbnail: receiptThumbnail} = - props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, props.receiptPath, props.receiptFilename) : {}; + receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); return ( + // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) - {props.isDistanceRequest && ( + {isDistanceRequest && ( - + )} + {receiptImage || receiptThumbnail ? ( ) : ( // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(props.policy) && - !props.isDistanceRequest && - props.iouType === CONST.IOU.TYPE.REQUEST && ( + PolicyUtils.isPaidGroupPolicy(policy) && + !isDistanceRequest && + iouType === CONST.IOU.TYPE.REQUEST && ( Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( CONST.IOU.ACTION.CREATE, - props.iouType, - transaction.transactionID, - props.reportID, + iouType, + transaction?.transactionID ?? '', + reportID, Navigation.getActiveRouteWithoutParams(), ), ) @@ -643,49 +648,43 @@ function MoneyRequestConfirmationList(props) { /> ) )} - {props.shouldShowSmartScanFields && ( + {shouldShowSmartScanFields && ( { - if (props.isDistanceRequest) { + if (isDistanceRequest) { return; } - if (props.isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.AMOUNT)); + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(props.iouType, props.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(iouType, reportID)); }} style={[styles.moneyRequestMenuItem, styles.mt2]} titleStyle={styles.moneyRequestConfirmationAmount} disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} /> )} { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( - CONST.IOU.ACTION.EDIT, - props.iouType, - transaction.transactionID, - props.reportID, - Navigation.getActiveRouteWithoutParams(), - ), + ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), ); }} - style={[styles.moneyRequestMenuItem]} + style={styles.moneyRequestMenuItem} titleStyle={styles.flex1} disabled={didConfirm} - interactive={!props.isReadOnly} + interactive={!isReadOnly} numberOfLinesTitle={2} /> {!shouldShowAllFields && ( @@ -698,162 +697,162 @@ function MoneyRequestConfirmationList(props) { <> {shouldShowDate && ( { Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( CONST.IOU.ACTION.EDIT, - props.iouType, - transaction.transactionID, - props.reportID, + iouType, + transaction?.transactionID ?? '', + reportID, Navigation.getActiveRouteWithoutParams(), ), ); }} disabled={didConfirm} - interactive={!props.isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} /> )} - {props.isDistanceRequest && ( + {isDistanceRequest && ( Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(iouType, reportID))} disabled={didConfirm || !canEditDistance} - interactive={!props.isReadOnly} + interactive={!isReadOnly} /> )} {shouldShowMerchant && ( { Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( CONST.IOU.ACTION.EDIT, - props.iouType, - transaction.transactionID, - props.reportID, + iouType, + transaction?.transactionID ?? '', + reportID, Navigation.getActiveRouteWithoutParams(), ), ); }} disabled={didConfirm} - interactive={!props.isReadOnly} - brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={shouldDisplayMerchantError ? translate('common.error.enterMerchant') : ''} /> )} {shouldShowCategories && ( { Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute( CONST.IOU.ACTION.EDIT, - props.iouType, - props.transaction.transactionID, - props.reportID, + iouType, + transaction?.transactionID ?? '', + reportID, Navigation.getActiveRouteWithoutParams(), ), ); }} - style={[styles.moneyRequestMenuItem]} + style={styles.moneyRequestMenuItem} titleStyle={styles.flex1} disabled={didConfirm} - interactive={!props.isReadOnly} - rightLabel={canUseViolations && Boolean(props.policy.requiresCategory) ? translate('common.required') : ''} + interactive={!isReadOnly} + rightLabel={canUseViolations && !!policy?.requiresCategory ? translate('common.required') : ''} /> )} {shouldShowTags && - _.map(policyTagLists, ({name}, index) => ( + policyTagLists.map(({name}, index) => ( { - if (props.isEditingSplitBill) { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( - CONST.IOU.ACTION.EDIT, - CONST.IOU.TYPE.SPLIT, - index, - props.transaction.transactionID, - props.reportID, - Navigation.getActiveRouteWithoutParams(), - props.reportActionID, - ), - ); + if (!isEditingSplitBill) { return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(props.iouType, props.reportID)); + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( + CONST.IOU.ACTION.EDIT, + CONST.IOU.TYPE.SPLIT, + index, + transaction?.transactionID ?? '', + reportID, + Navigation.getActiveRouteWithoutParams(), + reportActionID, + ), + ); }} - style={[styles.moneyRequestMenuItem]} + style={styles.moneyRequestMenuItem} disabled={didConfirm} - interactive={!props.isReadOnly} - rightLabel={canUseViolations && Boolean(props.policy.requiresTag) ? translate('common.required') : ''} + interactive={!isReadOnly} + rightLabel={canUseViolations && !!policy?.requiresTag ? translate('common.required') : ''} /> ))} {shouldShowTax && ( Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()), + ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), ) } disabled={didConfirm} - interactive={!props.isReadOnly} + interactive={!isReadOnly} /> )} {shouldShowTax && ( Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()), + ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), ) } disabled={didConfirm} - interactive={!props.isReadOnly} + interactive={!isReadOnly} /> )} {shouldShowBillable && ( - {translate('common.billable')} + {translate('common.billable')} onToggleBillable?.(isOn)} /> )} @@ -863,34 +862,26 @@ function MoneyRequestConfirmationList(props) { ); } -MoneyRequestConfirmationList.propTypes = propTypes; -MoneyRequestConfirmationList.defaultProps = defaultProps; MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - policyCategories: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - }, - policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - }, - mileageRate: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - selector: DistanceRequestUtils.getDefaultMileageRate, - }, - splitTransactionDraft: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, - }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - iou: { - key: ONYXKEYS.IOU, - }, - }), -)(MoneyRequestConfirmationList); +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + policyCategories: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + }, + policyTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + }, + mileageRate: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: DistanceRequestUtils.getDefaultMileageRate, + }, + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, + iou: { + key: ONYXKEYS.IOU, + }, +})(MoneyRequestConfirmationList); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 338796cd856e..a9304b9c3138 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -21,6 +21,7 @@ import HeaderWithBackButton from './HeaderWithBackButton'; import HoldBanner from './HoldBanner'; import * as Expensicons from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; +import {usePersonalDetails} from './OnyxProvider'; import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; type MoneyRequestHeaderOnyxProps = { @@ -46,13 +47,14 @@ type MoneyRequestHeaderProps = MoneyRequestHeaderOnyxProps & { report: Report; /** The policy which the report is tied to */ - policy: Policy; + policy: OnyxEntry; /** The report action the transaction is tied to from the parent report */ - parentReportAction: ReportAction & OriginalMessageIOU; + parentReportAction: OnyxEntry; }; function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, shownHoldUseExplanation = false, policy}: MoneyRequestHeaderProps) { + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const styles = useThemeStyles(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -69,7 +71,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; const deleteTransaction = useCallback(() => { - IOU.deleteMoneyRequest(parentReportAction?.originalMessage?.IOUTransactionID ?? '', parentReportAction, true); + if (parentReportAction) { + const iouTransactionID = parentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + IOU.deleteMoneyRequest(iouTransactionID, parentReportAction, true); + } + setIsDeleteModalVisible(false); }, [parentReportAction, setIsDeleteModalVisible]); @@ -83,11 +89,13 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction; const changeMoneyRequestStatus = () => { + const iouTransactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + if (isOnHold) { - IOU.unholdRequest(parentReportAction?.originalMessage?.IOUTransactionID ?? '', report?.reportID); + IOU.unholdRequest(iouTransactionID, report?.reportID); } else { const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); - Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type, parentReportAction?.originalMessage?.IOUTransactionID ?? '', report?.reportID, activeRoute)); + Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? '', iouTransactionID, report?.reportID, activeRoute)); } }; @@ -155,7 +163,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, Navigation.goBack(undefined, false, true)} /> diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index 74a2326748a1..87d3bdada6a2 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -98,7 +98,6 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .enabled(pinchEnabled) // The first argument is not used, but must be defined - // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesDown((_evt, state) => { // We don't want to activate pinch gesture when we are swiping in the pager if (!shouldDisableTransformationGestures.value) { diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index a28333725d6e..f550e93d6be2 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -121,7 +121,6 @@ const useTapGestures = ({ const doubleTapGesture = Gesture.Tap() // The first argument is not used, but must be defined - // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesDown((_evt, state) => { if (!shouldDisableTransformationGestures.value) { return; @@ -156,7 +155,6 @@ const useTapGestures = ({ .onBegin(() => { stopAnimation(); }) - // eslint-disable-next-line @typescript-eslint/naming-convention .onFinalize((_evt, success) => { if (!success || onTap === undefined) { return; diff --git a/src/components/Onfido/BaseOnfidoWeb.js b/src/components/Onfido/BaseOnfidoWeb.js index ee206b15fc24..57f10f49f396 100644 --- a/src/components/Onfido/BaseOnfidoWeb.js +++ b/src/components/Onfido/BaseOnfidoWeb.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import * as OnfidoSDK from 'onfido-sdk-ui'; +import {Onfido as OnfidoSDK} from 'onfido-sdk-ui'; import React, {forwardRef, useEffect} from 'react'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; @@ -15,7 +15,6 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo OnfidoSDK.init({ token: sdkToken, containerId: CONST.ONFIDO.CONTAINER_ID, - useMemoryHistory: true, customUI: { fontFamilyTitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`, fontFamilySubtitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`, @@ -86,18 +85,15 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo onSuccess(data); }, onError: (error) => { - const errorMessage = lodashGet(error, 'message', CONST.ERROR.UNKNOWN_ERROR); const errorType = lodashGet(error, 'type'); + const errorMessage = lodashGet(error, 'message', CONST.ERROR.UNKNOWN_ERROR); Log.hmmm('Onfido error', {errorType, errorMessage}); + if (errorType === CONST.WALLET.ERROR.ONFIDO_USER_CONSENT_DENIED) { + onUserExit(); + return; + } onError(errorMessage); }, - onUserExit: (userExitCode) => { - Log.hmmm('Onfido user exits the flow', {userExitCode}); - onUserExit(userExitCode); - }, - onModalRequestClose: () => { - Log.hmmm('Onfido user closed the modal'); - }, language: { // We need to use ES_ES as locale key because the key `ES` is not a valid config key for Onfido locale: preferredLocale === CONST.LOCALES.ES ? CONST.LOCALES.ES_ES_ONFIDO : preferredLocale, diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 575df128894a..3844080c6f5d 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -133,7 +133,6 @@ function BaseOptionsList( * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] */ - // eslint-disable-next-line @typescript-eslint/naming-convention const getItemLayout = (_data: OptionsListData[] | null, flatDataArrayIndex: number) => { if (!flattenedData.current[flatDataArrayIndex]) { flattenedData.current = buildFlatSectionArray(); diff --git a/src/components/PDFThumbnail/index.native.tsx b/src/components/PDFThumbnail/index.native.tsx index 4d3a33ae4e67..0232dba99f05 100644 --- a/src/components/PDFThumbnail/index.native.tsx +++ b/src/components/PDFThumbnail/index.native.tsx @@ -6,7 +6,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import type PDFThumbnailProps from './types'; -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword = () => {}}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword}: PDFThumbnailProps) { const styles = useThemeStyles(); const sizeStyles = [styles.w100, styles.h100]; @@ -25,6 +25,9 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena if (!('message' in error && typeof error.message === 'string' && error.message.match(/password/i))) { return; } + if (!onPassword) { + return; + } onPassword(); }} /> diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index e69e4dd5075b..a5b911deb6ff 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -12,7 +12,7 @@ if (!pdfjs.GlobalWorkerOptions.workerSrc) { pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'})); } -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword = () => {}}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword}: PDFThumbnailProps) { const styles = useThemeStyles(); const thumbnail = useMemo( @@ -25,9 +25,7 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena cMapPacked: true, }} externalLinkTarget="_blank" - onPassword={() => { - onPassword(); - }} + onPassword={onPassword} > diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index a391ff061baa..83da817da858 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -18,7 +18,7 @@ import Text from './Text'; type PopoverMenuItem = { /** An icon element displayed on the left side */ - icon: IconAsset; + icon?: IconAsset; /** Text label */ text: string; diff --git a/src/components/ProfilingToolMenu/index.native.tsx b/src/components/ProfilingToolMenu/index.native.tsx new file mode 100644 index 000000000000..e6a89a317ac7 --- /dev/null +++ b/src/components/ProfilingToolMenu/index.native.tsx @@ -0,0 +1,179 @@ +import React, {useCallback, useEffect, useState} from 'react'; +import DeviceInfo from 'react-native-device-info'; +import RNFS from 'react-native-fs'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {startProfiling, stopProfiling} from 'react-native-release-profiler'; +import Share from 'react-native-share'; +import Button from '@components/Button'; +import Switch from '@components/Switch'; +import TestToolRow from '@components/TestToolRow'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import toggleProfileTool from '@libs/actions/ProfilingTool'; +import getPlatform from '@libs/getPlatform'; +import Log from '@libs/Log'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import pkg from '../../../package.json'; + +type ProfilingToolMenuOnyxProps = { + isProfilingInProgress: OnyxEntry; +}; + +type ProfilingToolMenuProps = ProfilingToolMenuOnyxProps; + +function formatBytes(bytes: number, decimals = 2) { + if (!+bytes) { + return '0 Bytes'; + } + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KiB', 'MiB', 'GiB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; +} + +function ProfilingToolMenu({isProfilingInProgress = false}: ProfilingToolMenuProps) { + const styles = useThemeStyles(); + const [pathIOS, setPathIOS] = useState(''); + const [sharePath, setSharePath] = useState(''); + const [totalMemory, setTotalMemory] = useState(0); + const [usedMemory, setUsedMemory] = useState(0); + + // eslint-disable-next-line @lwc/lwc/no-async-await + const stop = useCallback(async () => { + const path = await stopProfiling(getPlatform() === CONST.PLATFORM.IOS); + setPathIOS(path); + + const amountOfTotalMemory = await DeviceInfo.getTotalMemory(); + const amountOfUsedMemory = await DeviceInfo.getUsedMemory(); + setTotalMemory(amountOfTotalMemory); + setUsedMemory(amountOfUsedMemory); + }, []); + + const onToggleProfiling = useCallback(() => { + const shouldProfiling = !isProfilingInProgress; + if (shouldProfiling) { + startProfiling(); + } else { + stop(); + } + toggleProfileTool(); + return () => { + stop(); + }; + }, [isProfilingInProgress, stop]); + + const getAppInfo = useCallback( + () => + JSON.stringify({ + appVersion: pkg.version, + environment: CONFIG.ENVIRONMENT, + platform: getPlatform(), + totalMemory: formatBytes(totalMemory, 2), + usedMemory: formatBytes(usedMemory, 2), + }), + [totalMemory, usedMemory], + ); + + useEffect(() => { + if (!pathIOS) { + return; + } + + // eslint-disable-next-line @lwc/lwc/no-async-await + const rename = async () => { + const newFileName = `Profile_trace_for_${pkg.version}.cpuprofile`; + const newFilePath = `${RNFS.DocumentDirectoryPath}/${newFileName}`; + + try { + const fileExists = await RNFS.exists(newFilePath); + if (fileExists) { + await RNFS.unlink(newFilePath); + Log.hmmm('[ProfilingToolMenu] existing file deleted successfully'); + } + } catch (error) { + const typedError = error as Error; + Log.hmmm('[ProfilingToolMenu] error checking/deleting existing file: ', typedError.message); + } + + // Copy the file to a new location with the desired filename + await RNFS.copyFile(pathIOS, newFilePath) + .then(() => { + Log.hmmm('[ProfilingToolMenu] file copied successfully'); + }) + .catch((error) => { + Log.hmmm('[ProfilingToolMenu] error copying file: ', error); + }); + + setSharePath(newFilePath); + }; + + rename(); + }, [pathIOS]); + + const onDownloadProfiling = useCallback(() => { + // eslint-disable-next-line @lwc/lwc/no-async-await + const shareFiles = async () => { + try { + // Define new filename and path for the app info file + const infoFileName = `App_Info_${pkg.version}.json`; + const infoFilePath = `${RNFS.DocumentDirectoryPath}/${infoFileName}`; + const actualInfoFile = `file://${infoFilePath}`; + + await RNFS.writeFile(infoFilePath, getAppInfo(), 'utf8'); + + const shareOptions = { + urls: [`file://${sharePath}`, actualInfoFile], + }; + + await Share.open(shareOptions); + } catch (error) { + console.error('Error renaming and sharing file:', error); + } + }; + shareFiles(); + }, [getAppInfo, sharePath]); + + return ( + <> + + Release options + + + + + + {!!pathIOS && `path: ${pathIOS}`} + {!!pathIOS && ( + +