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/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..a4fd0796f6ff 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 1001045002 + versionName "1.4.50-2" } flavorDimensions "default" 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/desktop/package-lock.json b/desktop/package-lock.json index f6f96b647ae1..b8ae9d0a2be5 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -10,7 +10,7 @@ "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", "electron-serve": "^1.3.0", - "electron-updater": "^6.1.8", + "electron-updater": "^6.1.9", "node-machine-id": "^1.1.12" } }, @@ -50,9 +50,9 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -156,11 +156,11 @@ } }, "node_modules/electron-updater": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.8.tgz", - "integrity": "sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==", + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.9.tgz", + "integrity": "sha512-omoTwGSG+/H8G62cEZS/dc5Lmc4HohAd4198AP+JNv8H7bfxXUCKekaR6WpsN1n2DiWzvcqOusfGSogZv/uj9w==", "dependencies": { - "builder-util-runtime": "9.2.3", + "builder-util-runtime": "9.2.4", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", @@ -467,9 +467,9 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" }, "builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "requires": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -541,11 +541,11 @@ "integrity": "sha512-OEC/48ZBJxR6XNSZtCl4cKPyQ1lvsu8yp8GdCplMWwGS1eEyMcEmzML5BRs/io/RLDnpgyf+7rSL+X6ICifRIg==" }, "electron-updater": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.8.tgz", - "integrity": "sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==", + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.9.tgz", + "integrity": "sha512-omoTwGSG+/H8G62cEZS/dc5Lmc4HohAd4198AP+JNv8H7bfxXUCKekaR6WpsN1n2DiWzvcqOusfGSogZv/uj9w==", "requires": { - "builder-util-runtime": "9.2.3", + "builder-util-runtime": "9.2.4", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", diff --git a/desktop/package.json b/desktop/package.json index de5834b9ce7a..606fcac92500 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -7,7 +7,7 @@ "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", "electron-serve": "^1.3.0", - "electron-updater": "^6.1.8", + "electron-updater": "^6.1.9", "node-machine-id": "^1.1.12" }, "author": "Expensify, Inc.", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 712909df6b61..dde63923045f 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.49 + 1.4.50 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.49.4 + 1.4.50.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index b1a4aa336ab8..212d94afdbdb 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.49 + 1.4.50 CFBundleSignature ???? CFBundleVersion - 1.4.49.4 + 1.4.50.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index c90bc159062b..16e9bac88187 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.49 + 1.4.50 CFBundleVersion - 1.4.49.4 + 1.4.50.2 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c93dfba50f5a..bb26b37d4015 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1198,6 +1198,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 +1447,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) @@ -1546,6 +1552,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 +1598,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`) @@ -1751,6 +1759,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 +1851,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: @@ -1945,6 +1957,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 +2003,7 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 RNReanimated: 3850671fd0c67051ea8e1e648e8c3e86bf3a28eb RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 + RNShare: 859ff710211285676b0bcedd156c12437ea1d564 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 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 81686286bf1c..d009c9d3ad3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.49-4", + "version": "1.4.50-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.49-4", + "version": "1.4.50-2", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -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", @@ -44937,6 +44939,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", @@ -45006,6 +45035,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", diff --git a/package.json b/package.json index fc6e1e803e97..58e204126829 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.49-4", + "version": "1.4.50-2", "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,6 +50,8 @@ "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", + "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", @@ -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", 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..a903adb8b84b 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', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bb1766f40e1f..371bd94e6225 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', @@ -553,6 +556,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..1c33e2ab5bab 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -557,6 +557,10 @@ const ROUTES = { route: 'workspace/:policyID/categories/settings', getRoute: (policyID: string) => `workspace/${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, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8546f543b77a..8b653a8e22a1 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -227,6 +227,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/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/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/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}) => ( void; + setIsDraggingOver?: (value: boolean) => void; }; type SetOnDropHandlerCallback = (event: DragEvent) => void; 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/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 2520520fd467..93ff73bcfaf2 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -40,7 +40,7 @@ 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) { @@ -79,8 +79,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)]; diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 338796cd856e..3dd06b67b637 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -46,10 +46,10 @@ 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) { @@ -69,7 +69,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 +87,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)); } }; 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 && ( + +