diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 57c59c88f9e5..80f75137bb71 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -9,17 +9,12 @@ module.exports = /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const _ = __nccwpck_require__(2947); -const lodashGet = __nccwpck_require__(6908); const core = __nccwpck_require__(2186); const {context} = __nccwpck_require__(5438); const CONST = __nccwpck_require__(4097); const ActionUtils = __nccwpck_require__(970); const GithubUtils = __nccwpck_require__(7999); -const prList = ActionUtils.getJSONInput('PR_LIST', {required: true}); -const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: true}); -const version = core.getInput('DEPLOY_VERSION', {required: true}); - /** * Return a nicely formatted message for the table based on the result of the GitHub action job * @@ -40,34 +35,6 @@ function getDeployTableMessage(platformResult) { } } -const androidResult = getDeployTableMessage(core.getInput('ANDROID', {required: true})); -const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', {required: true})); -const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true})); -const webResult = getDeployTableMessage(core.getInput('WEB', {required: true})); - -const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; - -/** - * @param {String} deployer - * @param {String} deployVerb - * @param {String} prTitle - * @returns {String} - */ -function getDeployMessage(deployer, deployVerb, prTitle) { - let message = `πŸš€ [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`; - message += ` by https://github.com/${deployer} in version: ${version} πŸš€`; - message += `\n\n platform | result \n ---|--- \nπŸ€– android πŸ€–|${androidResult} \nπŸ–₯ desktop πŸ–₯|${desktopResult}`; - message += `\n🍎 iOS 🍎|${iOSResult} \nπŸ•Έ web πŸ•Έ|${webResult}`; - - if (deployVerb === 'Cherry-picked' && !/no qa/gi.test(prTitle)) { - // eslint-disable-next-line max-len - message += - '\n\n@Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.'; - } - - return message; -} - /** * Comment Single PR * @@ -75,87 +42,108 @@ function getDeployMessage(deployer, deployVerb, prTitle) { * @param {String} message * @returns {Promise} */ -function commentPR(PR, message) { - return GithubUtils.createComment(context.repo.repo, PR, message) - .then(() => console.log(`Comment created on #${PR} successfully πŸŽ‰`)) - .catch((err) => { - console.log(`Unable to write comment on #${PR} 😞`); - core.setFailed(err.message); - }); +async function commentPR(PR, message) { + try { + await GithubUtils.createComment(context.repo.repo, PR, message); + console.log(`Comment created on #${PR} successfully πŸŽ‰`); + } catch (err) { + console.log(`Unable to write comment on #${PR} 😞`); + core.setFailed(err.message); + } } -const run = function () { +const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; + +async function run() { + const prList = _.map(ActionUtils.getJSONInput('PR_LIST', {required: true}), (num) => Number.parseInt(num, 10)); + const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: true}); + const version = core.getInput('DEPLOY_VERSION', {required: true}); + + const androidResult = getDeployTableMessage(core.getInput('ANDROID', {required: true})); + const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', {required: true})); + const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true})); + const webResult = getDeployTableMessage(core.getInput('WEB', {required: true})); + + /** + * @param {String} deployer + * @param {String} deployVerb + * @param {String} prTitle + * @returns {String} + */ + function getDeployMessage(deployer, deployVerb, prTitle) { + let message = `πŸš€ [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`; + message += ` by https://github.com/${deployer} in version: ${version} πŸš€`; + message += `\n\nplatform | result\n---|---\nπŸ€– android πŸ€–|${androidResult}\nπŸ–₯ desktop πŸ–₯|${desktopResult}`; + message += `\n🍎 iOS 🍎|${iOSResult}\nπŸ•Έ web πŸ•Έ|${webResult}`; + + if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle)) { + // eslint-disable-next-line max-len + message += + '\n\n@Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.'; + } + + return message; + } + if (isProd) { - // First find the deployer (who closed the last deploy checklist)? - return GithubUtils.octokit.issues - .listForRepo({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - labels: CONST.LABELS.STAGING_DEPLOY, - state: 'closed', - }) - .then(({data}) => _.first(data).number) - .then((lastDeployChecklistNumber) => GithubUtils.getActorWhoClosedIssue(lastDeployChecklistNumber)) - .then((actor) => { - // Create comment on each pull request (one after another to avoid throttling issues) - const deployMessage = getDeployMessage(actor, 'Deployed'); - _.reduce(prList, (promise, pr) => promise.then(() => commentPR(pr, deployMessage)), Promise.resolve()); - }); + // Find the previous deploy checklist + const {data: deployChecklists} = await GithubUtils.octokit.issues.listForRepo({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + labels: CONST.LABELS.STAGING_DEPLOY, + state: 'closed', + }); + const previousChecklistID = _.first(deployChecklists).number; + + // who closed the last deploy checklist? + const deployer = await GithubUtils.getActorWhoClosedIssue(previousChecklistID); + + // Create comment on each pull request (one at a time to avoid throttling issues) + const deployMessage = getDeployMessage(deployer, 'Deployed'); + for (const pr of prList) { + await commentPR(pr, deployMessage); + } + return; } // First find out if this is a normal staging deploy or a CP by looking at the commit message on the tag - return GithubUtils.octokit.repos - .listTags({ + const {data: recentTags} = await GithubUtils.octokit.repos.listTags({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + per_page: 100, + }); + const currentTag = _.find(recentTags, (tag) => tag.name === version); + if (!currentTag) { + const err = `Could not find tag matching ${version}`; + console.error(err); + core.setFailed(err); + return; + } + const {data: commit} = await GithubUtils.octokit.git.getCommit({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + commit_sha: currentTag.commit.sha, + }); + const isCP = /[\S\s]*\(cherry picked from commit .*\)/.test(commit.message); + + for (const prNumber of prList) { + /* + * Determine who the deployer for the PR is. The "deployer" for staging deploys is: + * 1. For regular staging deploys, the person who merged the PR. + * 2. For CPs, the person who committed the cherry-picked commit (not necessarily the author of the commit). + */ + const {data: pr} = await GithubUtils.octokit.pulls.get({ owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, - per_page: 100, - }) - .then(({data}) => { - const tagSHA = _.find(data, (tag) => tag.name === version).commit.sha; - return GithubUtils.octokit.git.getCommit({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - commit_sha: tagSHA, - }); - }) - .then(({data}) => { - const isCP = /Merge pull request #\d+ from Expensify\/.*-?cherry-pick-staging-\d+/.test(data.message); - _.reduce( - prList, - (promise, PR) => - promise - - // Then, for each PR, find out who merged it and determine the deployer - .then(() => - GithubUtils.octokit.pulls.get({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - pull_number: PR, - }), - ) - .then((response) => { - /* - * The deployer for staging deploys is: - * 1. For regular staging deploys, the person who merged the PR. - * 2. For automatic CPs (using the label), the person who merged the PR. - * 3. For manual CPs (using the GH UI), the person who triggered the workflow - * (reflected in the branch name). - */ - let deployer = lodashGet(response, 'data.merged_by.login', ''); - const issueTitle = lodashGet(response, 'data.title', ''); - const CPActorMatches = data.message.match(/Merge pull request #\d+ from Expensify\/(.+)-cherry-pick-staging-\d+/); - if (_.isArray(CPActorMatches) && CPActorMatches.length === 2 && CPActorMatches[1] !== CONST.OS_BOTIFY) { - deployer = CPActorMatches[1]; - } - - // Finally, comment on the PR - const deployMessage = getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', issueTitle); - return commentPR(PR, deployMessage); - }), - Promise.resolve(), - ); + pull_number: prNumber, }); -}; + const deployer = isCP ? commit.committer.name : pr.merged_by.login; + + const title = pr.title; + const deployMessage = getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', title); + await commentPR(prNumber, deployMessage); + } +} if (require.main === require.cache[eval('__filename')]) { run(); @@ -8171,7 +8159,7 @@ var modules = [ __nccwpck_require__(9557), __nccwpck_require__(1155), __nccwpck_require__(1644), - __nccwpck_require__(373), + __nccwpck_require__(6657), __nccwpck_require__(1080), __nccwpck_require__(1012), __nccwpck_require__(9695), @@ -8395,7 +8383,7 @@ InternalDecoderCesu8.prototype.end = function() { /***/ }), -/***/ 373: +/***/ 6657: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -10455,7 +10443,7 @@ module.exports = Map; /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { var mapCacheClear = __nccwpck_require__(1610), - mapCacheDelete = __nccwpck_require__(6657), + mapCacheDelete = __nccwpck_require__(5991), mapCacheGet = __nccwpck_require__(1372), mapCacheHas = __nccwpck_require__(609), mapCacheSet = __nccwpck_require__(5582); @@ -11297,7 +11285,7 @@ module.exports = mapCacheClear; /***/ }), -/***/ 6657: +/***/ 5991: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { var getMapData = __nccwpck_require__(9980); diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js index 708d001892c2..d03a947cdec8 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js @@ -1,15 +1,10 @@ const _ = require('underscore'); -const lodashGet = require('lodash/get'); const core = require('@actions/core'); const {context} = require('@actions/github'); const CONST = require('../../../libs/CONST'); const ActionUtils = require('../../../libs/ActionUtils'); const GithubUtils = require('../../../libs/GithubUtils'); -const prList = ActionUtils.getJSONInput('PR_LIST', {required: true}); -const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: true}); -const version = core.getInput('DEPLOY_VERSION', {required: true}); - /** * Return a nicely formatted message for the table based on the result of the GitHub action job * @@ -30,34 +25,6 @@ function getDeployTableMessage(platformResult) { } } -const androidResult = getDeployTableMessage(core.getInput('ANDROID', {required: true})); -const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', {required: true})); -const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true})); -const webResult = getDeployTableMessage(core.getInput('WEB', {required: true})); - -const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; - -/** - * @param {String} deployer - * @param {String} deployVerb - * @param {String} prTitle - * @returns {String} - */ -function getDeployMessage(deployer, deployVerb, prTitle) { - let message = `πŸš€ [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`; - message += ` by https://github.com/${deployer} in version: ${version} πŸš€`; - message += `\n\n platform | result \n ---|--- \nπŸ€– android πŸ€–|${androidResult} \nπŸ–₯ desktop πŸ–₯|${desktopResult}`; - message += `\n🍎 iOS 🍎|${iOSResult} \nπŸ•Έ web πŸ•Έ|${webResult}`; - - if (deployVerb === 'Cherry-picked' && !/no qa/gi.test(prTitle)) { - // eslint-disable-next-line max-len - message += - '\n\n@Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.'; - } - - return message; -} - /** * Comment Single PR * @@ -65,87 +32,108 @@ function getDeployMessage(deployer, deployVerb, prTitle) { * @param {String} message * @returns {Promise} */ -function commentPR(PR, message) { - return GithubUtils.createComment(context.repo.repo, PR, message) - .then(() => console.log(`Comment created on #${PR} successfully πŸŽ‰`)) - .catch((err) => { - console.log(`Unable to write comment on #${PR} 😞`); - core.setFailed(err.message); - }); +async function commentPR(PR, message) { + try { + await GithubUtils.createComment(context.repo.repo, PR, message); + console.log(`Comment created on #${PR} successfully πŸŽ‰`); + } catch (err) { + console.log(`Unable to write comment on #${PR} 😞`); + core.setFailed(err.message); + } } -const run = function () { +const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; + +async function run() { + const prList = _.map(ActionUtils.getJSONInput('PR_LIST', {required: true}), (num) => Number.parseInt(num, 10)); + const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: true}); + const version = core.getInput('DEPLOY_VERSION', {required: true}); + + const androidResult = getDeployTableMessage(core.getInput('ANDROID', {required: true})); + const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', {required: true})); + const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true})); + const webResult = getDeployTableMessage(core.getInput('WEB', {required: true})); + + /** + * @param {String} deployer + * @param {String} deployVerb + * @param {String} prTitle + * @returns {String} + */ + function getDeployMessage(deployer, deployVerb, prTitle) { + let message = `πŸš€ [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`; + message += ` by https://github.com/${deployer} in version: ${version} πŸš€`; + message += `\n\nplatform | result\n---|---\nπŸ€– android πŸ€–|${androidResult}\nπŸ–₯ desktop πŸ–₯|${desktopResult}`; + message += `\n🍎 iOS 🍎|${iOSResult}\nπŸ•Έ web πŸ•Έ|${webResult}`; + + if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle)) { + // eslint-disable-next-line max-len + message += + '\n\n@Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.'; + } + + return message; + } + if (isProd) { - // First find the deployer (who closed the last deploy checklist)? - return GithubUtils.octokit.issues - .listForRepo({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - labels: CONST.LABELS.STAGING_DEPLOY, - state: 'closed', - }) - .then(({data}) => _.first(data).number) - .then((lastDeployChecklistNumber) => GithubUtils.getActorWhoClosedIssue(lastDeployChecklistNumber)) - .then((actor) => { - // Create comment on each pull request (one after another to avoid throttling issues) - const deployMessage = getDeployMessage(actor, 'Deployed'); - _.reduce(prList, (promise, pr) => promise.then(() => commentPR(pr, deployMessage)), Promise.resolve()); - }); + // Find the previous deploy checklist + const {data: deployChecklists} = await GithubUtils.octokit.issues.listForRepo({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + labels: CONST.LABELS.STAGING_DEPLOY, + state: 'closed', + }); + const previousChecklistID = _.first(deployChecklists).number; + + // who closed the last deploy checklist? + const deployer = await GithubUtils.getActorWhoClosedIssue(previousChecklistID); + + // Create comment on each pull request (one at a time to avoid throttling issues) + const deployMessage = getDeployMessage(deployer, 'Deployed'); + for (const pr of prList) { + await commentPR(pr, deployMessage); + } + return; } // First find out if this is a normal staging deploy or a CP by looking at the commit message on the tag - return GithubUtils.octokit.repos - .listTags({ + const {data: recentTags} = await GithubUtils.octokit.repos.listTags({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + per_page: 100, + }); + const currentTag = _.find(recentTags, (tag) => tag.name === version); + if (!currentTag) { + const err = `Could not find tag matching ${version}`; + console.error(err); + core.setFailed(err); + return; + } + const {data: commit} = await GithubUtils.octokit.git.getCommit({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + commit_sha: currentTag.commit.sha, + }); + const isCP = /[\S\s]*\(cherry picked from commit .*\)/.test(commit.message); + + for (const prNumber of prList) { + /* + * Determine who the deployer for the PR is. The "deployer" for staging deploys is: + * 1. For regular staging deploys, the person who merged the PR. + * 2. For CPs, the person who committed the cherry-picked commit (not necessarily the author of the commit). + */ + const {data: pr} = await GithubUtils.octokit.pulls.get({ owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, - per_page: 100, - }) - .then(({data}) => { - const tagSHA = _.find(data, (tag) => tag.name === version).commit.sha; - return GithubUtils.octokit.git.getCommit({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - commit_sha: tagSHA, - }); - }) - .then(({data}) => { - const isCP = /Merge pull request #\d+ from Expensify\/.*-?cherry-pick-staging-\d+/.test(data.message); - _.reduce( - prList, - (promise, PR) => - promise - - // Then, for each PR, find out who merged it and determine the deployer - .then(() => - GithubUtils.octokit.pulls.get({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - pull_number: PR, - }), - ) - .then((response) => { - /* - * The deployer for staging deploys is: - * 1. For regular staging deploys, the person who merged the PR. - * 2. For automatic CPs (using the label), the person who merged the PR. - * 3. For manual CPs (using the GH UI), the person who triggered the workflow - * (reflected in the branch name). - */ - let deployer = lodashGet(response, 'data.merged_by.login', ''); - const issueTitle = lodashGet(response, 'data.title', ''); - const CPActorMatches = data.message.match(/Merge pull request #\d+ from Expensify\/(.+)-cherry-pick-staging-\d+/); - if (_.isArray(CPActorMatches) && CPActorMatches.length === 2 && CPActorMatches[1] !== CONST.OS_BOTIFY) { - deployer = CPActorMatches[1]; - } - - // Finally, comment on the PR - const deployMessage = getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', issueTitle); - return commentPR(PR, deployMessage); - }), - Promise.resolve(), - ); + pull_number: prNumber, }); -}; + const deployer = isCP ? commit.committer.name : pr.merged_by.login; + + const title = pr.title; + const deployMessage = getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', title); + await commentPR(prNumber, deployMessage); + } +} if (require.main === module) { run(); diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index d4c17a734b1c..494326869cca 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -77,6 +77,7 @@ jobs: id: cherryPick run: | echo "Attempting to cherry-pick ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}" + git config user.name ${{ github.actor }} if git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}; then echo "πŸŽ‰ No conflicts! CP was a success, PR can be automerged πŸŽ‰" echo "HAS_CONFLICTS=false" >> "$GITHUB_OUTPUT" @@ -87,6 +88,7 @@ jobs: GIT_MERGE_AUTOEDIT=no git cherry-pick --continue echo "HAS_CONFLICTS=true" >> "$GITHUB_OUTPUT" fi + git config user.name OSBotify - name: Push changes run: | diff --git a/android/app/build.gradle b/android/app/build.gradle index ed29eaa5e241..51c1cba7e621 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001036101 - versionName "1.3.61-1" + versionCode 1001036102 + versionName "1.3.61-2" } flavorDimensions "default" diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 42db4f642943..ae2b98ece85b 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -81,9 +81,9 @@ A job could be fixing a bug or working on a new feature. There are two ways you This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view all open jobs in the Expensify/App GH repository by searching for GH issues with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). Lastly, you can follow the [@ExpensifyOSS](https://twitter.com/ExpensifyOSS) Twitter account to see a live feed of jobs that are posted. #### Raising jobs and bugs -It’s possible that you found a new bug that we haven’t posted as a job to the [GitHub repository](https://github.com/Expensify/App/issues?q=is%3Aissue). This is an opportunity to raise it and claim the bug bounty. If it's a valid bug that we choose to resolve by deploying it to production β€” either internally or via an external contributor β€” then we will compensate you $250 for identifying the bug (we do not compensate for reporting new feature requests). If the bug is fixed by a PR that is not associated with your bug report, then you will not be eligible for the corresponding compensation unless you can find the PR that fixed it and prove your bug report came first. -- Note: If you get assigned the job you proposed **and** you complete the job, this $250 for identifying the improvement is *in addition to* the reward you will be paid for completing the job. -- Note about proposed bugs: Expensify has the right not to pay the $250 reward if the suggested bug has already been reported. Following, if more than one contributor proposes the same bug, the contributor who posted it first in the [#expensify-bugs](https://expensify.slack.com/archives/C049HHMV9SM) Slack channel is the one who is eligible for the bonus. +It’s possible that you found a new bug that we haven’t posted as a job to the [GitHub repository](https://github.com/Expensify/App/issues?q=is%3Aissue). This is an opportunity to raise it and claim the bug bounty. If it's a valid bug that we choose to resolve by deploying it to production β€” either internally or via an external contributor β€” then we will compensate you $50 for identifying the bug (we do not compensate for reporting new feature requests). If the bug is fixed by a PR that is not associated with your bug report, then you will not be eligible for the corresponding compensation unless you can find the PR that fixed it and prove your bug report came first. +- Note: If you get assigned the job you proposed **and** you complete the job, this $50 for identifying the improvement is *in addition to* the reward you will be paid for completing the job. +- Note about proposed bugs: Expensify has the right not to pay the $50 reward if the suggested bug has already been reported. Following, if more than one contributor proposes the same bug, the contributor who posted it first in the [#expensify-bugs](https://expensify.slack.com/archives/C049HHMV9SM) Slack channel is the one who is eligible for the bonus. - Note: whilst you may optionally propose a solution for that job on Slack, solutions are ultimately reviewed in GitHub. The onus is on you to propose the solution on GitHub, and/or ensure the issue creator will include a link to your proposal. Please follow these steps to propose a job or raise a bug: diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index d64dfea72b33..7d3288fdbaee 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.61.1 + 1.3.61.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 475e4f02046a..c1f9b921841c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.3.61.1 + 1.3.61.2 diff --git a/package-lock.json b/package-lock.json index 8df79230d1e0..8d87c8c87c93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.61-1", + "version": "1.3.61-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.61-1", + "version": "1.3.61-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 82cc96efd843..d1e0907e9317 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.61-1", + "version": "1.3.61-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.", diff --git a/patches/@react-navigation+native+6.1.6.patch b/patches/@react-navigation+native+6.1.6.patch index 61e5eb9892e1..eb933683c850 100644 --- a/patches/@react-navigation+native+6.1.6.patch +++ b/patches/@react-navigation+native+6.1.6.patch @@ -133,7 +133,7 @@ index 0000000..16da117 +//# sourceMappingURL=findFocusedRouteKey.js.map \ No newline at end of file diff --git a/node_modules/@react-navigation/native/lib/module/useLinking.js b/node_modules/@react-navigation/native/lib/module/useLinking.js -index 5bf2a88..a6d0670 100644 +index 5bf2a88..a4318ef 100644 --- a/node_modules/@react-navigation/native/lib/module/useLinking.js +++ b/node_modules/@react-navigation/native/lib/module/useLinking.js @@ -2,6 +2,7 @@ import { findFocusedRoute, getActionFromState as getActionFromStateDefault, getP @@ -144,7 +144,37 @@ index 5bf2a88..a6d0670 100644 import ServerContext from './ServerContext'; /** * Find the matching navigation state that changed between 2 navigation states -@@ -60,6 +61,44 @@ const series = cb => { +@@ -34,32 +35,52 @@ const findMatchingState = (a, b) => { + /** + * Run async function in series as it's called. + */ +-const series = cb => { +- // Whether we're currently handling a callback +- let handling = false; +- let queue = []; +- const callback = async () => { +- try { +- if (handling) { +- // If we're currently handling a previous event, wait before handling this one +- // Add the callback to the beginning of the queue +- queue.unshift(callback); +- return; +- } +- handling = true; +- await cb(); +- } finally { +- handling = false; +- if (queue.length) { +- // If we have queued items, handle the last one +- const last = queue.pop(); +- last === null || last === void 0 ? void 0 : last(); +- } +- } ++const series = (cb) => { ++ let queue = Promise.resolve(); ++ const callback = () => { ++ queue = queue.then(cb); + }; return callback; }; let linkingHandlers = []; @@ -189,7 +219,7 @@ index 5bf2a88..a6d0670 100644 export default function useLinking(ref, _ref) { let { independent, -@@ -251,6 +290,9 @@ export default function useLinking(ref, _ref) { +@@ -251,6 +272,9 @@ export default function useLinking(ref, _ref) { // Otherwise it's likely a change triggered by `popstate` path !== pendingPath) { const historyDelta = (focusedState.history ? focusedState.history.length : focusedState.routes.length) - (previousFocusedState.history ? previousFocusedState.history.length : previousFocusedState.routes.length); @@ -199,7 +229,7 @@ index 5bf2a88..a6d0670 100644 if (historyDelta > 0) { // If history length is increased, we should pushState // Note that path might not actually change here, for example, drawer open should pushState -@@ -262,34 +304,55 @@ export default function useLinking(ref, _ref) { +@@ -262,34 +286,55 @@ export default function useLinking(ref, _ref) { // If history length is decreased, i.e. entries were removed, we want to go back const nextIndex = history.backIndex({ diff --git a/src/CONST.ts b/src/CONST.ts index c4383a45b0b4..9735e6deb0ad 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1370,6 +1370,9 @@ const CONST = { MAKE_REQUEST_WITH_SIDE_EFFECTS: 'makeRequestWithSideEffects', }, + MAP_PADDING: 50, + MAP_MARKER_SIZE: 20, + QUICK_REACTIONS: [ { name: '+1', diff --git a/src/components/ConfirmedRoute.js b/src/components/ConfirmedRoute.js new file mode 100644 index 000000000000..07d0fdc6ca97 --- /dev/null +++ b/src/components/ConfirmedRoute.js @@ -0,0 +1,118 @@ +import React, {useEffect} from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import MapView from 'react-native-x-maps'; +import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; +import _ from 'underscore'; +import ONYXKEYS from '../ONYXKEYS'; +import CONST from '../CONST'; +import * as MapboxToken from '../libs/actions/MapboxToken'; +import * as Expensicons from './Icon/Expensicons'; +import theme from '../styles/themes/default'; +import styles from '../styles/styles'; +import transactionPropTypes from './transactionPropTypes'; +import BlockingView from './BlockingViews/BlockingView'; +import useNetwork from '../hooks/useNetwork'; +import useLocalize from '../hooks/useLocalize'; + +const propTypes = { + /** Transaction that stores the distance request data */ + transaction: transactionPropTypes, + + /** Token needed to render the map */ + mapboxToken: PropTypes.string, +}; + +const defaultProps = { + transaction: {}, + mapboxToken: '', +}; + +const getWaypointMarkers = (waypoints) => { + const numberOfWaypoints = _.size(waypoints); + const lastWaypointIndex = numberOfWaypoints - 1; + return _.filter( + _.map(waypoints, (waypoint, key) => { + if (!waypoint || waypoint.lng === undefined || waypoint.lat === undefined) { + return; + } + + const index = Number(key.replace('waypoint', '')); + let MarkerComponent; + if (index === 0) { + MarkerComponent = Expensicons.DotIndicatorUnfilled; + } else if (index === lastWaypointIndex) { + MarkerComponent = Expensicons.Location; + } else { + MarkerComponent = Expensicons.DotIndicator; + } + + return { + coordinate: [waypoint.lng, waypoint.lat], + markerComponent: () => ( + + ), + }; + }), + (waypoint) => waypoint, + ); +}; + +function ConfirmedRoute({mapboxToken, transaction}) { + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + const {route0: route} = transaction.routes || {}; + const waypoints = lodashGet(transaction, 'comment.waypoints', {}); + const coordinates = lodashGet(route, 'geometry.coordinates', []); + const waypointMarkers = getWaypointMarkers(waypoints); + + useEffect(() => { + MapboxToken.init(); + return MapboxToken.stop; + }, []); + + return ( + <> + {!isOffline && mapboxToken ? ( + + ) : ( + + + + )} + + ); +} + +export default withOnyx({ + transaction: { + key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + }, + mapboxToken: { + key: ONYXKEYS.MAPBOX_ACCESS_TOKEN, + selector: (mapboxAccessToken) => mapboxAccessToken && mapboxAccessToken.token, + }, +})(ConfirmedRoute); + +ConfirmedRoute.displayName = 'ConfirmedRoute'; +ConfirmedRoute.propTypes = propTypes; +ConfirmedRoute.defaultProps = defaultProps; diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js index 146b023bbf0c..126cea7e842e 100644 --- a/src/components/CountryPicker/CountrySelectorModal.js +++ b/src/components/CountryPicker/CountrySelectorModal.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import React, {useMemo} from 'react'; +import React, {useMemo, useEffect} from 'react'; import PropTypes from 'prop-types'; import CONST from '../../CONST'; import useLocalize from '../../hooks/useLocalize'; @@ -40,6 +40,13 @@ const defaultProps = { function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySelected, setSearchValue, searchValue}) { const {translate} = useLocalize(); + useEffect(() => { + if (isVisible) { + return; + } + setSearchValue(''); + }, [isVisible, setSearchValue]); + const countries = useMemo( () => _.map(translate('allCountries'), (countryName, countryISO) => ({ @@ -76,7 +83,6 @@ function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySele { - setSearchValue(lodashGet(allCountries, value, '')); - }, [value, allCountries]); + const [searchValue, setSearchValue] = useState(''); const showPickerModal = () => { - setSearchValue(lodashGet(allCountries, value, '')); setIsPickerVisible(true); }; diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js index 0dd74e4225d8..f5a0b1a50143 100644 --- a/src/components/DistanceRequest.js +++ b/src/components/DistanceRequest.js @@ -38,8 +38,14 @@ const MAX_WAYPOINTS = 25; const MAX_WAYPOINTS_TO_DISPLAY = 4; const propTypes = { - /** The transactionID of this request */ - transactionID: PropTypes.string, + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + iou: iouPropTypes, + + /** Type of money request (i.e. IOU) */ + iouType: PropTypes.oneOf(_.values(CONST.IOU.MONEY_REQUEST_TYPE)), + + /** The report to which the distance request is associated */ + report: reportPropTypes, /** The optimistic transaction for this request */ transaction: transactionPropTypes, @@ -55,17 +61,21 @@ const propTypes = { }; const defaultProps = { - transactionID: '', + iou: {}, + iouType: '', + report: {}, transaction: {}, mapboxAccessToken: {}, }; -function DistanceRequest({transactionID, transaction, mapboxAccessToken}) { +function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken}) { const [shouldShowGradient, setShouldShowGradient] = useState(false); const [scrollContainerHeight, setScrollContainerHeight] = useState(0); const [scrollContentHeight, setScrollContentHeight] = useState(0); const {isOffline} = useNetwork(); const {translate} = useLocalize(); + + const reportID = lodashGet(report, 'reportID', ''); const waypoints = useMemo(() => lodashGet(transaction, 'comment.waypoints', {}), [transaction]); const numberOfWaypoints = _.size(waypoints); @@ -100,8 +110,8 @@ function DistanceRequest({transactionID, transaction, mapboxAccessToken}) { coordinate: [waypoint.lng, waypoint.lat], markerComponent: () => ( ), @@ -122,12 +132,12 @@ function DistanceRequest({transactionID, transaction, mapboxAccessToken}) { }, []); useEffect(() => { - if (!transactionID || !_.isEmpty(waypoints)) { + if (!iou.transactionID || !_.isEmpty(waypoints)) { return; } // Create the initial start and stop waypoints - Transaction.createInitialWaypoints(transactionID); - }, [transactionID, waypoints]); + Transaction.createInitialWaypoints(iou.transactionID); + }, [iou.transactionID, waypoints]); const updateGradientVisibility = (event = {}) => { // If a waypoint extends past the bottom of the visible area show the gradient, else hide it. @@ -140,8 +150,8 @@ function DistanceRequest({transactionID, transaction, mapboxAccessToken}) { return; } - Transaction.getRoute(transactionID, waypoints); - }, [shouldFetchRoute, transactionID, waypoints, isOffline]); + Transaction.getRoute(iou.transactionID, waypoints); + }, [shouldFetchRoute, iou.transactionID, waypoints, isOffline]); useEffect(updateGradientVisibility, [scrollContainerHeight, scrollContentHeight]); @@ -204,7 +214,7 @@ function DistanceRequest({transactionID, transaction, mapboxAccessToken}) {