diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index 1683894c67e4..d17760baa91f 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -104,8 +104,20 @@ exports.updateiOSVersion = function updateiOSVersion(version) { /***/ }), /***/ 8007: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { +/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { +"use strict"; +__nccwpck_require__.r(__webpack_exports__); +/* harmony export */ __nccwpck_require__.d(__webpack_exports__, { +/* harmony export */ "MAX_INCREMENTS": () => (/* binding */ MAX_INCREMENTS), +/* harmony export */ "SEMANTIC_VERSION_LEVELS": () => (/* binding */ SEMANTIC_VERSION_LEVELS), +/* harmony export */ "getPreviousVersion": () => (/* binding */ getPreviousVersion), +/* harmony export */ "getVersionNumberFromString": () => (/* binding */ getVersionNumberFromString), +/* harmony export */ "getVersionStringFromNumber": () => (/* binding */ getVersionStringFromNumber), +/* harmony export */ "incrementMinor": () => (/* binding */ incrementMinor), +/* harmony export */ "incrementPatch": () => (/* binding */ incrementPatch), +/* harmony export */ "incrementVersion": () => (/* binding */ incrementVersion) +/* harmony export */ }); const _ = __nccwpck_require__(5067); const SEMANTIC_VERSION_LEVELS = { @@ -235,18 +247,7 @@ function getPreviousVersion(currentVersion, level) { return getVersionStringFromNumber(major, minor, patch, build - 1); } -module.exports = { - getVersionNumberFromString, - getVersionStringFromNumber, - incrementVersion, - - // For tests - MAX_INCREMENTS, - SEMANTIC_VERSION_LEVELS, - incrementMinor, - incrementPatch, - getPreviousVersion, -}; + /***/ }), @@ -5954,6 +5955,34 @@ module.exports = underscoreNodeF._; /******/ } /******/ /************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __nccwpck_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __nccwpck_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ /******/ /* webpack/runtime/compat */ /******/ /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js index f0e45257bbef..4441348a3c36 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js @@ -4,7 +4,7 @@ const _ = require('underscore'); const core = require('@actions/core'); const CONST = require('../../../libs/CONST'); const GithubUtils = require('../../../libs/GithubUtils'); -const GitUtils = require('../../../libs/GitUtils'); +const GitUtils = require('../../../libs/GitUtils').default; async function run() { // Note: require('package.json').version does not work because ncc will resolve that to a plain string at compile time diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 61411d351142..154dacbdc3c3 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -13,7 +13,7 @@ const _ = __nccwpck_require__(5067); const core = __nccwpck_require__(2186); const CONST = __nccwpck_require__(4097); const GithubUtils = __nccwpck_require__(7999); -const GitUtils = __nccwpck_require__(669); +const GitUtils = (__nccwpck_require__(1547)["default"]); async function run() { // Note: require('package.json').version does not work because ncc will resolve that to a plain string at compile time @@ -177,163 +177,6 @@ CONST.APP_REPO_GIT_URL = `git@github.com:${CONST.GITHUB_OWNER}/${CONST.APP_REPO} module.exports = CONST; -/***/ }), - -/***/ 669: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -const _ = __nccwpck_require__(5067); -const {spawn, execSync} = __nccwpck_require__(2081); -const CONST = __nccwpck_require__(4097); -const sanitizeStringForJSONParse = __nccwpck_require__(9338); -const {getPreviousVersion, SEMANTIC_VERSION_LEVELS} = __nccwpck_require__(8007); - -/** - * @param {String} tag - * @param {String} [shallowExcludeTag] when fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) - */ -function fetchTag(tag, shallowExcludeTag = '') { - let shouldRetry = true; - let needsRepack = false; - while (shouldRetry) { - try { - let command = ''; - if (needsRepack) { - // We have seen some scenarios where this fixes the git fetch. - // Why? Who knows... https://github.com/Expensify/App/pull/31459 - command = 'git repack -d'; - console.log(`Running command: ${command}`); - execSync(command); - } - - command = `git fetch origin tag ${tag} --no-tags`; - - // Note that this condition is only ever NOT true in the 1.0.0-0 edge case - if (shallowExcludeTag && shallowExcludeTag !== tag) { - command += ` --shallow-exclude=${shallowExcludeTag}`; - } - - console.log(`Running command: ${command}`); - execSync(command); - shouldRetry = false; - } catch (e) { - console.error(e); - if (!needsRepack) { - console.log('Attempting to repack and retry...'); - needsRepack = true; - } else { - console.error("Repack didn't help, giving up..."); - shouldRetry = false; - } - } - } -} - -/** - * Get merge logs between two tags (inclusive) as a JavaScript object. - * - * @param {String} fromTag - * @param {String} toTag - * @returns {Promise>>} - */ -function getCommitHistoryAsJSON(fromTag, toTag) { - // Fetch tags, exclude commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history - const previousPatchVersion = getPreviousVersion(fromTag, SEMANTIC_VERSION_LEVELS.PATCH); - fetchTag(fromTag, previousPatchVersion); - fetchTag(toTag, previousPatchVersion); - - console.log('Getting pull requests merged between the following tags:', fromTag, toTag); - return new Promise((resolve, reject) => { - let stdout = ''; - let stderr = ''; - const args = ['log', '--format={"commit": "%H", "authorName": "%an", "subject": "%s"},', `${fromTag}...${toTag}`]; - console.log(`Running command: git ${args.join(' ')}`); - const spawnedProcess = spawn('git', args); - spawnedProcess.on('message', console.log); - spawnedProcess.stdout.on('data', (chunk) => { - console.log(chunk.toString()); - stdout += chunk.toString(); - }); - spawnedProcess.stderr.on('data', (chunk) => { - console.error(chunk.toString()); - stderr += chunk.toString(); - }); - spawnedProcess.on('close', (code) => { - if (code !== 0) { - return reject(new Error(`${stderr}`)); - } - - resolve(stdout); - }); - spawnedProcess.on('error', (err) => reject(err)); - }).then((stdout) => { - // Sanitize just the text within commit subjects as that's the only potentially un-parseable text. - const sanitizedOutput = stdout.replace(/(?<="subject": ").*?(?="})/g, (subject) => sanitizeStringForJSONParse(subject)); - - // Then remove newlines, format as JSON and convert to a proper JS object - const json = `[${sanitizedOutput}]`.replace(/(\r\n|\n|\r)/gm, '').replace('},]', '}]'); - - return JSON.parse(json); - }); -} - -/** - * Parse merged PRs, excluding those from irrelevant branches. - * - * @param {Array>} commits - * @returns {Array} - */ -function getValidMergedPRs(commits) { - const mergedPRs = new Set(); - _.each(commits, (commit) => { - const author = commit.authorName; - if (author === CONST.OS_BOTIFY) { - return; - } - - const match = commit.subject.match(/Merge pull request #(\d+) from (?!Expensify\/.*-cherry-pick-staging)/); - if (!_.isArray(match) || match.length < 2) { - return; - } - - const pr = Number.parseInt(match[1], 10); - if (mergedPRs.has(pr)) { - // If a PR shows up in the log twice, that means that the PR was deployed in the previous checklist. - // That also means that we don't want to include it in the current checklist, so we remove it now. - mergedPRs.delete(pr); - return; - } - - mergedPRs.add(pr); - }); - - return Array.from(mergedPRs); -} - -/** - * Takes in two git tags and returns a list of PR numbers of all PRs merged between those two tags - * - * @param {String} fromTag - * @param {String} toTag - * @returns {Promise>} – Pull request numbers - */ -async function getPullRequestsMergedBetween(fromTag, toTag) { - console.log(`Looking for commits made between ${fromTag} and ${toTag}...`); - const commitList = await getCommitHistoryAsJSON(fromTag, toTag); - console.log(`Commits made between ${fromTag} and ${toTag}:`, commitList); - - // Find which commit messages correspond to merged PR's - const pullRequestNumbers = getValidMergedPRs(commitList).sort((a, b) => a - b); - console.log(`List of pull requests merged between ${fromTag} and ${toTag}`, pullRequestNumbers); - return pullRequestNumbers; -} - -module.exports = { - getValidMergedPRs, - getPullRequestsMergedBetween, -}; - - /***/ }), /***/ 7999: @@ -917,8 +760,20 @@ module.exports = function (inputString) { /***/ }), /***/ 8007: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { +/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { +"use strict"; +__nccwpck_require__.r(__webpack_exports__); +/* harmony export */ __nccwpck_require__.d(__webpack_exports__, { +/* harmony export */ "MAX_INCREMENTS": () => (/* binding */ MAX_INCREMENTS), +/* harmony export */ "SEMANTIC_VERSION_LEVELS": () => (/* binding */ SEMANTIC_VERSION_LEVELS), +/* harmony export */ "getPreviousVersion": () => (/* binding */ getPreviousVersion), +/* harmony export */ "getVersionNumberFromString": () => (/* binding */ getVersionNumberFromString), +/* harmony export */ "getVersionStringFromNumber": () => (/* binding */ getVersionStringFromNumber), +/* harmony export */ "incrementMinor": () => (/* binding */ incrementMinor), +/* harmony export */ "incrementPatch": () => (/* binding */ incrementPatch), +/* harmony export */ "incrementVersion": () => (/* binding */ incrementVersion) +/* harmony export */ }); const _ = __nccwpck_require__(5067); const SEMANTIC_VERSION_LEVELS = { @@ -1048,18 +903,7 @@ function getPreviousVersion(currentVersion, level) { return getVersionStringFromNumber(major, minor, patch, build - 1); } -module.exports = { - getVersionNumberFromString, - getVersionStringFromNumber, - incrementVersion, - - // For tests - MAX_INCREMENTS, - SEMANTIC_VERSION_LEVELS, - incrementMinor, - incrementPatch, - getPreviousVersion, -}; + /***/ }), @@ -16844,6 +16688,164 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 1547: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const child_process_1 = __nccwpck_require__(2081); +const CONST = __importStar(__nccwpck_require__(4097)); +const sanitizeStringForJSONParse_1 = __importDefault(__nccwpck_require__(9338)); +const VERSION_UPDATER = __importStar(__nccwpck_require__(8007)); +/** + * @param [shallowExcludeTag] When fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) + */ +function fetchTag(tag, shallowExcludeTag = '') { + let shouldRetry = true; + let needsRepack = false; + while (shouldRetry) { + try { + let command = ''; + if (needsRepack) { + // We have seen some scenarios where this fixes the git fetch. + // Why? Who knows... https://github.com/Expensify/App/pull/31459 + command = 'git repack -d'; + console.log(`Running command: ${command}`); + (0, child_process_1.execSync)(command); + } + command = `git fetch origin tag ${tag} --no-tags`; + // Note that this condition is only ever NOT true in the 1.0.0-0 edge case + if (shallowExcludeTag && shallowExcludeTag !== tag) { + command += ` --shallow-exclude=${shallowExcludeTag}`; + } + console.log(`Running command: ${command}`); + (0, child_process_1.execSync)(command); + shouldRetry = false; + } + catch (e) { + console.error(e); + if (!needsRepack) { + console.log('Attempting to repack and retry...'); + needsRepack = true; + } + else { + console.error("Repack didn't help, giving up..."); + shouldRetry = false; + } + } + } +} +/** + * Get merge logs between two tags (inclusive) as a JavaScript object. + */ +function getCommitHistoryAsJSON(fromTag, toTag) { + // Fetch tags, exclude commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history + const previousPatchVersion = VERSION_UPDATER.getPreviousVersion(fromTag, VERSION_UPDATER.SEMANTIC_VERSION_LEVELS.PATCH); + fetchTag(fromTag, previousPatchVersion); + fetchTag(toTag, previousPatchVersion); + console.log('Getting pull requests merged between the following tags:', fromTag, toTag); + return new Promise((resolve, reject) => { + let stdout = ''; + let stderr = ''; + const args = ['log', '--format={"commit": "%H", "authorName": "%an", "subject": "%s"},', `${fromTag}...${toTag}`]; + console.log(`Running command: git ${args.join(' ')}`); + const spawnedProcess = (0, child_process_1.spawn)('git', args); + spawnedProcess.on('message', console.log); + spawnedProcess.stdout.on('data', (chunk) => { + console.log(chunk.toString()); + stdout += chunk.toString(); + }); + spawnedProcess.stderr.on('data', (chunk) => { + console.error(chunk.toString()); + stderr += chunk.toString(); + }); + spawnedProcess.on('close', (code) => { + if (code !== 0) { + return reject(new Error(`${stderr}`)); + } + resolve(stdout); + }); + spawnedProcess.on('error', (err) => reject(err)); + }).then((stdout) => { + // Sanitize just the text within commit subjects as that's the only potentially un-parseable text. + const sanitizedOutput = stdout.replace(/(?<="subject": ").*?(?="})/g, (subject) => (0, sanitizeStringForJSONParse_1.default)(subject)); + // Then remove newlines, format as JSON and convert to a proper JS object + const json = `[${sanitizedOutput}]`.replace(/(\r\n|\n|\r)/gm, '').replace('},]', '}]'); + return JSON.parse(json); + }); +} +/** + * Parse merged PRs, excluding those from irrelevant branches. + */ +function getValidMergedPRs(commits) { + const mergedPRs = new Set(); + commits.forEach((commit) => { + const author = commit.authorName; + if (author === CONST.OS_BOTIFY) { + return; + } + const match = commit.subject.match(/Merge pull request #(\d+) from (?!Expensify\/.*-cherry-pick-staging)/); + if (!Array.isArray(match) || match.length < 2) { + return; + } + const pr = Number.parseInt(match[1], 10); + if (mergedPRs.has(pr)) { + // If a PR shows up in the log twice, that means that the PR was deployed in the previous checklist. + // That also means that we don't want to include it in the current checklist, so we remove it now. + mergedPRs.delete(pr); + return; + } + mergedPRs.add(pr); + }); + return Array.from(mergedPRs); +} +/** + * Takes in two git tags and returns a list of PR numbers of all PRs merged between those two tags + */ +async function getPullRequestsMergedBetween(fromTag, toTag) { + console.log(`Looking for commits made between ${fromTag} and ${toTag}...`); + const commitList = await getCommitHistoryAsJSON(fromTag, toTag); + console.log(`Commits made between ${fromTag} and ${toTag}:`, commitList); + // Find which commit messages correspond to merged PR's + const pullRequestNumbers = getValidMergedPRs(commitList).sort((a, b) => a - b); + console.log(`List of pull requests merged between ${fromTag} and ${toTag}`, pullRequestNumbers); + return pullRequestNumbers; +} +exports["default"] = { + getValidMergedPRs, + getPullRequestsMergedBetween, +}; + + /***/ }), /***/ 2877: @@ -19234,6 +19236,34 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ } /******/ /************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __nccwpck_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __nccwpck_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ /******/ /* webpack/runtime/compat */ /******/ /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; diff --git a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.js b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.js index a64ebfc240ba..da0dde4ff1ca 100644 --- a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.js +++ b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.js @@ -2,7 +2,7 @@ const _ = require('underscore'); const core = require('@actions/core'); const github = require('@actions/github'); const ActionUtils = require('../../../libs/ActionUtils'); -const GitUtils = require('../../../libs/GitUtils'); +const GitUtils = require('../../../libs/GitUtils').default; const GithubUtils = require('../../../libs/GithubUtils'); async function run() { diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index a20ff8d94dc4..f272929d536a 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -11,7 +11,7 @@ const _ = __nccwpck_require__(5067); const core = __nccwpck_require__(2186); const github = __nccwpck_require__(5438); const ActionUtils = __nccwpck_require__(970); -const GitUtils = __nccwpck_require__(669); +const GitUtils = (__nccwpck_require__(1547)["default"]); const GithubUtils = __nccwpck_require__(7999); async function run() { @@ -120,163 +120,6 @@ CONST.APP_REPO_GIT_URL = `git@github.com:${CONST.GITHUB_OWNER}/${CONST.APP_REPO} module.exports = CONST; -/***/ }), - -/***/ 669: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -const _ = __nccwpck_require__(5067); -const {spawn, execSync} = __nccwpck_require__(2081); -const CONST = __nccwpck_require__(4097); -const sanitizeStringForJSONParse = __nccwpck_require__(9338); -const {getPreviousVersion, SEMANTIC_VERSION_LEVELS} = __nccwpck_require__(8007); - -/** - * @param {String} tag - * @param {String} [shallowExcludeTag] when fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) - */ -function fetchTag(tag, shallowExcludeTag = '') { - let shouldRetry = true; - let needsRepack = false; - while (shouldRetry) { - try { - let command = ''; - if (needsRepack) { - // We have seen some scenarios where this fixes the git fetch. - // Why? Who knows... https://github.com/Expensify/App/pull/31459 - command = 'git repack -d'; - console.log(`Running command: ${command}`); - execSync(command); - } - - command = `git fetch origin tag ${tag} --no-tags`; - - // Note that this condition is only ever NOT true in the 1.0.0-0 edge case - if (shallowExcludeTag && shallowExcludeTag !== tag) { - command += ` --shallow-exclude=${shallowExcludeTag}`; - } - - console.log(`Running command: ${command}`); - execSync(command); - shouldRetry = false; - } catch (e) { - console.error(e); - if (!needsRepack) { - console.log('Attempting to repack and retry...'); - needsRepack = true; - } else { - console.error("Repack didn't help, giving up..."); - shouldRetry = false; - } - } - } -} - -/** - * Get merge logs between two tags (inclusive) as a JavaScript object. - * - * @param {String} fromTag - * @param {String} toTag - * @returns {Promise>>} - */ -function getCommitHistoryAsJSON(fromTag, toTag) { - // Fetch tags, exclude commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history - const previousPatchVersion = getPreviousVersion(fromTag, SEMANTIC_VERSION_LEVELS.PATCH); - fetchTag(fromTag, previousPatchVersion); - fetchTag(toTag, previousPatchVersion); - - console.log('Getting pull requests merged between the following tags:', fromTag, toTag); - return new Promise((resolve, reject) => { - let stdout = ''; - let stderr = ''; - const args = ['log', '--format={"commit": "%H", "authorName": "%an", "subject": "%s"},', `${fromTag}...${toTag}`]; - console.log(`Running command: git ${args.join(' ')}`); - const spawnedProcess = spawn('git', args); - spawnedProcess.on('message', console.log); - spawnedProcess.stdout.on('data', (chunk) => { - console.log(chunk.toString()); - stdout += chunk.toString(); - }); - spawnedProcess.stderr.on('data', (chunk) => { - console.error(chunk.toString()); - stderr += chunk.toString(); - }); - spawnedProcess.on('close', (code) => { - if (code !== 0) { - return reject(new Error(`${stderr}`)); - } - - resolve(stdout); - }); - spawnedProcess.on('error', (err) => reject(err)); - }).then((stdout) => { - // Sanitize just the text within commit subjects as that's the only potentially un-parseable text. - const sanitizedOutput = stdout.replace(/(?<="subject": ").*?(?="})/g, (subject) => sanitizeStringForJSONParse(subject)); - - // Then remove newlines, format as JSON and convert to a proper JS object - const json = `[${sanitizedOutput}]`.replace(/(\r\n|\n|\r)/gm, '').replace('},]', '}]'); - - return JSON.parse(json); - }); -} - -/** - * Parse merged PRs, excluding those from irrelevant branches. - * - * @param {Array>} commits - * @returns {Array} - */ -function getValidMergedPRs(commits) { - const mergedPRs = new Set(); - _.each(commits, (commit) => { - const author = commit.authorName; - if (author === CONST.OS_BOTIFY) { - return; - } - - const match = commit.subject.match(/Merge pull request #(\d+) from (?!Expensify\/.*-cherry-pick-staging)/); - if (!_.isArray(match) || match.length < 2) { - return; - } - - const pr = Number.parseInt(match[1], 10); - if (mergedPRs.has(pr)) { - // If a PR shows up in the log twice, that means that the PR was deployed in the previous checklist. - // That also means that we don't want to include it in the current checklist, so we remove it now. - mergedPRs.delete(pr); - return; - } - - mergedPRs.add(pr); - }); - - return Array.from(mergedPRs); -} - -/** - * Takes in two git tags and returns a list of PR numbers of all PRs merged between those two tags - * - * @param {String} fromTag - * @param {String} toTag - * @returns {Promise>} – Pull request numbers - */ -async function getPullRequestsMergedBetween(fromTag, toTag) { - console.log(`Looking for commits made between ${fromTag} and ${toTag}...`); - const commitList = await getCommitHistoryAsJSON(fromTag, toTag); - console.log(`Commits made between ${fromTag} and ${toTag}:`, commitList); - - // Find which commit messages correspond to merged PR's - const pullRequestNumbers = getValidMergedPRs(commitList).sort((a, b) => a - b); - console.log(`List of pull requests merged between ${fromTag} and ${toTag}`, pullRequestNumbers); - return pullRequestNumbers; -} - -module.exports = { - getValidMergedPRs, - getPullRequestsMergedBetween, -}; - - /***/ }), /***/ 7999: @@ -860,8 +703,20 @@ module.exports = function (inputString) { /***/ }), /***/ 8007: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { +/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { +"use strict"; +__nccwpck_require__.r(__webpack_exports__); +/* harmony export */ __nccwpck_require__.d(__webpack_exports__, { +/* harmony export */ "MAX_INCREMENTS": () => (/* binding */ MAX_INCREMENTS), +/* harmony export */ "SEMANTIC_VERSION_LEVELS": () => (/* binding */ SEMANTIC_VERSION_LEVELS), +/* harmony export */ "getPreviousVersion": () => (/* binding */ getPreviousVersion), +/* harmony export */ "getVersionNumberFromString": () => (/* binding */ getVersionNumberFromString), +/* harmony export */ "getVersionStringFromNumber": () => (/* binding */ getVersionStringFromNumber), +/* harmony export */ "incrementMinor": () => (/* binding */ incrementMinor), +/* harmony export */ "incrementPatch": () => (/* binding */ incrementPatch), +/* harmony export */ "incrementVersion": () => (/* binding */ incrementVersion) +/* harmony export */ }); const _ = __nccwpck_require__(5067); const SEMANTIC_VERSION_LEVELS = { @@ -991,18 +846,7 @@ function getPreviousVersion(currentVersion, level) { return getVersionStringFromNumber(major, minor, patch, build - 1); } -module.exports = { - getVersionNumberFromString, - getVersionStringFromNumber, - incrementVersion, - - // For tests - MAX_INCREMENTS, - SEMANTIC_VERSION_LEVELS, - incrementMinor, - incrementPatch, - getPreviousVersion, -}; + /***/ }), @@ -14076,6 +13920,164 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 1547: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const child_process_1 = __nccwpck_require__(2081); +const CONST = __importStar(__nccwpck_require__(4097)); +const sanitizeStringForJSONParse_1 = __importDefault(__nccwpck_require__(9338)); +const VERSION_UPDATER = __importStar(__nccwpck_require__(8007)); +/** + * @param [shallowExcludeTag] When fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) + */ +function fetchTag(tag, shallowExcludeTag = '') { + let shouldRetry = true; + let needsRepack = false; + while (shouldRetry) { + try { + let command = ''; + if (needsRepack) { + // We have seen some scenarios where this fixes the git fetch. + // Why? Who knows... https://github.com/Expensify/App/pull/31459 + command = 'git repack -d'; + console.log(`Running command: ${command}`); + (0, child_process_1.execSync)(command); + } + command = `git fetch origin tag ${tag} --no-tags`; + // Note that this condition is only ever NOT true in the 1.0.0-0 edge case + if (shallowExcludeTag && shallowExcludeTag !== tag) { + command += ` --shallow-exclude=${shallowExcludeTag}`; + } + console.log(`Running command: ${command}`); + (0, child_process_1.execSync)(command); + shouldRetry = false; + } + catch (e) { + console.error(e); + if (!needsRepack) { + console.log('Attempting to repack and retry...'); + needsRepack = true; + } + else { + console.error("Repack didn't help, giving up..."); + shouldRetry = false; + } + } + } +} +/** + * Get merge logs between two tags (inclusive) as a JavaScript object. + */ +function getCommitHistoryAsJSON(fromTag, toTag) { + // Fetch tags, exclude commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history + const previousPatchVersion = VERSION_UPDATER.getPreviousVersion(fromTag, VERSION_UPDATER.SEMANTIC_VERSION_LEVELS.PATCH); + fetchTag(fromTag, previousPatchVersion); + fetchTag(toTag, previousPatchVersion); + console.log('Getting pull requests merged between the following tags:', fromTag, toTag); + return new Promise((resolve, reject) => { + let stdout = ''; + let stderr = ''; + const args = ['log', '--format={"commit": "%H", "authorName": "%an", "subject": "%s"},', `${fromTag}...${toTag}`]; + console.log(`Running command: git ${args.join(' ')}`); + const spawnedProcess = (0, child_process_1.spawn)('git', args); + spawnedProcess.on('message', console.log); + spawnedProcess.stdout.on('data', (chunk) => { + console.log(chunk.toString()); + stdout += chunk.toString(); + }); + spawnedProcess.stderr.on('data', (chunk) => { + console.error(chunk.toString()); + stderr += chunk.toString(); + }); + spawnedProcess.on('close', (code) => { + if (code !== 0) { + return reject(new Error(`${stderr}`)); + } + resolve(stdout); + }); + spawnedProcess.on('error', (err) => reject(err)); + }).then((stdout) => { + // Sanitize just the text within commit subjects as that's the only potentially un-parseable text. + const sanitizedOutput = stdout.replace(/(?<="subject": ").*?(?="})/g, (subject) => (0, sanitizeStringForJSONParse_1.default)(subject)); + // Then remove newlines, format as JSON and convert to a proper JS object + const json = `[${sanitizedOutput}]`.replace(/(\r\n|\n|\r)/gm, '').replace('},]', '}]'); + return JSON.parse(json); + }); +} +/** + * Parse merged PRs, excluding those from irrelevant branches. + */ +function getValidMergedPRs(commits) { + const mergedPRs = new Set(); + commits.forEach((commit) => { + const author = commit.authorName; + if (author === CONST.OS_BOTIFY) { + return; + } + const match = commit.subject.match(/Merge pull request #(\d+) from (?!Expensify\/.*-cherry-pick-staging)/); + if (!Array.isArray(match) || match.length < 2) { + return; + } + const pr = Number.parseInt(match[1], 10); + if (mergedPRs.has(pr)) { + // If a PR shows up in the log twice, that means that the PR was deployed in the previous checklist. + // That also means that we don't want to include it in the current checklist, so we remove it now. + mergedPRs.delete(pr); + return; + } + mergedPRs.add(pr); + }); + return Array.from(mergedPRs); +} +/** + * Takes in two git tags and returns a list of PR numbers of all PRs merged between those two tags + */ +async function getPullRequestsMergedBetween(fromTag, toTag) { + console.log(`Looking for commits made between ${fromTag} and ${toTag}...`); + const commitList = await getCommitHistoryAsJSON(fromTag, toTag); + console.log(`Commits made between ${fromTag} and ${toTag}:`, commitList); + // Find which commit messages correspond to merged PR's + const pullRequestNumbers = getValidMergedPRs(commitList).sort((a, b) => a - b); + console.log(`List of pull requests merged between ${fromTag} and ${toTag}`, pullRequestNumbers); + return pullRequestNumbers; +} +exports["default"] = { + getValidMergedPRs, + getPullRequestsMergedBetween, +}; + + /***/ }), /***/ 2877: @@ -16438,6 +16440,34 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ } /******/ /************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __nccwpck_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __nccwpck_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ /******/ /* webpack/runtime/compat */ /******/ /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index 85e458af6947..545f4472ec72 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -5,8 +5,20 @@ /******/ var __webpack_modules__ = ({ /***/ 7: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { +/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { +"use strict"; +__nccwpck_require__.r(__webpack_exports__); +/* harmony export */ __nccwpck_require__.d(__webpack_exports__, { +/* harmony export */ "MAX_INCREMENTS": () => (/* binding */ MAX_INCREMENTS), +/* harmony export */ "SEMANTIC_VERSION_LEVELS": () => (/* binding */ SEMANTIC_VERSION_LEVELS), +/* harmony export */ "getPreviousVersion": () => (/* binding */ getPreviousVersion), +/* harmony export */ "getVersionNumberFromString": () => (/* binding */ getVersionNumberFromString), +/* harmony export */ "getVersionStringFromNumber": () => (/* binding */ getVersionStringFromNumber), +/* harmony export */ "incrementMinor": () => (/* binding */ incrementMinor), +/* harmony export */ "incrementPatch": () => (/* binding */ incrementPatch), +/* harmony export */ "incrementVersion": () => (/* binding */ incrementVersion) +/* harmony export */ }); const _ = __nccwpck_require__(67); const SEMANTIC_VERSION_LEVELS = { @@ -136,18 +148,7 @@ function getPreviousVersion(currentVersion, level) { return getVersionStringFromNumber(major, minor, patch, build - 1); } -module.exports = { - getVersionNumberFromString, - getVersionStringFromNumber, - incrementVersion, - // For tests - MAX_INCREMENTS, - SEMANTIC_VERSION_LEVELS, - incrementMinor, - incrementPatch, - getPreviousVersion, -}; /***/ }), @@ -5143,6 +5144,34 @@ module.exports = underscoreNodeF._; /******/ } /******/ /************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __nccwpck_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __nccwpck_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ /******/ /* webpack/runtime/compat */ /******/ /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; diff --git a/.github/libs/GitUtils.js b/.github/libs/GitUtils.ts similarity index 76% rename from .github/libs/GitUtils.js rename to .github/libs/GitUtils.ts index 2076763fbb55..e7423f97a934 100644 --- a/.github/libs/GitUtils.js +++ b/.github/libs/GitUtils.ts @@ -1,14 +1,18 @@ -const _ = require('underscore'); -const {spawn, execSync} = require('child_process'); -const CONST = require('./CONST'); -const sanitizeStringForJSONParse = require('./sanitizeStringForJSONParse'); -const {getPreviousVersion, SEMANTIC_VERSION_LEVELS} = require('../libs/versionUpdater'); +import {execSync, spawn} from 'child_process'; +import * as CONST from './CONST'; +import sanitizeStringForJSONParse from './sanitizeStringForJSONParse'; +import * as VERSION_UPDATER from './versionUpdater'; + +type CommitType = { + commit: string; + subject: string; + authorName: string; +}; /** - * @param {String} tag - * @param {String} [shallowExcludeTag] when fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) + * @param [shallowExcludeTag] When fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) */ -function fetchTag(tag, shallowExcludeTag = '') { +function fetchTag(tag: string, shallowExcludeTag = '') { let shouldRetry = true; let needsRepack = false; while (shouldRetry) { @@ -47,19 +51,15 @@ function fetchTag(tag, shallowExcludeTag = '') { /** * Get merge logs between two tags (inclusive) as a JavaScript object. - * - * @param {String} fromTag - * @param {String} toTag - * @returns {Promise>>} */ -function getCommitHistoryAsJSON(fromTag, toTag) { +function getCommitHistoryAsJSON(fromTag: string, toTag: string): Promise { // Fetch tags, exclude commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history - const previousPatchVersion = getPreviousVersion(fromTag, SEMANTIC_VERSION_LEVELS.PATCH); + const previousPatchVersion = VERSION_UPDATER.getPreviousVersion(fromTag, VERSION_UPDATER.SEMANTIC_VERSION_LEVELS.PATCH); fetchTag(fromTag, previousPatchVersion); fetchTag(toTag, previousPatchVersion); console.log('Getting pull requests merged between the following tags:', fromTag, toTag); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; const args = ['log', '--format={"commit": "%H", "authorName": "%an", "subject": "%s"},', `${fromTag}...${toTag}`]; @@ -89,26 +89,23 @@ function getCommitHistoryAsJSON(fromTag, toTag) { // Then remove newlines, format as JSON and convert to a proper JS object const json = `[${sanitizedOutput}]`.replace(/(\r\n|\n|\r)/gm, '').replace('},]', '}]'); - return JSON.parse(json); + return JSON.parse(json) as CommitType[]; }); } /** * Parse merged PRs, excluding those from irrelevant branches. - * - * @param {Array>} commits - * @returns {Array} */ -function getValidMergedPRs(commits) { - const mergedPRs = new Set(); - _.each(commits, (commit) => { +function getValidMergedPRs(commits: CommitType[]): number[] { + const mergedPRs = new Set(); + commits.forEach((commit) => { const author = commit.authorName; if (author === CONST.OS_BOTIFY) { return; } const match = commit.subject.match(/Merge pull request #(\d+) from (?!Expensify\/.*-cherry-pick-staging)/); - if (!_.isArray(match) || match.length < 2) { + if (!Array.isArray(match) || match.length < 2) { return; } @@ -128,12 +125,8 @@ function getValidMergedPRs(commits) { /** * Takes in two git tags and returns a list of PR numbers of all PRs merged between those two tags - * - * @param {String} fromTag - * @param {String} toTag - * @returns {Promise>} – Pull request numbers */ -async function getPullRequestsMergedBetween(fromTag, toTag) { +async function getPullRequestsMergedBetween(fromTag: string, toTag: string) { console.log(`Looking for commits made between ${fromTag} and ${toTag}...`); const commitList = await getCommitHistoryAsJSON(fromTag, toTag); console.log(`Commits made between ${fromTag} and ${toTag}:`, commitList); @@ -144,7 +137,8 @@ async function getPullRequestsMergedBetween(fromTag, toTag) { return pullRequestNumbers; } -module.exports = { +export default { getValidMergedPRs, getPullRequestsMergedBetween, }; +export type {CommitType}; diff --git a/.github/libs/versionUpdater.js b/.github/libs/versionUpdater.js index 61c8028e4e65..5526ee38d2ea 100644 --- a/.github/libs/versionUpdater.js +++ b/.github/libs/versionUpdater.js @@ -127,7 +127,7 @@ function getPreviousVersion(currentVersion, level) { return getVersionStringFromNumber(major, minor, patch, build - 1); } -module.exports = { +export { getVersionNumberFromString, getVersionStringFromNumber, incrementVersion, diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index ad0f60bf4018..338cb8313465 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -178,6 +178,21 @@ jobs: - name: Copy e2e code into zip folder run: cp -r tests/e2e zip + # Note: we can't reuse the apps tsconfig, as it depends on modules that aren't available in the AWS Device Farm environment + - name: Write tsconfig.json to zip folder + run: | + echo '{ + "compilerOptions": { + "target": "ESNext", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + } + }' > zip/tsconfig.json + - name: Zip everything in the zip directory up run: zip -qr App.zip ./zip diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index ebe31f41f3d8..7a293561de18 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup NodeJS uses: ./.github/actions/composite/setupNode @@ -22,6 +24,22 @@ jobs: git config --global user.email "test@test.com" git config --global user.name "Test" + - name: Get common ancestor commit + run: | + git fetch origin main + common_ancestor=$(git merge-base "${{ github.sha }}" origin/main) + echo "COMMIT_HASH=$common_ancestor" >> "$GITHUB_ENV" + + - name: Clean up deleted files + run: | + DELETED_FILES=$(git diff --name-only --diff-filter=D "$COMMIT_HASH" "${{ github.sha }}") + for file in $DELETED_FILES; do + if [ -n "$file" ]; then + rm -f "$file" + echo "Deleted file: $file" + fi + done + - name: Run performance testing script shell: bash run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6540a0fdd583..bdc14950a337 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,5 +62,8 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode + - name: Install ts-node + run: npm i -g ts-node + - name: Test CI git logic run: tests/unit/CIGitLogicTest.sh diff --git a/README.md b/README.md index c7ad11c1c93e..72736b3fedb7 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ For an M1 Mac, read this [SO](https://stackoverflow.com/questions/64901180/how-t * To run a on a **Development Simulator**: `npm run ios` * Changes applied to Javascript will be applied automatically, any changes to native code will require a recompile -If you want to run the app on an actual physical iOS device, please follow the instructions [here](https://github.com/Expensify/App/blob/docs/how-to-build-app-on-physcial-device/contributingGuides/HOW_TO_BUILD_APP_ON_PHYSICAL_IOS_DEVICE.md). +If you want to run the app on an actual physical iOS device, please follow the instructions [here](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_BUILD_APP_ON_PHYSICAL_IOS_DEVICE.md). ## Running the Android app 🤖 * Before installing Android dependencies, you need to obtain a token from Mapbox to download their SDKs. Please run `npm run configure-mapbox` and follow the instructions. If you already did this step for iOS, there is no need to repeat this step. diff --git a/__mocks__/@react-native-community/netinfo.ts b/__mocks__/@react-native-community/netinfo.ts index 0b7bdc9010a3..db0d34e2276d 100644 --- a/__mocks__/@react-native-community/netinfo.ts +++ b/__mocks__/@react-native-community/netinfo.ts @@ -2,12 +2,12 @@ import {NetInfoCellularGeneration, NetInfoStateType} from '@react-native-communi import type {addEventListener, configure, fetch, NetInfoState, refresh, useNetInfo} from '@react-native-community/netinfo'; const defaultState: NetInfoState = { - type: NetInfoStateType.cellular, + type: NetInfoStateType?.cellular, isConnected: true, isInternetReachable: true, details: { isConnectionExpensive: true, - cellularGeneration: NetInfoCellularGeneration['3g'], + cellularGeneration: NetInfoCellularGeneration?.['3g'], carrier: 'T-Mobile', }, }; diff --git a/__mocks__/react-native-onyx.js b/__mocks__/react-native-onyx.js deleted file mode 100644 index d44c73e824d3..000000000000 --- a/__mocks__/react-native-onyx.js +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ -import Onyx, {withOnyx} from 'react-native-onyx'; - -let connectCallbackDelay = 0; -function addDelayToConnectCallback(delay) { - connectCallbackDelay = delay; -} - -export default { - ...Onyx, - connect: (mapping) => - Onyx.connect({ - ...mapping, - callback: (...params) => { - if (connectCallbackDelay > 0) { - setTimeout(() => { - mapping.callback(...params); - }, connectCallbackDelay); - } else { - mapping.callback(...params); - } - }, - }), - addDelayToConnectCallback, -}; -export {withOnyx}; -/* eslint-enable rulesdir/prefer-onyx-connect-in-libs */ diff --git a/__mocks__/react-native-onyx.ts b/__mocks__/react-native-onyx.ts new file mode 100644 index 000000000000..253e3db47a96 --- /dev/null +++ b/__mocks__/react-native-onyx.ts @@ -0,0 +1,43 @@ +/** + * We are disabling the lint rule that doesn't allow the usage of Onyx.connect outside libs + * because the intent of this file is to mock the usage of react-native-onyx so we will have to mock the connect function + */ + +/* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ +import type {ConnectOptions, OnyxKey} from 'react-native-onyx'; +import Onyx, {withOnyx} from 'react-native-onyx'; + +let connectCallbackDelay = 0; +function addDelayToConnectCallback(delay: number) { + connectCallbackDelay = delay; +} + +type ReactNativeOnyxMock = { + addDelayToConnectCallback: (delay: number) => void; +} & typeof Onyx; + +type ConnectionCallback = NonNullable['callback']>; +type ConnectionCallbackParams = Parameters>; + +const reactNativeOnyxMock: ReactNativeOnyxMock = { + ...Onyx, + connect: (mapping: ConnectOptions) => { + const callback = (...params: ConnectionCallbackParams) => { + if (connectCallbackDelay > 0) { + setTimeout(() => { + (mapping.callback as (...args: ConnectionCallbackParams) => void)?.(...params); + }, connectCallbackDelay); + } else { + (mapping.callback as (...args: ConnectionCallbackParams) => void)?.(...params); + } + }; + return Onyx.connect({ + ...mapping, + callback, + }); + }, + addDelayToConnectCallback, +}; + +export default reactNativeOnyxMock; +export {withOnyx}; diff --git a/android/app/build.gradle b/android/app/build.gradle index c37b427daf63..1ee3d764415e 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 1001044202 - versionName "1.4.42-2" + versionCode 1001044305 + versionName "1.4.43-5" } flavorDimensions "default" diff --git a/android/app/src/main/java/com/expensify/chat/MainApplication.kt b/android/app/src/main/java/com/expensify/chat/MainApplication.kt index 193333368991..2362af009979 100644 --- a/android/app/src/main/java/com/expensify/chat/MainApplication.kt +++ b/android/app/src/main/java/com/expensify/chat/MainApplication.kt @@ -1,7 +1,9 @@ package com.expensify.chat +import android.app.ActivityManager import android.content.res.Configuration import android.database.CursorWindow +import android.os.Process import androidx.multidex.MultiDexApplication import com.expensify.chat.bootsplash.BootSplashPackage import com.facebook.react.PackageList @@ -40,6 +42,10 @@ class MainApplication : MultiDexApplication(), ReactApplication { override fun onCreate() { super.onCreate() + if (isOnfidoProcess()) { + return + } + SoLoader.init(this, /* native exopackage */false) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. @@ -73,4 +79,13 @@ class MainApplication : MultiDexApplication(), ReactApplication { super.onConfigurationChanged(newConfig) ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) } + + private fun isOnfidoProcess(): Boolean { + val pid = Process.myPid() + val manager = this.getSystemService(ACTIVITY_SERVICE) as ActivityManager + + return manager.runningAppProcesses.any { + it.pid == pid && it.processName.endsWith(":onfido_process") + } + } } diff --git a/android/build.gradle b/android/build.gradle index c4e25dde9e2b..7b5dd81e5bf1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -43,6 +43,13 @@ allprojects { def REACT_NATIVE_VERSION = new File(['node', '--print',"JSON.parse(require('fs').readFileSync(require.resolve('react-native/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim()) force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION force "com.facebook.react:hermes-engine:" + REACT_NATIVE_VERSION + + eachDependency { dependency -> + if (dependency.requested.group == 'org.bouncycastle') { + println dependency.requested.module + dependency.useTarget 'org.bouncycastle:bcprov-jdk15to18:1.71' + } + } } } repositories { diff --git a/assets/images/avatars/notifications-avatar.svg b/assets/images/avatars/notifications-avatar.svg new file mode 100644 index 000000000000..224baad22cf6 --- /dev/null +++ b/assets/images/avatars/notifications-avatar.svg @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/desktop/package-lock.json b/desktop/package-lock.json index bdc1b8e4bb1e..f6f96b647ae1 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.7", + "electron-updater": "^6.1.8", "node-machine-id": "^1.1.12" } }, @@ -156,9 +156,9 @@ } }, "node_modules/electron-updater": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.7.tgz", - "integrity": "sha512-SNOhYizjkm4ET+Y8ilJyUzcVsFJDtINzVN1TyHnZeMidZEG3YoBebMyXc/J6WSiXdUaOjC7ngekN6rNp6ardHA==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.8.tgz", + "integrity": "sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==", "dependencies": { "builder-util-runtime": "9.2.3", "fs-extra": "^10.1.0", @@ -541,9 +541,9 @@ "integrity": "sha512-OEC/48ZBJxR6XNSZtCl4cKPyQ1lvsu8yp8GdCplMWwGS1eEyMcEmzML5BRs/io/RLDnpgyf+7rSL+X6ICifRIg==" }, "electron-updater": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.7.tgz", - "integrity": "sha512-SNOhYizjkm4ET+Y8ilJyUzcVsFJDtINzVN1TyHnZeMidZEG3YoBebMyXc/J6WSiXdUaOjC7ngekN6rNp6ardHA==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.8.tgz", + "integrity": "sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==", "requires": { "builder-util-runtime": "9.2.3", "fs-extra": "^10.1.0", diff --git a/desktop/package.json b/desktop/package.json index da1610e49b23..de5834b9ce7a 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.7", + "electron-updater": "^6.1.8", "node-machine-id": "^1.1.12" }, "author": "Expensify, Inc.", diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md index 01d94b219801..bf4b21440b3c 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md @@ -49,16 +49,16 @@ The card will only appear in the drop-down list for assignment once it’s activ # Troubleshooting issues assigning company cards ## Why do bank connections break? -Banks often make changes to safeguard your confidential information, and when they do, we need to update the connection between Expensify and the bank. We have a team of engineers that works closely with banks to monitor this and update our software accordingly when this happens. +Banks often make changes to safeguard your confidential information, and when they do, we need to update the connection between Expensify and the bank. We have a team of engineers who work closely with banks to monitor this and update our software accordingly when this happens. The first step is to check if there have been any changes to your bank information. Have you recently changed your banking password without updating it in Expensify? Has your banking username or card number been updated? Did you update your security questions for your bank? -If you've answered "yes" to any of these questions, a Domain Admins need to update this information in Expensify and manually reestablish the connection by heading to *Settings* > *Domains* > _Domain Name_ > *Company Cards* > *Fix*. The Domain Admin will be prompted to enter the new credentials/updated information and this should reestablish the connection. +If you've answered "yes" to any of these questions, a Domain Admins need to update this information in Expensify and manually re-establish the connection by heading to *Settings* > *Domains* > _Domain Name_ > *Company Cards* > *Fix*. The Domain Admin will be prompted to enter the new credentials/updated information and this should reestablish the connection. ## How do I resolve errors while I’m trying to import my card?* Make sure you're importing your card in the correct spot in Expensify and selecting the right bank connection. For company cards, use the master administrative credentials to import your set of cards at *Settings* > *Domains* > _Domain Name_ > *Company Cards* > *Import Card*. Please note there are some things that cannot be bypassed within Expensify, including two-factor authentication being enabled within your bank account. This will prevent the connection from remaining stable and will need to be turned off on the bank side. ## What are the most reliable bank connections in Expensify?* -The most reliable corporate card to use with Expensify is the Expensify Card. We offer daily settlement, unapproved expense limits, and real-time compliance for secure and efficient spending, as well as 2% cash back. Click here to learn more or apply. +The most reliable corporate card to use with Expensify is the Expensify Visa® Commercial Card. We offer daily settlement, unapproved expense limits, and real-time compliance for secure and efficient spending, as well as 2% cash back (_Applies to USD purchases only._) Click here to learn more or apply. Additionally, we've teamed up with major banks worldwide to ensure a smooth import of credit card transactions into your accounts. Corporate cards from the following banks also offer the most dependable connections in Expensify: - American Express - Bank of America diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md index 9e35e51ec973..aa63aa3c38bd 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md @@ -195,7 +195,7 @@ As mentioned above, we’ll be able to pull in transactions as they post (daily) Expensify provides a corporate card with the following features: - Up to 2% cash back (_Applies to USD purchases only._) -- [SmartLimits]([https://help.expensify.com/articles/expensify-classic/expensify-card/Card-Settings](https://community.expensify.com/discussion/4851/deep-dive-what-are-smart-limits?utm_source=community-search&utm_medium=organic-search&utm_term=smart+limits)) to control what each individual cardholder can spend +- [SmartLimits](https://help.expensify.com/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features) to control what each individual cardholder can spend - A stable, unbreakable real-time connection (third-party bank feeds can run into connectivity issues) - Receipt compliance - informing notifications (e.g. add a receipt!) for users *as soon as the card is swiped* - Unlimited Virtual Cards - single-purpose cards with a fixed or monthly limit for specific company purchases diff --git a/docs/articles/expensify-classic/settings/account-settings/Manage-devices.md b/docs/articles/expensify-classic/settings/account-settings/Manage-devices.md new file mode 100644 index 000000000000..864c59a7472a --- /dev/null +++ b/docs/articles/expensify-classic/settings/account-settings/Manage-devices.md @@ -0,0 +1,18 @@ +--- +title: Manage devices +description: Control which devices can access your Expensify account +--- +
+ +You can see which devices have been used to access your Expensify account and even remove devices that you no longer want to have access to your account. + +{% include info.html %} +This process is currently not available from the mobile app and must be completed from the Expensify website. +{% include end-info.html %} + +1. Hover over Settings and click **Account**. +2. Under Account Details, scroll down to the Device Management section. +3. Click **Device Management** to expand the section. +4. Review the devices that have access to your account. To remove access for a specific device, click **Revoke** next to it. + +
diff --git a/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md b/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md new file mode 100644 index 000000000000..2d561ea598d9 --- /dev/null +++ b/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md @@ -0,0 +1,15 @@ +--- +title: Set notifications +description: Select your Expensify notification preferences +--- +
+ +{% include info.html %} +This process is currently not available from the mobile app and must be completed from the Expensify website. +{% include end-info.html %} + +1. Hover over Settings and click **Account**. +2. Click the **Preferences** tab on the left. +3. Scroll down to the Contact Preferences section. +4. Select the checkbox for the types of notifications you wish to receive. +
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 60ccc41d056d..1eca5a5021f7 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.42 + 1.4.43 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.42.2 + 1.4.43.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3308fbfa510a..e4453165b29a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.42 + 1.4.43 CFBundleSignature ???? CFBundleVersion - 1.4.42.2 + 1.4.43.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 5773eef2dba2..1bb165389913 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.42 + 1.4.43 CFBundleVersion - 1.4.42.2 + 1.4.43.5 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 80933065c450..52c817c739b3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -280,9 +280,9 @@ PODS: - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) - - Onfido (28.3.1) - - onfido-react-native-sdk (8.3.0): - - Onfido (~> 28.3.0) + - Onfido (29.6.0) + - onfido-react-native-sdk (10.6.0): + - Onfido (~> 29.6.0) - React - OpenSSL-Universal (1.1.1100) - Plaid (4.7.0) @@ -1903,8 +1903,8 @@ SPEC CHECKSUMS: MapboxMaps: cbb38845a9bf49b124f0e937975d560a4e01894e MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 - Onfido: 564f60c39819635ec5b549285a1eec278cc9ba67 - onfido-react-native-sdk: b346a620af5669f9fecb6dc3052314a35a94ad9f + Onfido: c52e797b10cc9e6d29ba91996cb62e501000bfdd + onfido-react-native-sdk: 4e7f0a7a986ed93cb906d2e0b67a6aab9202de0b OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c Plaid: 431ef9be5314a1345efb451bc5e6b067bfb3b4c6 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 diff --git a/package-lock.json b/package-lock.json index c94923a81699..93951ba0f241 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.42-2", + "version": "1.4.43-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.42-2", + "version": "1.4.43-5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -24,7 +24,7 @@ "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", - "@onfido/react-native-sdk": "8.3.0", + "@onfido/react-native-sdk": "10.6.0", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", @@ -8113,12 +8113,18 @@ } }, "node_modules/@onfido/react-native-sdk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-8.3.0.tgz", - "integrity": "sha512-nnhuvezd35v08WXUTQlX+gr4pbnNnwNV5KscC/jJrfjGikNUJnhnAHYxfnfJccTn44qUC6vRaKWq2GfpMUnqNA==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-10.6.0.tgz", + "integrity": "sha512-mNXfEjWWWgf3o/3F8orPmp24cShHsINJ1e15EeGNYOtm1XBPzq1FbEiiDp0pyuxgwpNFybGZGPjJcYpX0wwa4g==", + "dependencies": { + "js-base64": "3.7.5" + }, + "engines": { + "node": ">=16" + }, "peerDependencies": { "react": ">=17.0.0", - "react-native": ">=0.68.2 <1.0.x" + "react-native": ">=0.70.0 <1.0.x" } }, "node_modules/@pkgjs/parseargs": { @@ -38888,6 +38894,11 @@ "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", "license": "MIT" }, + "node_modules/js-base64": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", + "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" + }, "node_modules/js-cookie": { "version": "3.0.1", "license": "MIT", diff --git a/package.json b/package.json index 825f5b5a9908..76e3f689ca39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.42-2", + "version": "1.4.43-5", "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.", @@ -72,7 +72,7 @@ "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", - "@onfido/react-native-sdk": "8.3.0", + "@onfido/react-native-sdk": "10.6.0", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", diff --git a/patches/@onfido+react-native-sdk+10.6.0.patch b/patches/@onfido+react-native-sdk+10.6.0.patch new file mode 100644 index 000000000000..d61f4ab454c9 --- /dev/null +++ b/patches/@onfido+react-native-sdk+10.6.0.patch @@ -0,0 +1,18 @@ +diff --git a/node_modules/@onfido/react-native-sdk/android/build.gradle b/node_modules/@onfido/react-native-sdk/android/build.gradle +index 33a4229..1720bef 100644 +--- a/node_modules/@onfido/react-native-sdk/android/build.gradle ++++ b/node_modules/@onfido/react-native-sdk/android/build.gradle +@@ -84,6 +84,13 @@ android { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } ++ ++ publishing { ++ singleVariant('release') { ++ withSourcesJar() ++ withJavadocJar() ++ } ++ } + } + + repositories { diff --git a/patches/@onfido+react-native-sdk+8.3.0.patch b/patches/@onfido+react-native-sdk+8.3.0.patch deleted file mode 100644 index 5d3fd51bf826..000000000000 --- a/patches/@onfido+react-native-sdk+8.3.0.patch +++ /dev/null @@ -1,31 +0,0 @@ -diff --git a/node_modules/@onfido/react-native-sdk/android/build.gradle b/node_modules/@onfido/react-native-sdk/android/build.gradle -index b4c7106..c6efb0f 100644 ---- a/node_modules/@onfido/react-native-sdk/android/build.gradle -+++ b/node_modules/@onfido/react-native-sdk/android/build.gradle -@@ -84,6 +84,13 @@ android { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -+ -+ publishing { -+ singleVariant('release') { -+ withSourcesJar() -+ withJavadocJar() -+ } -+ } - } - - repositories { -@@ -135,9 +142,9 @@ afterEvaluate { project -> - group = "Reporting" - description = "Generate Jacoco coverage reports after running tests." - reports { -- xml.enabled = true -- html.enabled = true -- csv.enabled = true -+ xml.required = true -+ html.required = true -+ csv.required = true - } - classDirectories.setFrom(fileTree( - dir: 'build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/onfido/reactnative/sdk', diff --git a/src/CONST.ts b/src/CONST.ts index df0e1ad1c4ee..6a57738d06ec 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -37,6 +37,9 @@ const keyInputRightArrow = KeyCommand?.constants?.keyInputRightArrow ?? 'keyInpu // describes if a shortcut key can cause navigation const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT'; +// Explicit type annotation is required +const cardActiveStates: number[] = [2, 3, 4, 7]; + const CONST = { ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, @@ -123,7 +126,7 @@ const CONST = { }, DATE_BIRTH: { - MIN_AGE: 5, + MIN_AGE: 0, MIN_AGE_FOR_PAYMENT: 18, MAX_AGE: 150, }, @@ -145,23 +148,23 @@ const CONST = { SMALL_SCREEN: { IMAGE_HEIGHT: 300, CONTAINER_MINHEIGHT: 200, - VIEW_HEIGHT: 185, + VIEW_HEIGHT: 240, }, WIDE_SCREEN: { IMAGE_HEIGHT: 450, CONTAINER_MINHEIGHT: 500, - VIEW_HEIGHT: 275, + VIEW_HEIGHT: 390, }, MONEY_OR_TASK_REPORT: { SMALL_SCREEN: { IMAGE_HEIGHT: 300, CONTAINER_MINHEIGHT: 280, - VIEW_HEIGHT: 220, + VIEW_HEIGHT: 240, }, WIDE_SCREEN: { IMAGE_HEIGHT: 450, CONTAINER_MINHEIGHT: 280, - VIEW_HEIGHT: 275, + VIEW_HEIGHT: 390, }, }, }, @@ -1441,7 +1444,7 @@ const CONST = { CLOSED: 6, STATE_SUSPENDED: 7, }, - ACTIVE_STATES: [2, 3, 4, 7], + ACTIVE_STATES: cardActiveStates, }, AVATAR_ROW_SIZE: { DEFAULT: 4, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c1151f35dc7d..ca9be53798bc 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -73,7 +73,6 @@ const ROUTES = { SETTINGS_TIMEZONE: 'settings/profile/timezone', SETTINGS_TIMEZONE_SELECT: 'settings/profile/timezone/select', SETTINGS_PRONOUNS: 'settings/profile/pronouns', - SETTINGS_LOUNGE_ACCESS: 'settings/profile/lounge-access', SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', @@ -511,7 +510,7 @@ const ROUTES = { // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', - getRoute: (contentType: string) => `referral/${contentType}` as const, + getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo), }, PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', } as const; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 648d2ef1b7d8..18754a3513c1 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -29,7 +29,6 @@ const SCREENS = { SECURITY: 'Settings_Security', ABOUT: 'Settings_About', APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links', - LOUNGE_ACCESS: 'Settings_Lounge_Access', ADD_DEBIT_CARD: 'Settings_Add_Debit_Card', ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', CLOSE: 'Settings_Close', diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 4abe5655e307..803b7f2cdabe 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -4,7 +4,6 @@ import React from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; -import compose from '@libs/compose'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import iouReportPropTypes from '@pages/iouReportPropTypes'; @@ -13,7 +12,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import * as Expensicons from './Icon/Expensicons'; import PopoverMenu from './PopoverMenu'; import refPropTypes from './refPropTypes'; -import withWindowDimensions from './withWindowDimensions'; const propTypes = { /** Should the component be visible? */ @@ -122,11 +120,8 @@ AddPaymentMethodMenu.propTypes = propTypes; AddPaymentMethodMenu.defaultProps = defaultProps; AddPaymentMethodMenu.displayName = 'AddPaymentMethodMenu'; -export default compose( - withWindowDimensions, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), -)(AddPaymentMethodMenu); +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(AddPaymentMethodMenu); diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index d02957aa8137..6da7be841537 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -100,7 +100,7 @@ function AttachmentView({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); - const isVideo = Str.isVideo(source); + const isVideo = typeof source === 'string' && Str.isVideo(source); useEffect(() => { if (!isFocused) { diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js index a957e31a9de4..0e57bcf4db03 100644 --- a/src/components/CategoryPicker/index.js +++ b/src/components/CategoryPicker/index.js @@ -1,22 +1,18 @@ import lodashGet from 'lodash/get'; -import React, {useMemo, useState} from 'react'; +import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import OptionsSelector from '@components/OptionsSelector'; +import SelectionList from '@components/SelectionList'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {defaultProps, propTypes} from './categoryPickerPropTypes'; function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, onSubmit}) { - const styles = useThemeStyles(); const {translate} = useLocalize(); - const [searchValue, setSearchValue] = useState(''); - - const policyCategoriesCount = OptionsListUtils.getEnabledCategoriesCount(_.values(policyCategories)); - const isCategoriesCountBelowThreshold = policyCategoriesCount < CONST.CATEGORY_LIST_THRESHOLD; + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const selectedOptions = useMemo(() => { if (!selectedCategory) { @@ -28,17 +24,18 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC name: selectedCategory, enabled: true, accountID: null, + isSelected: true, }, ]; }, [selectedCategory]); - const sections = useMemo(() => { + const [sections, headerMessage, policyCategoriesCount, shouldShowTextInput] = useMemo(() => { const validPolicyRecentlyUsedCategories = _.filter(policyRecentlyUsedCategories, (p) => !_.isEmpty(p)); const {categoryOptions} = OptionsListUtils.getFilteredOptions( {}, {}, [], - searchValue, + debouncedSearchValue, selectedOptions, [], false, @@ -49,31 +46,28 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC false, ); - return categoryOptions; - }, [policyCategories, policyRecentlyUsedCategories, searchValue, selectedOptions]); + const header = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(categoryOptions, '[0].data', []).length > 0, debouncedSearchValue); + const policiesCount = OptionsListUtils.getEnabledCategoriesCount(_.values(policyCategories)); + const isCategoriesCountBelowThreshold = policyCategoriesCount < CONST.CATEGORY_LIST_THRESHOLD; + const showInput = !isCategoriesCountBelowThreshold; + + return [categoryOptions, header, policiesCount, showInput]; + }, [policyCategories, policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions]); - const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(sections, '[0].data.length', 0) > 0, searchValue); - const shouldShowTextInput = !isCategoriesCountBelowThreshold; - const selectedOptionKey = lodashGet(_.filter(lodashGet(sections, '[0].data', []), (category) => category.searchText === selectedCategory)[0], 'keyForList'); + const selectedOptionKey = useMemo( + () => lodashGet(_.filter(lodashGet(sections, '[0].data', []), (category) => category.searchText === selectedCategory)[0], 'keyForList'), + [sections, selectedCategory], + ); return ( - ); } diff --git a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx index 21c57f79f39c..49915ebfbf1b 100644 --- a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx +++ b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx @@ -6,14 +6,14 @@ import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import type CalendarPickerRadioItem from './types'; +import type CalendarPickerListItem from './types'; type YearPickerModalProps = { /** Whether the modal is visible */ isVisible: boolean; /** The list of years to render */ - years: CalendarPickerRadioItem[]; + years: CalendarPickerListItem[]; /** Currently selected year */ currentYear?: number; @@ -33,7 +33,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear const yearsList = searchText === '' ? years : years.filter((year) => year.text.includes(searchText)); return { headerMessage: !yearsList.length ? translate('common.noResultsFound') : '', - sections: [{data: yearsList, indexOffset: 0}], + sections: [{data: yearsList.sort((a, b) => b.value - a.value), indexOffset: 0}], }; }, [years, searchText, translate]); @@ -72,12 +72,13 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear inputMode={CONST.INPUT_MODE.NUMERIC} headerMessage={headerMessage} sections={sections} - onSelectRow={(option: CalendarPickerRadioItem) => { + onSelectRow={(option) => { onYearChange?.(option.value); }} initiallyFocusedOptionKey={currentYear.toString()} showScrollIndicator shouldStopPropagation + shouldUseDynamicMaxToRenderPerBatch /> diff --git a/src/components/DatePicker/CalendarPicker/index.tsx b/src/components/DatePicker/CalendarPicker/index.tsx index 60673f9a28d2..1b290aacd30d 100644 --- a/src/components/DatePicker/CalendarPicker/index.tsx +++ b/src/components/DatePicker/CalendarPicker/index.tsx @@ -13,7 +13,7 @@ import getButtonState from '@libs/getButtonState'; import CONST from '@src/CONST'; import ArrowIcon from './ArrowIcon'; import generateMonthMatrix from './generateMonthMatrix'; -import type CalendarPickerRadioItem from './types'; +import type CalendarPickerListItem from './types'; import YearPickerModal from './YearPickerModal'; type CalendarPickerProps = { @@ -59,7 +59,7 @@ function CalendarPicker({ const minYear = getYear(new Date(minDate)); const maxYear = getYear(new Date(maxDate)); - const [years, setYears] = useState( + const [years, setYears] = useState( Array.from({length: maxYear - minYear + 1}, (v, i) => i + minYear).map((year) => ({ text: year.toString(), value: year, diff --git a/src/components/DatePicker/CalendarPicker/types.ts b/src/components/DatePicker/CalendarPicker/types.ts index ce5e16d50535..3183b3a82b6f 100644 --- a/src/components/DatePicker/CalendarPicker/types.ts +++ b/src/components/DatePicker/CalendarPicker/types.ts @@ -1,8 +1,8 @@ -import type {RadioItem} from '@components/SelectionList/types'; +import type {ListItem} from '@components/SelectionList/types'; -type CalendarPickerRadioItem = RadioItem & { +type CalendarPickerListItem = ListItem & { /** The value representing a year in the CalendarPicker */ value: number; }; -export default CalendarPickerRadioItem; +export default CalendarPickerListItem; diff --git a/src/components/EmojiPicker/EmojiSkinToneList.js b/src/components/EmojiPicker/EmojiSkinToneList.js index da2559535895..7f36d929025f 100644 --- a/src/components/EmojiPicker/EmojiSkinToneList.js +++ b/src/components/EmojiPicker/EmojiSkinToneList.js @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import * as Emojis from '@assets/emojis'; @@ -27,11 +27,18 @@ function EmojiSkinToneList() { * @param {object} skinToneEmoji */ function updateSelectedSkinTone(skinToneEmoji) { - toggleIsSkinToneListVisible(); setHighlightedIndex(skinToneEmoji.skinTone); setPreferredSkinTone(skinToneEmoji.skinTone); } + useEffect(() => { + if (!isSkinToneListVisible) { + return; + } + toggleIsSkinToneListVisible(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- only run when preferredSkinTone updates + }, [preferredSkinTone]); + const currentSkinTone = getSkinToneEmojiFromIndex(preferredSkinTone); return ( diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index cb2740fdafe5..ae98978ffcad 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -10,6 +10,7 @@ import type Picker from '@components/Picker'; import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import type StatePicker from '@components/StatePicker'; import type TextInput from '@components/TextInput'; +import type ValuePicker from '@components/ValuePicker'; import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker'; import type {TranslationPaths} from '@src/languages/types'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; @@ -32,7 +33,8 @@ type ValidInputs = | typeof CountrySelector | typeof AmountForm | typeof BusinessTypePicker - | typeof StatePicker; + | typeof StatePicker + | typeof ValuePicker; type ValueTypeKey = 'string' | 'boolean' | 'date'; type ValueTypeMap = { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index 863fe6fbabb1..465a4f747bcb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -65,7 +65,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { // eslint-disable-next-line react/jsx-props-no-multi-spaces target={htmlAttribs.target || '_blank'} rel={htmlAttribs.rel || 'noopener noreferrer'} - style={[parentStyle, styles.textUnderlinePositionUnder, styles.textDecorationSkipInkNone, style]} + style={[style, parentStyle, styles.textUnderlinePositionUnder, styles.textDecorationSkipInkNone]} key={key} // Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined} diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index ced18fdf6ba6..553a60e568ec 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -12,6 +12,7 @@ import DeletedRoomAvatar from '@assets/images/avatars/deleted-room.svg'; import DomainRoomAvatar from '@assets/images/avatars/domain-room.svg'; import FallbackAvatar from '@assets/images/avatars/fallback-avatar.svg'; import FallbackWorkspaceAvatar from '@assets/images/avatars/fallback-workspace-avatar.svg'; +import NotificationsAvatar from '@assets/images/avatars/notifications-avatar.svg'; import ActiveRoomAvatar from '@assets/images/avatars/room.svg'; import BackArrow from '@assets/images/back-left.svg'; import Bank from '@assets/images/bank.svg'; @@ -146,7 +147,6 @@ import Wallet from '@assets/images/wallet.svg'; import Workspace from '@assets/images/workspace-default-avatar.svg'; import Wrench from '@assets/images/wrench.svg'; import Zoom from '@assets/images/zoom.svg'; -import LoungeAccess from './svgs/LoungeAccessIcon'; export { ActiveRoomAvatar, @@ -235,7 +235,6 @@ export { LinkCopy, Location, Lock, - LoungeAccess, Luggage, MagnifyingGlass, Mail, @@ -249,6 +248,7 @@ export { ExpensifyLogoNew, NewWindow, NewWorkspace, + NotificationsAvatar, Offline, OfflineCloud, OldDotWireframe, diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 911f09104349..3f6b6ca20540 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -12,7 +12,6 @@ import JewelBoxBlue from '@assets/images/product-illustrations/jewel-box--blue.s import JewelBoxGreen from '@assets/images/product-illustrations/jewel-box--green.svg'; import JewelBoxPink from '@assets/images/product-illustrations/jewel-box--pink.svg'; import JewelBoxYellow from '@assets/images/product-illustrations/jewel-box--yellow.svg'; -import Lounge from '@assets/images/product-illustrations/lounge.svg'; import MagicCode from '@assets/images/product-illustrations/magic-code.svg'; import MoneyEnvelopeBlue from '@assets/images/product-illustrations/money-envelope--blue.svg'; import MoneyMousePink from '@assets/images/product-illustrations/money-mouse--pink.svg'; @@ -107,7 +106,6 @@ export { CreditCardsNew, InvoiceBlue, LockOpen, - Lounge, Luggage, MoneyIntoWallet, MoneyWings, diff --git a/src/components/Icon/svgs/LoungeAccessIcon.tsx b/src/components/Icon/svgs/LoungeAccessIcon.tsx deleted file mode 100644 index 0dcf9052ca87..000000000000 --- a/src/components/Icon/svgs/LoungeAccessIcon.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import Svg, {G, Path, Polygon} from 'react-native-svg'; -import useTheme from '@hooks/useTheme'; - -type LoungeAccessIconProps = { - /** The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue'. */ - fill?: string; - - /** Is icon hovered */ - hovered?: string; - - /** Is icon pressed */ - pressed?: string; - - /** Icon's width */ - width?: number; - - /** Icon's height */ - height?: number; -}; - -function LoungeAccessIcon({fill, hovered = 'false', pressed = 'false', width, height}: LoungeAccessIconProps) { - const theme = useTheme(); - return ( - - - - - - - - - - - - - ); -} - -LoungeAccessIcon.displayName = 'LoungeAccessIcon'; -export default LoungeAccessIcon; diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index b43e5265a970..33dfd2b920a9 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -245,7 +245,6 @@ function KYCWall({ return ( <> setShouldShowAddPaymentMenu(false)} diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index 1ee8010574c3..53ed00e04143 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -63,7 +63,7 @@ type KYCWallProps = { onSuccessfulKYC: (iouPaymentType?: PaymentMethodType, currentSource?: Source) => void; /** Children to build the KYC */ - children: (continueAction: (event: GestureResponderEvent | KeyboardEvent | undefined, method: PaymentMethodType) => void, anchorRef: RefObject) => void; + children: (continueAction: (event: GestureResponderEvent | KeyboardEvent | undefined, method?: PaymentMethodType) => void, anchorRef: RefObject) => void; }; -export type {AnchorPosition, KYCWallProps, PaymentMethod, DomRect}; +export type {AnchorPosition, KYCWallProps, PaymentMethod, DomRect, PaymentMethodType, Source}; diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx index 808bc344af37..18cb9188d60c 100644 --- a/src/components/LottieAnimations/index.tsx +++ b/src/components/LottieAnimations/index.tsx @@ -3,11 +3,6 @@ import variables from '@styles/variables'; import type DotLottieAnimation from './types'; const DotLottieAnimations = { - ExpensifyLounge: { - file: require('@assets/animations/ExpensifyLounge.lottie'), - w: 1920, - h: 1080, - }, FastMoney: { file: require('@assets/animations/FastMoney.lottie'), w: 375, diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index a3178f642852..77447f13644c 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -30,7 +30,21 @@ const MapView = forwardRef( const [isIdle, setIsIdle] = useState(false); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); - const hasAskedForLocationPermission = useRef(false); + const shouldInitializeCurrentPosition = useRef(true); + + // Determines if map can be panned to user's detected + // location without bothering the user. It will return + // false if user has already started dragging the map or + // if there are one or more waypoints present. + const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); + + const setCurrentPositionToInitialState = useCallback(() => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (cachedUserLocation || !initialState) { + return; + } + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + }, [initialState, cachedUserLocation]); useFocusEffect( useCallback(() => { @@ -38,34 +52,24 @@ const MapView = forwardRef( return; } - if (hasAskedForLocationPermission.current) { + if (!shouldInitializeCurrentPosition.current) { return; } - hasAskedForLocationPermission.current = true; - getCurrentPosition( - (params) => { - const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; - setCurrentPosition(currentCoords); - setUserLocation(currentCoords); - }, - () => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (cachedUserLocation || !initialState) { - return; - } - - setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); - }, - ); - }, [cachedUserLocation, initialState, isOffline]), - ); + shouldInitializeCurrentPosition.current = false; - // Determines if map can be panned to user's detected - // location without bothering the user. It will return - // false if user has already started dragging the map or - // if there are one or more waypoints present. - const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); + if (!shouldPanMapToCurrentPosition()) { + setCurrentPositionToInitialState(); + return; + } + + getCurrentPosition((params) => { + const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; + setCurrentPosition(currentCoords); + setUserLocation(currentCoords); + }, setCurrentPositionToInitialState); + }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]), + ); useEffect(() => { if (!currentPosition || !cameraRef.current) { diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index 289f7d0d62a8..05be6d6409e8 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -53,7 +53,21 @@ const MapView = forwardRef( const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const [shouldResetBoundaries, setShouldResetBoundaries] = useState(false); const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); - const hasAskedForLocationPermission = useRef(false); + const shouldInitializeCurrentPosition = useRef(true); + + // Determines if map can be panned to user's detected + // location without bothering the user. It will return + // false if user has already started dragging the map or + // if there are one or more waypoints present. + const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); + + const setCurrentPositionToInitialState = useCallback(() => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (cachedUserLocation || !initialState) { + return; + } + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + }, [initialState, cachedUserLocation]); useFocusEffect( useCallback(() => { @@ -61,34 +75,24 @@ const MapView = forwardRef( return; } - if (hasAskedForLocationPermission.current) { + if (!shouldInitializeCurrentPosition.current) { return; } - hasAskedForLocationPermission.current = true; - getCurrentPosition( - (params) => { - const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; - setCurrentPosition(currentCoords); - setUserLocation(currentCoords); - }, - () => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (cachedUserLocation || !initialState) { - return; - } - - setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); - }, - ); - }, [cachedUserLocation, initialState, isOffline]), - ); + shouldInitializeCurrentPosition.current = false; - // Determines if map can be panned to user's detected - // location without bothering the user. It will return - // false if user has already started dragging the map or - // if there are one or more waypoints present. - const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); + if (!shouldPanMapToCurrentPosition()) { + setCurrentPositionToInitialState(); + return; + } + + getCurrentPosition((params) => { + const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; + setCurrentPosition(currentCoords); + setUserLocation(currentCoords); + }, setCurrentPositionToInitialState); + }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]), + ); useEffect(() => { if (!currentPosition || !mapRef) { diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 7fc5013f58a0..2b8d25d5d639 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -80,7 +80,7 @@ function BaseModal( isVisibleRef.current = isVisible; let removeOnCloseListener: () => void; if (isVisible) { - Modal.willAlertModalBecomeVisible(true); + Modal.willAlertModalBecomeVisible(true, type === CONST.MODAL.MODAL_TYPE.POPOVER || type === CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED); // To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu removeOnCloseListener = Modal.setCloseModal(onClose); } @@ -91,7 +91,7 @@ function BaseModal( } removeOnCloseListener(); }; - }, [isVisible, wasVisible, onClose]); + }, [isVisible, wasVisible, onClose, type]); useEffect( () => () => { diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 26016bd51ec0..690897d548ce 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -84,6 +84,7 @@ class BaseOptionsSelector extends Component { const allOptions = this.flattenSections(); const sections = this.sliceSections(); const focusedIndex = this.getInitiallyFocusedIndex(allOptions); + this.focusedOption = allOptions[focusedIndex]; this.state = { sections, @@ -146,6 +147,10 @@ class BaseOptionsSelector extends Component { }); } + if (prevState.focusedIndex !== this.state.focusedIndex) { + this.focusedOption = this.state.allOptions[this.state.focusedIndex]; + } + if (_.isEqual(this.props.sections, prevProps.sections)) { return; } @@ -162,13 +167,14 @@ class BaseOptionsSelector extends Component { } const newFocusedIndex = this.props.selectedOptions.length; const isNewFocusedIndex = newFocusedIndex !== this.state.focusedIndex; - + const prevFocusedOption = _.find(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList); + const prevFocusedOptionIndex = prevFocusedOption ? _.findIndex(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList) : undefined; // eslint-disable-next-line react/no-did-update-set-state this.setState( { sections: newSections, allOptions: newOptions, - focusedIndex: _.isNumber(this.props.focusedIndex) ? this.props.focusedIndex : newFocusedIndex, + focusedIndex: prevFocusedOptionIndex || (_.isNumber(this.props.focusedIndex) ? this.props.focusedIndex : newFocusedIndex), }, () => { // If we just toggled an option on a multi-selection page or cleared the search input, scroll to top diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index 762e79fab63c..e1cd18ba4767 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -3,32 +3,32 @@ import {createPortal} from 'react-dom'; import Modal from '@components/Modal'; import {PopoverContext} from '@components/PopoverProvider'; import PopoverWithoutOverlay from '@components/PopoverWithoutOverlay'; -import withWindowDimensions from '@components/withWindowDimensions'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; -import type {PopoverWithWindowDimensionsProps} from './types'; +import type {PopoverProps} from './types'; /* * This is a convenience wrapper around the Modal component for a responsive Popover. * On small screen widths, it uses BottomDocked modal type, and a Popover type on wide screen widths. */ -function Popover(props: PopoverWithWindowDimensionsProps) { +function Popover(props: PopoverProps) { const { isVisible, onClose, - isSmallScreenWidth, fullscreen, animationInTiming = CONST.ANIMATED_TRANSITION, onLayout, animationOutTiming, disableAnimation = true, - withoutOverlay, + withoutOverlay = false, anchorPosition = {}, anchorRef = () => {}, animationIn = 'fadeIn', animationOut = 'fadeOut', } = props; + const {isSmallScreenWidth} = useWindowDimensions(); const withoutOverlayRef = useRef(null); const {close, popover} = React.useContext(PopoverContext); @@ -106,4 +106,4 @@ function Popover(props: PopoverWithWindowDimensionsProps) { Popover.displayName = 'Popover'; -export default withWindowDimensions(Popover); +export default Popover; diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index c6c0e98b546c..e06037f47b63 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -26,7 +26,7 @@ type PopoverProps = BaseModalProps & disableAnimation?: boolean; /** Whether we don't want to show overlay */ - withoutOverlay: boolean; + withoutOverlay?: boolean; /** The dimensions of the popover */ popoverDimensions?: PopoverDimensions; diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index 71fedf0e6a01..06478b468e1e 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -60,7 +60,7 @@ function PopoverWithoutOverlay( close(anchorRef); Modal.onModalDidClose(); } - Modal.willAlertModalBecomeVisible(isVisible); + Modal.willAlertModalBecomeVisible(isVisible, true); return () => { if (!removeOnClose) { diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx index 13fa07a4918b..f1dc47b8556b 100644 --- a/src/components/ProcessMoneyRequestHoldMenu.tsx +++ b/src/components/ProcessMoneyRequestHoldMenu.tsx @@ -45,7 +45,7 @@ function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosit {translate('iou.holdEducationalTitle')} - {translate('iou.hold')}; + {translate('iou.hold')}