diff --git a/.eslintignore b/.eslintignore index 396bfd28c614..d3358a02fe4b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ **/node_modules/* **/dist/* +android/**/build/* .github/actions/**/index.js" docs/vendor/** diff --git a/.eslintrc.js b/.eslintrc.js index 281f8269804e..c0c95d3f5686 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -72,8 +72,8 @@ const restrictedImportPatterns = [ ]; module.exports = { - extends: ['expensify', 'plugin:storybook/recommended', 'plugin:react-hooks/recommended', 'plugin:react-native-a11y/basic', 'plugin:@dword-design/import-alias/recommended', 'prettier'], - plugins: ['react-hooks', 'react-native-a11y'], + extends: ['expensify', 'plugin:storybook/recommended', 'plugin:react-native-a11y/basic', 'plugin:@dword-design/import-alias/recommended', 'prettier'], + plugins: ['react-native-a11y'], parser: 'babel-eslint', ignorePatterns: ['!.*', 'src/vendor', '.github/actions/**/index.js', 'desktop/dist/*.js', 'dist/*.js', 'node_modules/.bin/**', 'node_modules/.cache/**', '.git/**'], env: { @@ -87,6 +87,7 @@ module.exports = { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], plugins: ['react'], rules: { + 'prefer-regex-literals': 'off', 'rulesdir/no-multiple-onyx-in-file': 'off', 'rulesdir/onyx-props-must-have-default': 'off', 'react-native-a11y/has-accessibility-hint': ['off'], @@ -112,6 +113,7 @@ module.exports = { '@styles': './src/styles', // This path is provide alias for files like `ONYXKEYS` and `CONST`. '@src': './src', + '@desktop': './desktop', }, }, ], diff --git a/.github/ISSUE_TEMPLATE/NewLibraryRequest.md b/.github/ISSUE_TEMPLATE/NewLibraryRequest.md index 83397d812780..bd370970420e 100644 --- a/.github/ISSUE_TEMPLATE/NewLibraryRequest.md +++ b/.github/ISSUE_TEMPLATE/NewLibraryRequest.md @@ -5,6 +5,8 @@ labels: Weekly, AutoAssignerAppLibraryReview --- In order to properly evaluate if a new library can be added to `package.json`, please fill out this request form. It will be automatically assigned someone from our review team that will go through and vet the library. +*In order to add any new production dependency, it must be approved by the App Deployer team. They will evaluate the library and decide if it's something we want to move forward with or if other alternatives should be explored.* + Note: This is only for production dependencies. While we don't want people to add packages to dev-dependencies willy-nilly, we recognize that there isn't as great of a need there to secure them. # Name of library: diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml index 7e1b5fbbae90..0b32d8ee6dc1 100644 --- a/.github/actions/composite/setupNode/action.yml +++ b/.github/actions/composite/setupNode/action.yml @@ -1,6 +1,11 @@ name: Set up Node description: Set up Node +outputs: + cache-hit: + description: Was there a cache hit on the main node_modules? + value: ${{ steps.cache-node-modules.outputs.cache-hit }} + runs: using: composite steps: 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/failureNotifier.yml b/.github/workflows/failureNotifier.yml index d2e0ec4f38e5..9eb5bc6eb409 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -88,7 +88,7 @@ jobs: repo: context.repo.repo, title: issueTitle, body: issueBody, - labels: [failureLabel, 'Daily'], + labels: [failureLabel, 'Hourly'], assignees: [prMerger] }); } diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 7c7b51240fdb..818441828bf0 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -184,6 +184,7 @@ jobs: run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - name: Setup Node + id: setup-node uses: ./.github/actions/composite/setupNode - name: Setup Ruby @@ -197,7 +198,7 @@ jobs: id: pods-cache with: path: ios/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock') }} + key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} restore-keys: ${{ runner.os }}-pods-cache- - name: Compare Podfile.lock and Manifest.lock @@ -206,7 +207,7 @@ jobs: - name: Install cocoapods uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 - if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' + if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' with: timeout_minutes: 10 max_attempts: 5 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/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 25a14fb27e1b..9548c3a6e595 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -154,6 +154,7 @@ jobs: echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc - name: Setup Node + id: setup-node uses: ./.github/actions/composite/setupNode - name: Setup XCode @@ -170,7 +171,7 @@ jobs: id: pods-cache with: path: ios/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock') }} + key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} restore-keys: ${{ runner.os }}-pods-cache- - name: Compare Podfile.lock and Manifest.lock @@ -179,7 +180,7 @@ jobs: - name: Install cocoapods uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 - if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' + if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' with: timeout_minutes: 10 max_attempts: 5 diff --git a/.prettierrc.js b/.prettierrc.js index bcc67708cc95..5c9b25ec472a 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -6,7 +6,20 @@ module.exports = { arrowParens: 'always', printWidth: 190, singleAttributePerLine: true, - importOrder: ['@assets/(.*)$', '@components/(.*)$', '@hooks/(.*)$', '@libs/(.*)$', '@navigation/(.*)$', '@pages/(.*)$', '@styles/(.*)$', '@userActions/(.*)$', '@src/(.*)$', '^[./]'], + /** `importOrder` should be defined in an alphabetical order. */ + importOrder: [ + '@assets/(.*)$', + '@components/(.*)$', + '@desktop/(.*)$', + '@hooks/(.*)$', + '@libs/(.*)$', + '@navigation/(.*)$', + '@pages/(.*)$', + '@styles/(.*)$', + '@userActions/(.*)$', + '@src/(.*)$', + '^[./]', + ], importOrderSortSpecifiers: true, importOrderCaseInsensitive: true, }; 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-localize.ts b/__mocks__/react-native-localize.ts index aa0322d6714c..0188b8afd23f 100644 --- a/__mocks__/react-native-localize.ts +++ b/__mocks__/react-native-localize.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-import-module-exports import mockRNLocalize from 'react-native-localize/mock'; module.exports = mockRNLocalize; 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/__mocks__/react-native-webview.js b/__mocks__/react-native-webview.ts similarity index 55% rename from __mocks__/react-native-webview.js rename to __mocks__/react-native-webview.ts index 58875fd5288b..8266c7b1eda0 100644 --- a/__mocks__/react-native-webview.js +++ b/__mocks__/react-native-webview.ts @@ -1,6 +1,8 @@ +import type {View as RNView} from 'react-native'; + jest.mock('react-native-webview', () => { const {View} = require('react-native'); return { - WebView: () => View, + WebView: () => View as RNView, }; }); diff --git a/android/app/build.gradle b/android/app/build.gradle index c37b427daf63..9886cd5cccec 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 1001044600 + versionName "1.4.46-0" } 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/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index 3d16e607be49..8eff32dedf76 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -15,6 +15,8 @@ import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; +import android.media.AudioAttributes; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.service.notification.StatusBarNotification; @@ -31,6 +33,7 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.versionedparcelable.ParcelUtils; +import com.expensify.chat.R; import com.urbanairship.AirshipConfigOptions; import com.urbanairship.json.JsonMap; import com.urbanairship.json.JsonValue; @@ -105,6 +108,9 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ builder.setChannelId(CHANNEL_MESSAGES_ID); } else { builder.setPriority(PRIORITY_MAX); + // Set sound for versions below Oreo + // for Oreo and above we set sound on the notification's channel level + builder.setSound(getSoundFile(context)); } // Attempt to parse data and apply custom notification styling @@ -130,6 +136,13 @@ private void createAndRegisterNotificationChannel(@NonNull Context context) { NotificationChannelGroup channelGroup = new NotificationChannelGroup(NOTIFICATION_GROUP_CHATS, CHANNEL_GROUP_NAME); NotificationChannel channel = new NotificationChannel(CHANNEL_MESSAGES_ID, CHANNEL_MESSAGES_NAME, NotificationManager.IMPORTANCE_HIGH); + AudioAttributes audioAttributes = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build(); + + channel.setSound(getSoundFile(context), audioAttributes); + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannelGroup(channelGroup); notificationManager.createNotificationChannel(channel); @@ -333,4 +346,8 @@ private Bitmap fetchIcon(@NonNull Context context, String urlString) { return null; } + + private Uri getSoundFile(Context context) { + return Uri.parse("android.resource://" + context.getPackageName() + "/" + R.raw.receive); + } } 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/assets/images/chatbubble-add.svg b/assets/images/chatbubble-add.svg index 48eebf863cc3..3e2c270d681c 100644 --- a/assets/images/chatbubble-add.svg +++ b/assets/images/chatbubble-add.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + diff --git a/assets/images/chatbubble-reply.svg b/assets/images/chatbubble-reply.svg new file mode 100644 index 000000000000..f8d53ebff807 --- /dev/null +++ b/assets/images/chatbubble-reply.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/assets/images/chatbubble-unread.svg b/assets/images/chatbubble-unread.svg index 492616cf2ab5..f5a27a74f5fe 100644 --- a/assets/images/chatbubble-unread.svg +++ b/assets/images/chatbubble-unread.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + diff --git a/assets/images/folder.svg b/assets/images/folder.svg new file mode 100644 index 000000000000..17cef959132f --- /dev/null +++ b/assets/images/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/product-illustrations/emptystate__expenses.svg b/assets/images/product-illustrations/emptystate__expenses.svg new file mode 100644 index 000000000000..c01a89109cbf --- /dev/null +++ b/assets/images/product-illustrations/emptystate__expenses.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/product-illustrations/mushroom-top-hat.svg b/assets/images/product-illustrations/mushroom-top-hat.svg new file mode 100644 index 000000000000..cb808f7289e0 --- /dev/null +++ b/assets/images/product-illustrations/mushroom-top-hat.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/product-illustrations/three_legged_laptop_woman.svg b/assets/images/product-illustrations/three_legged_laptop_woman.svg new file mode 100644 index 000000000000..6be000b92e37 --- /dev/null +++ b/assets/images/product-illustrations/three_legged_laptop_woman.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__approval.svg b/assets/images/simple-illustrations/simple-illustration__approval.svg new file mode 100644 index 000000000000..bdef2436958b --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__approval.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__folder-open.svg b/assets/images/simple-illustrations/simple-illustration__folder-open.svg new file mode 100644 index 000000000000..c104313a9b6c --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__folder-open.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg b/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg new file mode 100644 index 000000000000..fc7082e9932c --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg @@ -0,0 +1 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg b/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg new file mode 100644 index 000000000000..33d1fc0fa044 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__workflows.svg b/assets/images/simple-illustrations/simple-illustration__workflows.svg new file mode 100644 index 000000000000..47d30d54310f --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__workflows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/workflows.svg b/assets/images/workflows.svg new file mode 100644 index 000000000000..24156c66eb69 --- /dev/null +++ b/assets/images/workflows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/workspace-profile-light.png b/assets/images/workspace-profile-light.png new file mode 100644 index 000000000000..7e82c98656d2 Binary files /dev/null and b/assets/images/workspace-profile-light.png differ diff --git a/assets/images/workspace-profile.png b/assets/images/workspace-profile.png index 72112566e35f..df1f6f9fd645 100644 Binary files a/assets/images/workspace-profile.png and b/assets/images/workspace-profile.png differ diff --git a/babel.config.js b/babel.config.js index d3bcecdae8cb..2a09d086dc5c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -68,6 +68,7 @@ const metro = { // This path is provide alias for files like `ONYXKEYS` and `CONST`. '@src': './src', '@userActions': './src/libs/actions', + '@desktop': './desktop', }, }, ], diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 209a4c0c2753..170198987793 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -219,6 +219,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ // This path is provide alias for files like `ONYXKEYS` and `CONST`. '@src': path.resolve(__dirname, '../../src/'), '@userActions': path.resolve(__dirname, '../../src/libs/actions/'), + '@desktop': path.resolve(__dirname, '../../desktop'), }, // React Native libraries may have web-specific module implementations that appear with the extension `.web.js` 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/billing-and-subscriptions/Billing-Overview.md b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md index 09dd4de2867b..3fe5ec41f5f6 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md @@ -3,7 +3,7 @@ title: Billing Overview description: An overview of how billing works in Expensify. --- # Overview -Expensify’s billing is based on monthly member activity. At the beginning of each month, you’ll be billed for the previous month’s activity. Your Expensify bill ultimately depends on your plan type, whether you're on an annual subscription or pay-per-use billing, and whether you’re using Expensify Cards. +Expensify’s billing is based on monthly member activity. At the beginning of each month, you’ll be billed for the previous month’s activity. Your Expensify bill ultimately depends on your plan type, whether you're on an annual subscription or pay-per-use billing, and whether you’re using the Expensify Visa® Commercial Card. # How billing works in Expensify Expensify bills the owners of Group Workspaces on the first of each month for the previous month's usage. You can find billing receipts in **Settings > Account > Payments > Billing History**. We recommend that businesses have one billing owner for all of their Group Workspaces. ## Active members @@ -23,7 +23,7 @@ Bundling the Expensify Card with an annual subscription ensures you pay the lowe If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per member. This additional 50% discount, when coupled with an annual subscription, brings the price per member to $5 on a Collect plan and $9 on a Control plan. -Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more. Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. +Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more (_applies to USD purchases only_). Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. ## Savings calculator To see how much money you can save (and even earn!) by using the Expensify Card, check out our [savings calculator](https://use.expensify.com/price-savings-calculator). Just enter a few details and see how much you’ll save! {% include faq-begin.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md index 16e628acbeee..043cc4be1e26 100644 --- a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md @@ -81,7 +81,7 @@ Follow the below steps to run reconciliation on the Expensify Card settlements: - Entry ID: a unique number grouping card payments and transactions settled by those payments - Amount: the amount debited from the Business Bank Account for payments - Merchant: the business where a purchase was made - - Card: refers to the Expensify credit card number and cardholder's email address + - Card: refers to the Expensify Card number and cardholder's email address - Business Account: the business bank account connected to Expensify that the settlement is paid from - Transaction ID: a special ID that helps Expensify support locate transactions if there's an issue diff --git a/docs/articles/expensify-classic/getting-started/Create-a-company-workspace.md b/docs/articles/expensify-classic/getting-started/Create-a-company-workspace.md new file mode 100644 index 000000000000..4cc95cdcf918 --- /dev/null +++ b/docs/articles/expensify-classic/getting-started/Create-a-company-workspace.md @@ -0,0 +1,171 @@ +--- +title: Create a company workspace +description: Get started with Expensify by creating a workspace for your company +--- +
+ +# Overview + +Welcome to Expensify! If you are creating an Expensify account for your company, follow the steps below to get started. + +{% include info.html %} +You can also schedule a free private onboarding session where one of our Setup Specialists will walk you through the entire process. Check your email and notifications in Expensify for your unique signup link. +{% include end-info.html %} + +# 1. Meet Concierge + +Your personal assistant, Concierge, lives on your Expensify Home page on both desktop and the mobile app. + +Concierge will walk you through setting up your account and also provide: +- Reminders to do things like submit your expenses +- Alerts when more information is needed on an expense report +- Updates on new and improved account features + +You can also get support at any time by clicking the green chat bubble in the right corner. This will open a chat with Concierge where you can ask questions and receive direct support. + +# 2. Create a workspace + +
    +
  1. Hover over Settings, then click Workspaces.
  2. +
  3. Click the Group tab on the left.
  4. +
  5. Enter a name for the workspace (the name of your business or department is a great choice, if applicable), then click Select next to the workspace type that best fits your needs.
  6. +
+ +# 3. Add a business bank account + +Connecting your business bank account allows you to: +- Reimburse expenses via direct bank transfer +- Pay bills +- Collect invoice payments +- Issue Expensify Cards + +{% include info.html %} +The person who completes this process does not need to be a signer on the account, however they will be required to enter their own personal information as well. If someone other than the Expensify account holder enters their personal information in this section, the details will be flagged for review, and adding the bank account to Expensify will be delayed. +{% include end-info.html %} + +To add a business bank account, + +
    +
  1. Hover over Settings, then click Workspaces.
  2. +
  3. Click the Group tab on the left.
  4. +
  5. Click the desired workspace name.
  6. +
  7. Click the Reimbursement tab.
  8. +
  9. Click the Direct box, then click Add Business Bank Account.
  10. +
  11. Click Connect to your bank.
  12. +
  13. Click Continue to continue to Plaid.
  14. +
  15. Select the bank and log in to your business bank account.
  16. +
      +
    • If the bank is not listed, close the Plaid window and select Connect Manually to enter your account and routing numbers.
    • +
    +
  17. Select the bank account if multiple are available.
  18. +
  19. Verify the bank account by entering some additional information:
  20. +
      +
    • Enter the legal business name.
    • +
    • Enter the company address (Must be a physical location in the U.S. Maildrop address, P.O. boxes, or UPS Store addresses are flagged for review and will create a delay verifying the bank account).
    • +
    • Enter the Tax Identification Number (TIN)
    • +
    • Enter the company website, formatted like https://www.expensify.com
    • +
    • Enter the Industry Classification Code. You can locate a list of Industry Classification Codes here.
    • +
    • Enter your personal information into the Requestor Information section, including your physical U.S. address and SSN issued from the U.S.
    • +
    • Upload photos of your ID. It must be issued by the U.S. and be current (i.e., the expiration date must be in the future).
    • +
    • Take a short video of yourself to verify your identity.
    • +
    • Check the appropriate box under Additional Information to accept the agreement terms and verify that all of the information is true and accurate.
    • +
        +
      • A Beneficial Owner is an individual who owns 25% or more of the business. If you or someone else is a Beneficial Owner, check the appropriate box. If someone else is a Beneficial Owner, their personal information will need to be provided as well.
      • +
      • If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section.
      • +
      +
    +
  21. Within 1-2 business days, Expensify will send three test transactions to your bank account that you’ll enter into Expensify to validate your bank account by either:
  22. +
      +
    • Clicking the validate task from Concierge on your Home page.
    • +
    • Going to Settings > Account > Payments and clicking Enter test transactions.
    • +
    +
+ +{% include info.html %} +If after two business days you do not see these test transactions, click the green chat bubble in the right corner to get support from Concierge. +{% include end-info.html %} + +# 4. Connect your accounting system + +If you use an external accounting system like QuickBooks, you can link it with Expensify to help you import accounting data, code expenses, and more. + +To add an accounting system integration, +
    +
  1. Hover over Settings, then click Workspaces.
  2. +
  3. Click the Group tab on the left.
  4. +
  5. Click the desired workspace name.
  6. +
  7. Click the Connections tab.
  8. +
  9. Under Accounting Integrations, click the name of your accounting system, select the “Connect to…” option, and click the related button.
  10. +
  11. Depending on the integration you selected, you’ll either be prompted with a login screen for the accounting system or additional steps for how to proceed.
  12. +
+ +For a walkthrough for how to set up a specific accounting system, visit our [Integrations](https://help.expensify.com/expensify-classic/hubs/integrations/) articles. + +# 5. Set approval rules + +Determine the basic guidelines that apply to all submitted expenses. If a submitted expense does not meet these rules, it will be flagged as a violation. You can set rules for expenses, per diem, travel, and reports. + +
    +
  1. Hover over Settings, then click Workspaces.
  2. +
  3. Click the Group tab on the left.
  4. +
  5. Click the desired workspace name.
  6. +
  7. Click the Expenses tab and set the desired rules.
  8. +
      +
    • Determine if expense violations will be enforced. If enabled, expenses that exceed the set number of days or amount will be flagged as an expense violation.
    • +
    • Determine how cash expenses are treated.
    • +
    • Determine if expenses can be re-billed to someone else as an invoice.
    • +
    • Determine if eRecipts can be submitted as proof of an expense.
    • + +{% include info.html %} +If eReceipts are enabled, imported credit card expenses of $75 USD or less will be tracked automatically—no paper receipt is necessary. + +eReceipts meet IRS documentation requirements as per Publication 463; However, the IRS will not accept an eReceipt for lodging purchases (for example, hotel expenses will require a paper receipt). +{% include end-info.html %} + +
    • Determine if receipts are visible to anyone with the URL.
    • + +{% include info.html %} +If disabled, receipts can be seen only by admins for the workspace or someone who has been sent the report that the receipts are related to. +{% include end-info.html %} + +
    • Set your mileage rates for distance expenses.
    • +
    • Determine if time expenses can be submitted as an hourly rate.
    • +
    +
  9. Click the Reports tab and set the desired rules.
  10. +
      +
    • Set the currency that will be used for all reports.
    • +
    • Determine if Schedule Submit will be allowed. If enabled, all created expenses will be automatically assigned to a report. Concierge will then submit expenses for approval on the employee's behalf instantly either daily, weekly, etc., based on your frequency setting.
    • +
    • Determine if a default report title will be required. If enabled, all reports will be named based on the set formula.
    • +
    • Determine if additional fields should be added to each report or invoice.
    • +
    +
  11. Click the Travel tab and set the desired rules.
  12. +
      +
    • Determine what flight class and hotel rating Concierge should select when booking travel. Concierge can automatically book flights and hotels for your employees if they have an Expensify card.
    • +
    +
  13. Click the Per Diem tab and set the desired rules.
  14. +
      +
    • Determine if per diem expenses will be allowed. Then import your per diem expense rules spreadsheet that contains the list of location-based expense amounts (for example, an employee in California might receive a different amount for lunch than an employee in Louisiana).
    • +
    +
+ +# 6. Secure your account + +Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication. This will require you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) when you log in. + +
    +
  1. Hover over Settings, then click Account.
  2. +
  3. Under the Account Details tab, scroll down to the Two Factor Authentication section and enable the toggle.
  4. +
  5. Save a copy of your backup codes. This step is critical—You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes.
  6. +
      +
    • Click Download to save a copy of your backup codes to your computer.
    • +
    • Click Copy to paste the codes into a document or other secure location.
    • +
    +
  7. Click Continue.
  8. +
  9. Download or open your authenticator app and either:
  10. +
      +
    • Scan the QR code shown on your computer screen.
    • +
    • Enter the 6-digit code from your authenticator app into Expensify and click Verify.
    • +
    +
+ +When you log in to Expensify in the future, you’ll be emailed a magic code that you’ll use to log in with. Then you’ll be prompted to open your authenticator app to get the 6-digit code and enter it into Expensify. A new code regenerates every few seconds, so the code is always different. If the code time runs out, you can generate a new code as needed. 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/getting-started/Invite-Members.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members.md similarity index 100% rename from docs/articles/expensify-classic/getting-started/Invite-Members.md rename to docs/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members.md diff --git a/docs/articles/expensify-classic/settings/Account-Details.md b/docs/articles/expensify-classic/settings/Account-Details.md deleted file mode 100644 index 535e74eeb701..000000000000 --- a/docs/articles/expensify-classic/settings/Account-Details.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Account Details -description: The Account Details section of your account is where you can update your profile photo, enable 2FA, and change the email address associated with your Expensify account. ---- - -# Overview -The Account Details section of your account is where you can update your profile photo, enable 2FA, and change the email address associated with your account. - -You can have multiple email addresses tied to your account to make it easier to submit expenses or manage your account. Let’s go over how to configure the various account settings located under the Account Details section of your Expensify account. - -# How to add a profile photo -To update your name or add a profile photo, navigate to **Settings** > **Account** > **Account Details.** Under “your profile” you’ll notice a profile picture thumbnail, click “edit photo” underneath to update the profile image. - -# How to enable Two-Factor Authentication -Setting up Two-factor Authentication is one of the best ways to secure your account. This can be enabled individually in your account settings by following **Settings** > **Accounts** > **Account Details** > **Two Factor Authentication** and toggle the switch to **Enabled.** - -Save or download your **Recovery Codes.** It’s important to keep these safe! You WILL lose access to your account if you cannot use your authenticator app and do not have your recovery codes. - -Use your favorite authenticator app to connect to Expensify using the QR code or click the link to enter the secret key manually. - -Once connected, quickly enter the code generated by your app into Expensify before the timeframe runs out! - -# How to manage your devices -You can access your Expensify account on multiple devices, which allows for easy access to your account data. By heading to **Settings** > **Account** > **Account Details** > **Device Management**, you can review the devices that have access to your account. - -From that same place in your account, you can remove any devices that should no longer have access. To do this, select the **Revoke** button next to each device you wish to remove access to your account. - -# How to add a Secondary Login -A Secondary Login is helpful if you have multiple email addresses and don’t necessarily need multiple Expensify accounts. By adding additional emails to your Expensify account, you can use them to forward receipts to receipts@expensify.com and they will be uploaded to your main Expensify account. To get this added to your account, follow these steps: - -1. Log in to your Expensify account through a web browser at www.expensify.com. Please note that this process cannot be completed using the mobile app; it must be done from the website at expensify.com. -2. Navigate to **Settings** > **Account** > **Account Details**. Scroll down to find the 'Secondary Logins' section, then click the 'Add Secondary Login' button. -3. Input the email address or mobile phone number you wish to add, ensuring you include the international code if applicable. -4. You will receive a prompt to enter the Magic Code, which will be sent to the email address you're adding as a secondary login. - -# How to update your email address -Once a Secondary Login is added to your account, you can make it your primary email address. The primary address on an Expensify account is the address that will receive email notifications and updates regarding the account. Any new email addresses must be added as a secondary login before they can be made a primary address. - -1. Log in to your Expensify account through a web browser at www.expensify.com. Please note that this process cannot be completed using the mobile app; it must be done from the website at expensify.com. -2. Navigate to **Settings** > **Account** > **Account Details**. Scroll down to find the 'Secondary Logins' section, then select the **"Make Primary"** button next to the email address. -3. You can keep the old address as a secondary login or delete email addresses by selecting the **"Remove"** button. - - -# Deep Dive -## Managing emails connected to other Expensify accounts -A secondary login can only be added if it is not linked to an existing account. If you have two email addresses with Expensify accounts linked to them, you'll need to merge them instead. - -Alternatively, you can remove a personal email address from a previous work/organization account to use it elsewhere. - -Is your Secondary Login (personal email) validated in your company account? If so, do the following: -1. Navigate to expensify.com -2. Log in using your validated Secondary Login -3. Navigate to **Account** > **Settings** > **Account Details** > **Secondary Logins** -4. Remove your personal email address from the account by clicking the **"Remove"** button next to your email - -Is your Secondary Login (personal email) invalidated in your company account? If so, do the following: -1. Navigate to expensify.com -2. Enter your invalidated secondary login email address -3. You will be presented with a confirmation message saying Expensify sent you an email with a validation link -4. Head to your personal email account and follow the prompts -5. You'll receive a link in the email to click that will unlink the two accounts - -{% include faq-begin.md %} -## The profile picture on my account updated automatically. Why did this happen? -Our focus is always on making your experience user-friendly and saving you valuable time. One of the ways we achieve this is by utilizing a public API to retrieve public data linked to your email address. - -This tool searches for public accounts or profiles associated with your email address, such as on LinkedIn. When it identifies one, it pulls in the uploaded profile picture and name to Expensify. - -While this automated process is generally accurate, there may be instances where it's not entirely correct. If this happens, we apologize for any inconvenience caused. The good news is that rectifying such situations is a straightforward process. You can quickly update your information manually by following the directions provided above, ensuring your data is accurate and up to date in no time. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/settings/Merge-Accounts.md b/docs/articles/expensify-classic/settings/Merge-Accounts.md deleted file mode 100644 index 34bf422aa983..000000000000 --- a/docs/articles/expensify-classic/settings/Merge-Accounts.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Merge Accounts -description: How to merge two Expensify accounts and why this is useful. ---- - -# Overview - -Merging accounts allows you to combine two accounts. When you combine two accounts, all receipts, expenses, expense reports, invoices, bills, imported cards, secondary logins, co-pilots, and group workspace settings will be combined into one account. -This can be useful if you start off with an account of your own but your organization creates a separate account for you. You can then track both personal and business expenses via one account. - -# How to merge accounts -Merging two accounts together is fairly straightforward. Let’s go over how to do that below: -1. Navigate to [expensify.com](https://www.expensify.com) -2. Log into the account you want to set as the Primary account -3. Navigate to **Settings > Account > Account Details** -4. Scroll down to Merge Accounts and fill in the fields -6. Click Merge Accounts -7. Once you click Merge, a magic code is sent to you via email -8. Paste the code into the required field -If you have any questions about this process, feel free to reach out to Concierge for some assistance! - -{% include faq-begin.md %} -## Can you merge accounts from the mobile app? -No, accounts can only be merged from the full website at expensify.com. -## Can I administratively merge two accounts together? -No, only the account holder (member) can perform account merging. -## Is merging accounts reversible? -No, merging accounts is not reversible. It is a permanent action that cannot be undone. -## I have open expenses in the account I'm merging from. Will those expenses merge into the new account? -All expenses must be reported and submitted for them to merge into the new account. Any open expenses will not merge. -## Are there any restrictions on account merging? -Yes! Please see below: -- If your email address belongs to a verified domain (verified in Expensify), you must start the process from the email account under the verified domain. You cannot merge a verified company email account into a personal account. -- If you have two accounts with two different verified domains, you cannot merge them together. -## What happens to my “personal” Individual workspace when merging accounts? -The old “personal” Individual workspace is deleted. If you plan to submit reports under a different workspace in the future, ensure that any reports on the Individual workspace in the old account are marked as Open before merging the accounts. You can typically do this by selecting “Undo Submit” on any submitted reports. - -{% include faq-end.md %} 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/docs/redirects.csv b/docs/redirects.csv index 76b7bac3fc99..4ed309467f13 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -25,16 +25,16 @@ https://community.expensify.com/discussion/5194/how-to-assign-a-vacation-delegat https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vacation-delegate-from-account-settings,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate https://community.expensify.com/discussion/5274/how-to-set-up-an-adp-indirect-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/get-paid-back/Distance-Tracking -https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details -https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details -https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details -https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details -https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Perks.html,https://use.expensify.com/company-credit-card https://help.expensify.com/articles/expensify-classic/expensify-partner-program/How-to-Join-the-ExpensifyApproved!-Partner-Program.html,https://use.expensify.com/accountants-program https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners, https://use.expensify.com/blog/maximizing-rewards-expensifyapproved-accounting-partners-now-earn-0-5-revenue-share https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursements -https://community.expensify.com/discussion/4452/how-to-merge-accounts,https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts +https://community.expensify.com/discussion/4452/how-to-merge-accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://community.expensify.com/discussion/4783/how-to-add-or-remove-a-copilot,https://help.expensify.com/articles/expensify-classic/account-settings/Copilot https://community.expensify.com/discussion/4343/expensify-anz-partnership-announcement,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ https://community.expensify.com/discussion/7318/deep-dive-company-credit-card-import-options,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards @@ -54,3 +54,9 @@ https://help.expensify.com/articles/expensify-classic/getting-started/Employees, https://help.expensify.com/articles/expensify-classic/getting-started/Using-The-App,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace https://help.expensify.com/articles/expensify-classic/getting-started/support/Expensify-Support,https://use.expensify.com/support https://help.expensify.com/articles/expensify-classic/getting-started/Plan-Types,https://use.expensify.com/ +https://help.expensify.com/articles/new-expensify/payments/Referral-Program,https://help.expensify.com/articles/expensify-classic/get-paid-back/Referral-Program +https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://help.expensify.com/articles/expensify-classic/account-settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts +https://help.expensify.com/articles/expensify-classic/getting-started/Individual-Users,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself +https://help.expensify.com/articles/expensify-classic/getting-started/Invite-Members,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 60ccc41d056d..dff05f61933e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.42 + 1.4.46 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.42.2 + 1.4.46.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3308fbfa510a..fa6995f65b5a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.42 + 1.4.46 CFBundleSignature ???? CFBundleVersion - 1.4.42.2 + 1.4.46.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 5773eef2dba2..e8cd0ebb4e0a 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.42 + 1.4.46 CFBundleVersion - 1.4.42.2 + 1.4.46.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/NotificationServiceExtension/NotificationService.swift b/ios/NotificationServiceExtension/NotificationService.swift index c4eb01981bf2..e489cb368d17 100644 --- a/ios/NotificationServiceExtension/NotificationService.swift +++ b/ios/NotificationServiceExtension/NotificationService.swift @@ -24,6 +24,8 @@ class NotificationService: UANotificationServiceExtension { return } + bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName("receive.mp3")) + if #available(iOSApplicationExtension 15.0, *) { configureCommunicationNotification(notificationContent: bestAttemptContent, contentHandler: contentHandler) } else { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 80933065c450..12c0c99c0d9a 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) @@ -1163,9 +1163,7 @@ PODS: - React-Core - react-native-blob-util (0.19.4): - React-Core - - react-native-cameraroll (7.4.0): - - glog - - RCT-Folly (= 2022.05.16.00) + - react-native-cameraroll (5.4.0): - React-Core - react-native-config (1.4.6): - react-native-config/App (= 1.4.6) @@ -1434,7 +1432,7 @@ PODS: - React-Core - RNReactNativeHapticFeedback (2.2.0): - React-Core - - RNReanimated (3.6.1): + - RNReanimated (3.7.1): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1903,8 +1901,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 @@ -1931,7 +1929,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 9ee041e1d7be96da6d76a251f92e72b711c651d6 react-native-airship: 6ded22e4ca54f2f80db80b7b911c2b9b696d9335 react-native-blob-util: 30a6c9fd067aadf9177e61a998f2c7efb670598d - react-native-cameraroll: 3301d62d45616ee9da55ceed04be8d788c3de3ef + react-native-cameraroll: 8ffb0af7a5e5de225fd667610e2979fc1f0c2151 react-native-config: 7cd105e71d903104e8919261480858940a6b9c0e react-native-document-picker: 3599b238843369026201d2ef466df53f77ae0452 react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903 @@ -1988,7 +1986,7 @@ SPEC CHECKSUMS: rnmapbox-maps: fcf7f1cbdc8bd7569c267d07284e8a5c7bee06ed RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 - RNReanimated: 57f436e7aa3d277fbfed05e003230b43428157c0 + RNReanimated: beb07f7f900543928467da8107c175d1e57a1049 RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a diff --git a/jest.config.js b/jest.config.js index 95ecc350ed9f..441507af4228 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { `/?(*.)+(spec|test).${testFileExtension}`, ], transform: { - '^.+\\.jsx?$': 'babel-jest', + '^.+\\.[jt]sx?$': 'babel-jest', '^.+\\.svg?$': 'jest-transformer-svg', }, transformIgnorePatterns: ['/node_modules/(?!react-native)/'], diff --git a/jest/setup.ts b/jest/setup.ts index 4a23a85edb83..11b0d77ed7ac 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -31,7 +31,12 @@ jest.spyOn(console, 'debug').mockImplementation((...params) => { // This mock is required for mocking file systems when running tests jest.mock('react-native-fs', () => ({ - unlink: jest.fn(() => new Promise((res) => res())), + unlink: jest.fn( + () => + new Promise((res) => { + res(); + }), + ), CachesDirectoryPath: jest.fn(), })); diff --git a/package-lock.json b/package-lock.json index c94923a81699..5f55ddd82868 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.42-2", + "version": "1.4.46-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.42-2", + "version": "1.4.46-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -24,9 +24,9 @@ "@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-camera-roll/camera-roll": "5.4.0", "@react-native-clipboard/clipboard": "^1.13.2", "@react-native-community/geolocation": "^3.0.6", "@react-native-community/netinfo": "11.2.1", @@ -97,7 +97,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.2", + "react-native-onyx": "2.0.7", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -106,7 +106,7 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "^3.6.1", + "react-native-reanimated": "^3.7.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", @@ -206,13 +206,12 @@ "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.43", + "eslint-config-expensify": "^2.0.44", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", "eslint-plugin-jsdoc": "^46.2.6", "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.5.13", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", @@ -236,7 +235,7 @@ "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", "ts-node": "^10.9.2", - "type-fest": "^3.12.0", + "type-fest": "^4.10.2", "typescript": "^5.3.2", "wait-port": "^0.2.9", "webpack": "^5.76.0", @@ -450,6 +449,35 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/eslint-parser": { + "version": "7.23.10", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.10.tgz", + "integrity": "sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==", + "dev": true, + "peer": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", @@ -7324,20 +7352,71 @@ "license": "MIT" }, "node_modules/@lwc/eslint-plugin-lwc": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@lwc/eslint-plugin-lwc/-/eslint-plugin-lwc-0.11.0.tgz", - "integrity": "sha512-wJOD4XWOH91GaZfypMSKfEeMXqMfvKdsb2gSJ/9FEwJVlziKg1aagtRYJh2ln3DyEZV33tBC/p/dWzIeiwa1tg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@lwc/eslint-plugin-lwc/-/eslint-plugin-lwc-1.7.2.tgz", + "integrity": "sha512-fvdW/yvkNfqgt2Cc4EJCRYE55QJVNXdDaVTHRk5i1kkKP2Xj3GG0nAsYwXYqApEeRpUTpUZljPlO29/SWRXJoA==", "dev": true, - "license": "MIT", "dependencies": { - "minimatch": "^3.0.4" + "globals": "^13.24.0", + "minimatch": "^9.0.3" }, "engines": { "node": ">=10.0.0" }, "peerDependencies": { - "babel-eslint": "^10", - "eslint": "^6 || ^7" + "@babel/eslint-parser": "^7", + "eslint": "^7 || ^8" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@malept/cross-spawn-promise": { @@ -7689,6 +7768,16 @@ "uuid": "8.3.2" } }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "peer": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8113,12 +8202,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": { @@ -8132,9 +8227,9 @@ } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", - "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz", + "integrity": "sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ==", "dev": true, "dependencies": { "ansi-html-community": "^0.0.8", @@ -8154,7 +8249,7 @@ "@types/webpack": "4.x || 5.x", "react-refresh": ">=0.10.0 <1.0.0", "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <4.0.0", + "type-fest": ">=0.17.0 <5.0.0", "webpack": ">=4.43.0 <6.0.0", "webpack-dev-server": "3.x || 4.x", "webpack-hot-middleware": "2.x", @@ -8730,12 +8825,10 @@ } }, "node_modules/@react-native-camera-roll/camera-roll": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-7.4.0.tgz", - "integrity": "sha512-y0bVpMJLaFphYvMMx1BsqgMA0kXq9CKxKYNnt4ocUvwJj5Rp4TZ233rzJoDqz1oxd56Tz5f1g+yhYN5RImKl8Q==", - "engines": { - "node": ">= 18.17.0" - }, + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-5.4.0.tgz", + "integrity": "sha512-SMEhc+2hQWubwzxR6Zac0CmrJ2rdoHHBo0ibG2iNMsxR0dnU5AdRGnYF/tyK9i20/i7ZNxn+qsEJ69shpkd6gg==", + "license": "MIT", "peerDependencies": { "react-native": ">=0.59" } @@ -22852,24 +22945,6 @@ "node": ">=10" } }, - "node_modules/aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha512-majUxHgLehQTeSA+hClx+DY09OVUqG3GtezWkF1krgLGNdlDu9l9V8DaqNMWbq4Eddc8wsyDA0hpDUtnYxQEXw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, - "node_modules/aria-query/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -23330,13 +23405,6 @@ "node": ">=4" } }, - "node_modules/axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -25620,13 +25688,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true, - "license": "MIT" - }, "node_modules/charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", @@ -25859,9 +25920,9 @@ } }, "node_modules/classnames": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.4.0.tgz", - "integrity": "sha512-lWxiIlphgAhTLN657pwU/ofFxsUTOWc2CRIFeoV5st0MGRJHStUnWIUJgDHxjUO/F0mXzGufXIM4Lfu/8h+MpA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz", + "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA==" }, "node_modules/clean-css": { "version": "5.3.2", @@ -25967,16 +26028,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 10" - } - }, "node_modules/clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -26847,10 +26898,11 @@ } }, "node_modules/core-js-pure": { - "version": "3.24.1", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.36.0.tgz", + "integrity": "sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ==", "dev": true, "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -29485,6 +29537,27 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, + "engines": { + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" + } + }, "node_modules/eslint-config-airbnb-base": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", @@ -29529,579 +29602,24 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.43", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.43.tgz", - "integrity": "sha512-kLd6NyYbyb3mCB6VH6vu49/RllwNo0rdXcLUUGB7JGny+2N19jOmBJ4/GLKsbpFzvEZEghXfn7BITPRkxVJcgg==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.44.tgz", + "integrity": "sha512-fwa7lcQk7llYgqcWA1TX4kcSigYqSVkKGk+anODwYlYSbVbXwzzkQsncsaiWVTM7+eJdk46GmWPeiMAWOGWPvw==", "dev": true, "dependencies": { - "@lwc/eslint-plugin-lwc": "^0.11.0", + "@lwc/eslint-plugin-lwc": "^1.7.2", "babel-eslint": "^10.1.0", - "eslint": "6.8.0", - "eslint-config-airbnb": "18.0.1", - "eslint-config-airbnb-base": "14.0.0", + "eslint": "^7.32.0", + "eslint-config-airbnb": "19.0.4", + "eslint-config-airbnb-base": "15.0.0", "eslint-plugin-es": "^4.1.0", "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.18.0", - "eslint-plugin-rulesdir": "^0.2.0", - "lodash": "^4.17.21", - "underscore": "^1.13.1" - } - }, - "node_modules/eslint-config-expensify/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint-config-expensify/node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/eslint-config-expensify/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/eslint-config-expensify/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-config-expensify/node_modules/eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-config-airbnb": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", - "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-config-airbnb-base": "^14.0.0", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2", "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^1.7.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-config-airbnb-base": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", - "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "confusing-browser-globals": "^1.0.7", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.0.tgz", - "integrity": "sha512-p+PGoGeV4SaZRDsXqdj9OWcOrOpZn8gXoGPcIQTzo2IDMbAKhNDnME9myZWqO3Ic4R3YmwAZ1lDjWl2R2hMUVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.1", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.3", - "object.entries": "^1.1.1", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "resolve": "^1.14.2" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react-hooks": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", - "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=7" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true, - "license": "ISC" - }, - "node_modules/eslint-config-expensify/node_modules/globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-config-expensify/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint-config-expensify/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-config-expensify/node_modules/jsx-ast-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", - "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.1", - "object.assign": "^4.1.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.5.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-config-expensify/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "eslint-plugin-react": "^7.18.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-rulesdir": "^0.2.2", + "lodash": "^4.17.21", + "underscore": "^1.13.6" } }, "node_modules/eslint-config-prettier": { @@ -30562,9 +30080,10 @@ } }, "node_modules/eslint-plugin-rulesdir": { - "version": "0.2.1", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.2.tgz", + "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -31243,7 +30762,7 @@ "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==", "license": "MIT", "dependencies": { - "classnames": "2.4.0", + "classnames": "2.5.0", "clipboard": "2.0.11", "html-entities": "^2.4.0", "jquery": "3.6.0", @@ -31252,7 +30771,7 @@ "prop-types": "15.8.1", "react": "16.12.0", "react-dom": "16.12.0", - "semver": "^7.5.2", + "semver": "^7.6.0", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", "ua-parser-js": "^1.0.37", "underscore": "1.13.6" @@ -31726,47 +31245,6 @@ "node": ">=0.10.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -32049,32 +31527,6 @@ "dev": true, "license": "ISC" }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -34649,107 +34101,6 @@ "css-in-js-utils": "^2.0.0" } }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -38599,6 +37950,17 @@ "node": ">=8" } }, + "node_modules/jest-watch-typeahead/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-watcher": { "version": "29.4.1", "license": "MIT", @@ -38888,6 +38250,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", @@ -41644,13 +41011,6 @@ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" - }, "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -45130,9 +44490,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.2.tgz", - "integrity": "sha512-24kcG3ChBXp+uSSCXudFvZTdCnKLRHQRgvTcnh2eA7COtKvbL8ggEJNkglSYmcf5WoDzLgYyWiKvcjcXQnmBvw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.7.tgz", + "integrity": "sha512-UGMUTSFxYEzNn3wuCGzaf0t6D5XwcE+3J2pYj7wPlbskdcHVLijZZEwgSSDBF7hgNfCuZ+ImetskPNktnf9hkg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -45270,9 +44630,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz", - "integrity": "sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.7.1.tgz", + "integrity": "sha512-bapCxhnS58+GZynQmA/f5U8vRlmhXlI/WhYg0dqnNAGXHNIc+38ahRWcG8iK8e0R2v9M8Ky2ZWObEC6bmweofg==", "dependencies": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", @@ -47387,16 +46747,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", @@ -47650,9 +47000,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -50821,11 +50171,12 @@ } }, "node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.3.tgz", + "integrity": "sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==", + "dev": true, "engines": { - "node": ">=14.16" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -53201,19 +52552,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/write-file-atomic": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", @@ -53225,19 +52563,6 @@ "signal-exit": "^3.0.2" } }, - "node_modules/write/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ws": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", diff --git a/package.json b/package.json index 825f5b5a9908..e3c23d4538d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.42-2", + "version": "1.4.46-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -12,7 +12,7 @@ "startAndroidEmulator": "scripts/start-android.sh", "postinstall": "scripts/postInstall.sh", "clean": "npx react-native clean-project-auto", - "android": "scripts/set-pusher-suffix.sh && npx react-native run-android --mode=developmentDebug --appId=com.expensify.chat.dev", + "android": "scripts/set-pusher-suffix.sh && npx react-native run-android --mode=developmentDebug --appId=com.expensify.chat.dev --active-arch-only", "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --mode=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"", "pod-install": "cd ios && bundle exec pod install", "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", @@ -50,8 +50,8 @@ "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", - "test:e2e": "ts-node tests/e2e/testRunner.js --development --skipCheckout --skipInstallDeps --buildMode none", - "test:e2e:dev": "ts-node tests/e2e/testRunner.js --development --skipCheckout --config ./config.dev.js --buildMode skip --skipInstallDeps", + "test:e2e": "ts-node tests/e2e/testRunner.js --config ./config.local.ts", + "test:e2e:dev": "ts-node tests/e2e/testRunner.js --config ./config.dev.js", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js", @@ -72,9 +72,9 @@ "@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-camera-roll/camera-roll": "5.4.0", "@react-native-clipboard/clipboard": "^1.13.2", "@react-native-community/geolocation": "^3.0.6", "@react-native-community/netinfo": "11.2.1", @@ -145,7 +145,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.2", + "react-native-onyx": "2.0.7", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -154,7 +154,7 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "^3.6.1", + "react-native-reanimated": "^3.7.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", @@ -254,13 +254,12 @@ "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.43", + "eslint-config-expensify": "^2.0.44", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", "eslint-plugin-jsdoc": "^46.2.6", "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.5.13", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", @@ -284,7 +283,7 @@ "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", "ts-node": "^10.9.2", - "type-fest": "^3.12.0", + "type-fest": "^4.10.2", "typescript": "^5.3.2", "wait-port": "^0.2.9", "webpack": "^5.76.0", 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/patches/@react-native-camera-roll+camera-roll+7.4.0.patch b/patches/@react-native-camera-roll+camera-roll+5.4.0.patch similarity index 100% rename from patches/@react-native-camera-roll+camera-roll+7.4.0.patch rename to patches/@react-native-camera-roll+camera-roll+5.4.0.patch diff --git a/patches/expo-av+13.10.4.patch b/patches/expo-av+13.10.4.patch new file mode 100644 index 000000000000..c7b1626e233a --- /dev/null +++ b/patches/expo-av+13.10.4.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/expo-av/android/build.gradle b/node_modules/expo-av/android/build.gradle +index 2d68ca6..c3fa3c5 100644 +--- a/node_modules/expo-av/android/build.gradle ++++ b/node_modules/expo-av/android/build.gradle +@@ -7,10 +7,11 @@ apply plugin: 'maven-publish' + group = 'host.exp.exponent' + version = '13.10.4' + ++def REACT_NATIVE_PATH = this.hasProperty('reactNativeProject') ? this.reactNativeProject + '/node_modules/react-native/package.json' : 'react-native/package.json' + def REACT_NATIVE_BUILD_FROM_SOURCE = findProject(":ReactAndroid") != null + def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE + ? findProject(":ReactAndroid").getProjectDir().parent +- : new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).parent ++ : new File(["node", "--print", "require.resolve('${REACT_NATIVE_PATH}')"].execute(null, rootDir).text.trim()).parent + + def reactNativeArchitectures() { + def value = project.getProperties().get("reactNativeArchitectures") diff --git a/patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch similarity index 100% rename from patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch rename to patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch diff --git a/patches/react-native-reanimated+3.6.1.patch b/patches/react-native-reanimated+3.7.1.patch similarity index 100% rename from patches/react-native-reanimated+3.6.1.patch rename to patches/react-native-reanimated+3.7.1.patch diff --git a/src/App.tsx b/src/App.tsx index 9562ea647e25..0e247d5faa53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,6 @@ import {PortalProvider} from '@gorhom/portal'; import React from 'react'; import {LogBox} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; -import Onyx from 'react-native-onyx'; import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; @@ -31,8 +30,6 @@ import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; -import * as Session from './libs/actions/Session'; -import * as Environment from './libs/Environment/Environment'; import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; @@ -42,12 +39,6 @@ type AppProps = { url?: Route; }; -// For easier debugging and development, when we are in web we expose Onyx to the window, so you can more easily set data into Onyx -if (window && Environment.isDevelopment()) { - window.Onyx = Onyx; - window.setSupportToken = Session.setSupportAuthToken; -} - LogBox.ignoreLogs([ // Basically it means that if the app goes in the background and back to foreground on Android, // the timer is lost. Currently Expensify is using a 30 minutes interval to refresh personal details. @@ -92,7 +83,6 @@ function App({url}: AppProps) { - {/* @ts-expect-error TODO: Remove this once Expensify (https://github.com/Expensify/App/issues/25231) is migrated to TypeScript. */} diff --git a/src/CONST.ts b/src/CONST.ts index 5115b7f4cb42..e8fcc20292f7 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, }, }, }, @@ -175,6 +178,7 @@ const CONST = { DATE: { SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss', FNS_FORMAT_STRING: 'yyyy-MM-dd', + FNS_DATE_TIME_FORMAT_STRING: 'yyyy-MM-dd HH:mm:ss', LOCAL_TIME_FORMAT: 'h:mm a', YEAR_MONTH_FORMAT: 'yyyyMM', MONTH_FORMAT: 'MMMM', @@ -892,6 +896,7 @@ const CONST = { DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, + DEFAULT_NETWORK_DATA: {isOffline: false}, FORMS: { LOGIN_FORM: 'LoginForm', VALIDATE_CODE_FORM: 'ValidateCodeForm', @@ -948,6 +953,9 @@ const CONST = { EMOJI_DEFAULT_SKIN_TONE: -1, + // Amount of emojis to render ahead at the end of the update cycle + EMOJI_DRAW_AMOUNT: 250, + INVISIBLE_CODEPOINTS: ['fe0f', '200d', '2066'], UNICODE: { @@ -1441,7 +1449,7 @@ const CONST = { CLOSED: 6, STATE_SUSPENDED: 7, }, - ACTIVE_STATES: [2, 3, 4, 7], + ACTIVE_STATES: cardActiveStates, }, AVATAR_ROW_SIZE: { DEFAULT: 4, @@ -1535,6 +1543,8 @@ const CONST = { PATH_WITHOUT_POLICY_ID: /\/w\/[a-zA-Z0-9]+(\/|$)/, POLICY_ID_FROM_PATH: /\/w\/([a-zA-Z0-9]+)(\/|$)/, + + SHORT_MENTION: new RegExp(`@[\\w\\-\\+\\'#]+(?:\\.[\\w\\-\\'\\+]+)*`, 'gim'), }, PRONOUNS: { @@ -1551,7 +1561,9 @@ const CONST = { WORKSPACE_INVOICES: 'WorkspaceSendInvoices', WORKSPACE_TRAVEL: 'WorkspaceBookTravel', WORKSPACE_MEMBERS: 'WorkspaceManageMembers', + WORKSPACE_WORKFLOWS: 'WorkspaceWorkflows', WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount', + WORKSPACE_SETTINGS: 'WorkspaceSettings', }, get EXPENSIFY_EMAILS() { return [ @@ -3103,6 +3115,8 @@ const CONST = { */ ADDITIONAL_ALLOWED_CHARACTERS: 20, + VALIDATION_REIMBURSEMENT_INPUT_LIMIT: 20, + REFERRAL_PROGRAM: { CONTENT_TYPES: { MONEY_REQUEST: 'request', @@ -3268,6 +3282,12 @@ const CONST = { MOBILE_PAGINATION_SIZE: 15, WEB_PAGINATION_SIZE: 50, + /** Dimensions for illustration shown in Confirmation Modal */ + CONFIRM_CONTENT_SVG_SIZE: { + HEIGHT: 220, + WIDTH: 130, + }, + DEBUG_CONSOLE: { LEVELS: { INFO: 'INFO', @@ -3303,6 +3323,18 @@ const CONST = { ADDRESS: 3, }, }, + + EXIT_SURVEY: { + REASONS: { + FEATURE_NOT_AVAILABLE: 'featureNotAvailable', + DONT_UNDERSTAND: 'dontUnderstand', + PREFER_CLASSIC: 'preferClassic', + }, + }, + + SESSION_STORAGE_KEYS: { + INITIAL_URL: 'INITIAL_URL', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.js b/src/Expensify.tsx similarity index 64% rename from src/Expensify.js rename to src/Expensify.tsx index dfb59a0f8848..f822862ec434 100644 --- a/src/Expensify.js +++ b/src/Expensify.tsx @@ -1,9 +1,8 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import type {NativeEventSubscription} from 'react-native'; import {AppState, Linking} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Onyx, {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import ConfirmModal from './components/ConfirmModal'; import DeeplinkWrapper from './components/DeeplinkWrapper'; import EmojiPicker from './components/EmojiPicker/EmojiPicker'; @@ -12,14 +11,13 @@ import GrowlNotification from './components/GrowlNotification'; import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper'; import SplashScreenHider from './components/SplashScreenHider'; import UpdateAppModal from './components/UpdateAppModal'; -import withLocalize, {withLocalizePropTypes} from './components/withLocalize'; import CONST from './CONST'; +import useLocalize from './hooks/useLocalize'; import * as EmojiPickerAction from './libs/actions/EmojiPickerAction'; import * as Report from './libs/actions/Report'; import * as User from './libs/actions/User'; import * as ActiveClientManager from './libs/ActiveClientManager'; import BootSplash from './libs/BootSplash'; -import compose from './libs/compose'; import * as Growl from './libs/Growl'; import Log from './libs/Log'; import migrateOnyx from './libs/migrateOnyx'; @@ -27,16 +25,18 @@ import Navigation from './libs/Navigation/Navigation'; import NavigationRoot from './libs/Navigation/NavigationRoot'; import NetworkConnection from './libs/NetworkConnection'; import PushNotification from './libs/Notification/PushNotification'; -// eslint-disable-next-line no-unused-vars -import subscribePushNotification from './libs/Notification/PushNotification/subscribePushNotification'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import './libs/Notification/PushNotification/subscribePushNotification'; import StartupTimer from './libs/StartupTimer'; // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection -// eslint-disable-next-line no-unused-vars +// eslint-disable-next-line @typescript-eslint/no-unused-vars import UnreadIndicatorUpdater from './libs/UnreadIndicatorUpdater'; import Visibility from './libs/Visibility'; import ONYXKEYS from './ONYXKEYS'; import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu'; import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu'; +import type {Route} from './ROUTES'; +import type {ScreenShareRequest, Session} from './types/onyx'; Onyx.registerLogger(({level, message}) => { if (level === 'alert') { @@ -47,82 +47,63 @@ Onyx.registerLogger(({level, message}) => { } }); -const propTypes = { - /* Onyx Props */ +type ExpensifyOnyxProps = { + /** Whether the app is waiting for the server's response to determine if a room is public */ + isCheckingPublicRoom: OnyxEntry; /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), + session: OnyxEntry; /** Whether a new update is available and ready to install. */ - updateAvailable: PropTypes.bool, + updateAvailable: OnyxEntry; - /** Tells us if the sidebar has rendered - TODO: We don't use it as temporary solution to fix not hidding splashscreen */ - // eslint-disable-next-line react/no-unused-prop-types - isSidebarLoaded: PropTypes.bool, + /** Tells us if the sidebar has rendered */ + isSidebarLoaded: OnyxEntry; /** Information about a screen share call requested by a GuidesPlus agent */ - screenShareRequest: PropTypes.shape({ - /** Access token required to join a screen share room, generated by the backend */ - accessToken: PropTypes.string, - - /** Name of the screen share room to join */ - roomName: PropTypes.string, - }), - - /** Whether the app is waiting for the server's response to determine if a room is public */ - isCheckingPublicRoom: PropTypes.bool, + screenShareRequest: OnyxEntry; /** True when the user must update to the latest minimum version of the app */ - updateRequired: PropTypes.bool, + updateRequired: OnyxEntry; /** Whether we should display the notification alerting the user that focus mode has been auto-enabled */ - focusModeNotification: PropTypes.bool, + focusModeNotification: OnyxEntry; /** Last visited path in the app */ - lastVisitedPath: PropTypes.string, - - ...withLocalizePropTypes, + lastVisitedPath: OnyxEntry; }; -const defaultProps = { - session: { - authToken: null, - accountID: null, - }, - updateAvailable: false, - isSidebarLoaded: false, - screenShareRequest: null, - isCheckingPublicRoom: true, - updateRequired: false, - focusModeNotification: false, - lastVisitedPath: undefined, -}; +type ExpensifyProps = ExpensifyOnyxProps; const SplashScreenHiddenContext = React.createContext({}); -function Expensify(props) { - const appStateChangeListener = useRef(null); +function Expensify({ + isCheckingPublicRoom = true, + session, + updateAvailable, + isSidebarLoaded = false, + screenShareRequest, + updateRequired = false, + focusModeNotification = false, + lastVisitedPath, +}: ExpensifyProps) { + const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); const [isSplashHidden, setIsSplashHidden] = useState(false); const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false); - const [initialUrl, setInitialUrl] = useState(null); + const {translate} = useLocalize(); + const [initialUrl, setInitialUrl] = useState(null); useEffect(() => { - if (props.isCheckingPublicRoom) { + if (isCheckingPublicRoom) { return; } setAttemptedToOpenPublicRoom(true); - }, [props.isCheckingPublicRoom]); + }, [isCheckingPublicRoom]); - const isAuthenticated = useMemo(() => Boolean(lodashGet(props.session, 'authToken', null)), [props.session]); - const autoAuthState = useMemo(() => lodashGet(props.session, 'autoAuthState', ''), [props.session]); + const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]); + const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]); const contextValue = useMemo( () => ({ @@ -168,8 +149,16 @@ function Expensify(props) { Log.info('[BootSplash] splash screen status', false, {appState, status}); if (status === 'visible') { - const propsToLog = _.omit(props, ['children', 'session']); - propsToLog.isAuthenticated = isAuthenticated; + const propsToLog: Omit = { + isCheckingPublicRoom, + updateRequired, + updateAvailable, + isSidebarLoaded, + screenShareRequest, + focusModeNotification, + isAuthenticated, + lastVisitedPath, + }; Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false); } }); @@ -194,7 +183,7 @@ function Expensify(props) { // If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report Linking.getInitialURL().then((url) => { setInitialUrl(url); - Report.openReportFromDeepLink(url, isAuthenticated); + Report.openReportFromDeepLink(url ?? '', isAuthenticated); }); // Open chat report from a deep link (only mobile native) @@ -216,7 +205,7 @@ function Expensify(props) { return null; } - if (props.updateRequired) { + if (updateRequired) { throw new Error(CONST.ERROR.UPDATE_REQUIRED); } @@ -231,20 +220,19 @@ function Expensify(props) { {/* We include the modal for showing a new update at the top level so the option is always present. */} - {/* If the update is required we won't show this option since a full screen update view will be displayed instead. */} - {props.updateAvailable && !props.updateRequired ? : null} - {props.screenShareRequest ? ( + {updateAvailable && !updateRequired ? : null} + {screenShareRequest ? ( User.joinScreenShare(props.screenShareRequest.accessToken, props.screenShareRequest.roomName)} + title={translate('guides.screenShare')} + onConfirm={() => User.joinScreenShare(screenShareRequest.accessToken, screenShareRequest.roomName)} onCancel={User.clearScreenShareRequest} - prompt={props.translate('guides.screenShareRequest')} - confirmText={props.translate('common.join')} - cancelText={props.translate('common.decline')} + prompt={translate('guides.screenShareRequest')} + confirmText={translate('common.join')} + cancelText={translate('common.decline')} isVisible /> ) : null} - {props.focusModeNotification ? : null} + {focusModeNotification ? : null} )} @@ -254,7 +242,7 @@ function Expensify(props) { @@ -265,40 +253,35 @@ function Expensify(props) { ); } -Expensify.propTypes = propTypes; -Expensify.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - isCheckingPublicRoom: { - key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, - initWithStoredValues: false, - }, - session: { - key: ONYXKEYS.SESSION, - }, - updateAvailable: { - key: ONYXKEYS.UPDATE_AVAILABLE, - initWithStoredValues: false, - }, - isSidebarLoaded: { - key: ONYXKEYS.IS_SIDEBAR_LOADED, - }, - screenShareRequest: { - key: ONYXKEYS.SCREEN_SHARE_REQUEST, - }, - updateRequired: { - key: ONYXKEYS.UPDATE_REQUIRED, - initWithStoredValues: false, - }, - focusModeNotification: { - key: ONYXKEYS.FOCUS_MODE_NOTIFICATION, - initWithStoredValues: false, - }, - lastVisitedPath: { - key: ONYXKEYS.LAST_VISITED_PATH, - }, - }), -)(Expensify); +export default withOnyx({ + isCheckingPublicRoom: { + key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, + initWithStoredValues: false, + }, + session: { + key: ONYXKEYS.SESSION, + }, + updateAvailable: { + key: ONYXKEYS.UPDATE_AVAILABLE, + initWithStoredValues: false, + }, + updateRequired: { + key: ONYXKEYS.UPDATE_REQUIRED, + initWithStoredValues: false, + }, + isSidebarLoaded: { + key: ONYXKEYS.IS_SIDEBAR_LOADED, + }, + screenShareRequest: { + key: ONYXKEYS.SCREEN_SHARE_REQUEST, + }, + focusModeNotification: { + key: ONYXKEYS.FOCUS_MODE_NOTIFICATION, + initWithStoredValues: false, + }, + lastVisitedPath: { + key: ONYXKEYS.LAST_VISITED_PATH, + }, +})(Expensify); export {SplashScreenHiddenContext}; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9d35994875e1..d4a0b8a21d66 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -205,6 +205,9 @@ const ONYXKEYS = { /** Is report data loading? */ IS_LOADING_APP: 'isLoadingApp', + /** Is the user in the process of switching to OldDot? */ + IS_SWITCHING_TO_OLD_DOT: 'isSwitchingToOldDot', + /** Is the test tools modal open? */ IS_TEST_TOOLS_MODAL_OPEN: 'isTestToolsModalOpen', @@ -269,6 +272,9 @@ const ONYXKEYS = { /** Indicates whether we should store logs or not */ SHOULD_STORE_LOGS: 'shouldStoreLogs', + // Paths of PDF file that has been cached during one session + CACHED_PDF_PATHS: 'cachedPDFPaths', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -279,7 +285,6 @@ const ONYXKEYS = { POLICY_CATEGORIES: 'policyCategories_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', - POLICY_TAX_RATE: 'policyTaxRates_', POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', POLICY_REPORT_FIELDS: 'policyReportFields_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', @@ -388,6 +393,10 @@ const ONYXKEYS = { REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm', PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', + EXIT_SURVEY_REASON_FORM: 'exitSurveyReasonForm', + EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft', + EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm', + EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft', }, } as const; @@ -396,36 +405,37 @@ type AllOnyxKeys = DeepValueOf; type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; - [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceProfileDescriptionForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; - [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.Form; + [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: FormTypes.RoomNameForm; - [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: FormTypes.Form; + [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.ReportDescriptionForm; + [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.LegalNameForm; + [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: FormTypes.WorkspaceInviteMessageForm; [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: FormTypes.DateOfBirthForm; - [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: FormTypes.Form; + [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: FormTypes.HomeAddressForm; [ONYXKEYS.FORMS.NEW_ROOM_FORM]: FormTypes.NewRoomForm; - [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.EDIT_TASK_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: FormTypes.Form; + [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.RoomSettingsForm; + [ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.NewTaskForm; + [ONYXKEYS.FORMS.EDIT_TASK_FORM]: FormTypes.EditTaskForm; + [ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM]: FormTypes.ExitSurveyReasonForm; + [ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM]: FormTypes.ExitSurveyResponseForm; + [ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM]: FormTypes.MoneyRequestDescriptionForm; + [ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM]: FormTypes.MoneyRequestMerchantForm; + [ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: FormTypes.MoneyRequestAmountForm; + [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: FormTypes.MoneyRequestDateForm; [ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM]: FormTypes.MoneyRequestHoldReasonForm; - [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: FormTypes.Form; + [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.NewContactMethodForm; + [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.WaypointForm; + [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.SettingsStatusSetForm; + [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: FormTypes.SettingsStatusClearDateForm; + [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: FormTypes.SettingsStatusSetClearAfterForm; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: FormTypes.PrivateNotesForm; [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: FormTypes.IKnowTeacherForm; [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: FormTypes.IntroSchoolPrincipalForm; - [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.Form; - [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.Form; + [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.ReportVirtualCardFraudForm; + [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.ReportPhysicalCardForm; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: FormTypes.GetPhysicalCardForm; [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: FormTypes.ReportFieldEditForm; [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; @@ -469,7 +479,6 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; - [ONYXKEYS.COLLECTION.POLICY_TAX_RATE]: string[]; }; type OnyxValuesMapping = { @@ -491,7 +500,7 @@ type OnyxValuesMapping = { [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: OnyxTypes.PrivatePersonalDetails; [ONYXKEYS.TASK]: OnyxTypes.Task; [ONYXKEYS.WORKSPACE_RATE_AND_UNIT]: OnyxTypes.WorkspaceRateAndUnit; - [ONYXKEYS.CURRENCY_LIST]: Record; + [ONYXKEYS.CURRENCY_LIST]: OnyxTypes.CurrencyList; [ONYXKEYS.UPDATE_AVAILABLE]: boolean; [ONYXKEYS.SCREEN_SHARE_REQUEST]: OnyxTypes.ScreenShareRequest; [ONYXKEYS.COUNTRY_CODE]: number; @@ -535,6 +544,7 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean; [ONYXKEYS.IS_LOADING_APP]: boolean; + [ONYXKEYS.IS_SWITCHING_TO_OLD_DOT]: boolean; [ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer; [ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string; [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean; @@ -557,6 +567,7 @@ type OnyxValuesMapping = { [ONYXKEYS.PLAID_CURRENT_EVENT]: string; [ONYXKEYS.LOGS]: Record; [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; + [ONYXKEYS.CACHED_PDF_PATHS]: Record; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; @@ -575,4 +586,4 @@ type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: type AssertOnyxKeys = AssertTypesEqual; export default ONYXKEYS; -export type {OnyxValues, OnyxKey, OnyxCollectionKey, OnyxValue, OnyxValueKey, OnyxFormKey, OnyxFormValuesMapping, OnyxFormDraftKey}; +export type {OnyxValues, OnyxKey, OnyxCollectionKey, OnyxValue, OnyxValueKey, OnyxFormKey, OnyxFormValuesMapping, OnyxFormDraftKey, OnyxCollectionValuesMapping}; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 559c49ff3e2e..22ebffd52eec 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', @@ -85,28 +84,28 @@ const ROUTES = { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { - route: '/settings/wallet/card/:domain', - getRoute: (domain: string) => `/settings/wallet/card/${domain}` as const, + route: 'settings/wallet/card/:domain', + getRoute: (domain: string) => `settings/wallet/card/${domain}` as const, }, SETTINGS_REPORT_FRAUD: { - route: '/settings/wallet/card/:domain/report-virtual-fraud', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-virtual-fraud` as const, + route: 'settings/wallet/card/:domain/report-virtual-fraud', + getRoute: (domain: string) => `settings/wallet/card/${domain}/report-virtual-fraud` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: { - route: '/settings/wallet/card/:domain/get-physical/name', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/name` as const, + route: 'settings/wallet/card/:domain/get-physical/name', + getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/name` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE: { - route: '/settings/wallet/card/:domain/get-physical/phone', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/phone` as const, + route: 'settings/wallet/card/:domain/get-physical/phone', + getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/phone` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS: { - route: '/settings/wallet/card/:domain/get-physical/address', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/address` as const, + route: 'settings/wallet/card/:domain/get-physical/address', + getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/address` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_CONFIRM: { - route: '/settings/wallet/card/:domain/get-physical/confirm', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/confirm` as const, + route: 'settings/wallet/card/:domain/get-physical/confirm', + getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/confirm` as const, }, SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', @@ -118,8 +117,8 @@ const ROUTES = { SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { - route: '/settings/wallet/card/:domain/report-card-lost-or-damaged', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-card-lost-or-damaged` as const, + route: 'settings/wallet/card/:domain/report-card-lost-or-damaged', + getRoute: (domain: string) => `settings/wallet/card/${domain}/report-card-lost-or-damaged` as const, }, SETTINGS_WALLET_CARD_ACTIVATE: { route: 'settings/wallet/card/:domain/activate', @@ -160,6 +159,17 @@ const ROUTES = { getRoute: (source: string) => `settings/troubleshoot/console/share-log?source=${encodeURI(source)}` as const, }, + SETTINGS_EXIT_SURVEY_REASON: 'settings/exit-survey/reason', + SETTINGS_EXIT_SURVEY_RESPONSE: { + route: 'settings/exit-survey/response', + getRoute: (reason?: ValueOf, backTo?: string) => + getUrlWithBackToParam(`settings/exit-survey/response${reason ? `?reason=${encodeURIComponent(reason)}` : ''}`, backTo), + }, + SETTINGS_EXIT_SURVEY_CONFIRM: { + route: 'settings/exit-survey/confirm', + getRoute: (backTo?: string) => getUrlWithBackToParam('settings/exit-survey/confirm', backTo), + }, + KEYBOARD_SHORTCUTS: 'keyboard-shortcuts', NEW: 'new', @@ -220,6 +230,10 @@ const ROUTES = { route: 'r/:reportID/settings/who-can-post', getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` as const, }, + REPORT_SETTINGS_VISIBILITY: { + route: 'r/:reportID/settings/visibility', + getRoute: (reportID: string) => `r/${reportID}/settings/visibility` as const, + }, SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const, @@ -284,10 +298,6 @@ const ROUTES = { route: ':iouType/new/currency/:reportID?', getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}` as const, }, - MONEY_REQUEST_CATEGORY: { - route: ':iouType/new/category/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}` as const, - }, MONEY_REQUEST_HOLD_REASON: { route: ':iouType/edit/reason/:transactionID?', getRoute: (iouType: string, transactionID: string, reportID: string, backTo: string) => `${iouType}/edit/reason/${transactionID}?backTo=${backTo}&reportID=${reportID}` as const, @@ -335,9 +345,9 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/taxAmount/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_CATEGORY: { - route: 'create/:iouType/category/:transactionID/:reportID', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/category/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/category/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/category/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_CURRENCY: { route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?', @@ -460,6 +470,10 @@ const ROUTES = { route: 'workspace/:policyID/profile/description', getRoute: (policyID: string) => `workspace/${policyID}/profile/description` as const, }, + WORKSPACE_PROFILE_SHARE: { + route: 'workspace/:policyID/profile/share', + getRoute: (policyID: string) => `workspace/${policyID}/profile/share` as const, + }, WORKSPACE_AVATAR: { route: 'workspace/:policyID/avatar', getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const, @@ -468,6 +482,10 @@ const ROUTES = { route: 'workspace/:policyID/settings/currency', getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, }, + WORKSPACE_WORKFLOWS: { + route: 'workspace/:policyID/workflows', + getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const, + }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', getRoute: (policyID: string) => `workspace/${policyID}/card` as const, @@ -504,10 +522,19 @@ const ROUTES = { route: 'workspace/:policyID/members', getRoute: (policyID: string) => `workspace/${policyID}/members` as const, }, + WORKSPACE_CATEGORIES: { + route: 'workspace/:policyID/categories', + getRoute: (policyID: string) => `workspace/${policyID}/categories` as const, + }, + WORKSPACE_CATEGORIES_SETTINGS: { + route: 'workspace/:policyID/categories/settings', + getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, + }, + // 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; @@ -543,4 +570,4 @@ type Route = RouteIsPlainString extends true ? never : AllRoutes; type HybridAppRoute = (typeof HYBRID_APP_ROUTES)[keyof typeof HYBRID_APP_ROUTES]; -export type {Route, HybridAppRoute}; +export type {Route, HybridAppRoute, AllRoutes}; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index cfb2fe1725d2..ac75968e68b9 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', @@ -81,6 +80,12 @@ const SCREENS = { REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', }, + + EXIT_SURVEY: { + REASON: 'Settings_ExitSurvey_Reason', + RESPONSE: 'Settings_ExitSurvey_Response', + CONFIRM: 'Settings_ExitSurvey_Confirm', + }, }, SAVE_THE_WORLD: { ROOT: 'SaveTheWorld_Root', @@ -150,7 +155,6 @@ const SCREENS = { PARTICIPANTS: 'Money_Request_Participants', CONFIRMATION: 'Money_Request_Confirmation', CURRENCY: 'Money_Request_Currency', - CATEGORY: 'Money_Request_Category', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', DISTANCE: 'Money_Request_Distance', @@ -168,6 +172,7 @@ const SCREENS = { ROOM_NAME: 'Report_Settings_Room_Name', NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', WRITE_CAPABILITY: 'Report_Settings_Write_Capability', + VISIBILITY: 'Report_Settings_Visibility', }, NEW_TASK: { @@ -208,9 +213,13 @@ const SCREENS = { MEMBERS: 'Workspace_Members', INVITE: 'Workspace_Invite', INVITE_MESSAGE: 'Workspace_Invite_Message', + CATEGORIES: 'Workspace_Categories', CURRENCY: 'Workspace_Profile_Currency', + WORKFLOWS: 'Workspace_Workflows', DESCRIPTION: 'Workspace_Profile_Description', + SHARE: 'Workspace_Profile_Share', NAME: 'Workspace_Profile_Name', + CATEGORIES_SETTINGS: 'Categories_Settings', }, EDIT_REQUEST: { 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/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 89e87eeebe54..4f21f4aa1dc3 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -271,8 +271,9 @@ function AddressSearch( }; const renderHeaderComponent = () => ( + // eslint-disable-next-line react/jsx-no-useless-fragment <> - {predefinedPlaces.length > 0 && ( + {(predefinedPlaces?.length ?? 0) > 0 && ( <> {/* This will show current location button in list if there are some recent destinations */} {shouldShowCurrentLocationButton && ( @@ -339,7 +340,7 @@ function AddressSearch( fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={predefinedPlaces} + predefinedPlaces={predefinedPlaces ?? undefined} listEmptyComponent={listEmptyComponent} listLoaderComponent={listLoader} renderHeaderComponent={renderHeaderComponent} @@ -348,7 +349,7 @@ function AddressSearch( const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text; return ( - {title && {title}} + {!!title && {title}} {subtitle} ); @@ -398,10 +399,10 @@ function AddressSearch( if (inputID) { onInputChange?.(text); } else { - onInputChange({street: text}); + onInputChange?.({street: text}); } // If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering - if (!text && !predefinedPlaces.length) { + if (!text && !predefinedPlaces?.length) { setDisplayListViewBorder(false); } }, diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 9b4254a9bc45..e4735e9d0020 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -20,6 +20,8 @@ type RenamedInputKeysProps = { lat: string; lng: string; zipCode: string; + address?: string; + country?: string; }; type OnPressProps = { @@ -59,7 +61,7 @@ type AddressSearchProps = { defaultValue?: string; /** A callback function when the value of this field has changed */ - onInputChange: (value: string | number | RenamedInputKeysProps | StreetValue, key?: string) => void; + onInputChange?: (value: string | number | RenamedInputKeysProps | StreetValue, key?: string) => void; /** A callback function when an address has been auto-selected */ onPress?: (props: OnPressProps) => void; @@ -74,10 +76,10 @@ type AddressSearchProps = { canUseCurrentLocation?: boolean; /** A list of predefined places that can be shown when the user isn't searching for something */ - predefinedPlaces?: Place[]; + predefinedPlaces?: Place[] | null; /** A map of inputID key names */ - renamedInputKeys: RenamedInputKeysProps; + renamedInputKeys?: RenamedInputKeysProps; /** Maximum number of characters allowed in search input */ maxInputLength?: number; diff --git a/src/components/Alert/index.js b/src/components/Alert/index.js deleted file mode 100644 index 3fc90433f13e..000000000000 --- a/src/components/Alert/index.js +++ /dev/null @@ -1,24 +0,0 @@ -import _ from 'underscore'; - -/** - * Shows an alert modal with ok and cancel options. - * - * @param {String} title The title of the alert - * @param {String} description The description of the alert - * @param {Object[]} [options] An array of objects with `style` and `onPress` properties - */ -export default (title, description, options) => { - const result = _.filter(window.confirm([title, description], Boolean)).join('\n'); - - if (result) { - const confirmOption = _.find(options, ({style}) => style !== 'cancel'); - if (confirmOption && confirmOption.onPress) { - confirmOption.onPress(); - } - } else { - const cancelOption = _.find(options, ({style}) => style === 'cancel'); - if (cancelOption && cancelOption.onPress) { - cancelOption.onPress(); - } - } -}; diff --git a/src/components/Alert/index.native.js b/src/components/Alert/index.native.js deleted file mode 100644 index 31c837a7dd6b..000000000000 --- a/src/components/Alert/index.native.js +++ /dev/null @@ -1,3 +0,0 @@ -import {Alert} from 'react-native'; - -export default Alert.alert; diff --git a/src/components/Alert/index.native.tsx b/src/components/Alert/index.native.tsx new file mode 100644 index 000000000000..b72eff5d9b58 --- /dev/null +++ b/src/components/Alert/index.native.tsx @@ -0,0 +1,6 @@ +import {Alert as AlertRN} from 'react-native'; +import type Alert from './types'; + +const alert: Alert = AlertRN.alert; + +export default alert; diff --git a/src/components/Alert/index.tsx b/src/components/Alert/index.tsx new file mode 100644 index 000000000000..f212f06aa9d3 --- /dev/null +++ b/src/components/Alert/index.tsx @@ -0,0 +1,16 @@ +import type Alert from './types'; + +/** Shows an alert modal with ok and cancel options. */ +const alert: Alert = (title, description, options) => { + const result = window.confirm([title, description].filter(Boolean).join('\n')); + + if (result) { + const confirmOption = options?.find(({style}) => style !== 'cancel'); + confirmOption?.onPress?.(); + } else { + const cancelOption = options?.find(({style}) => style === 'cancel'); + cancelOption?.onPress?.(); + } +}; + +export default alert; diff --git a/src/components/Alert/types.ts b/src/components/Alert/types.ts new file mode 100644 index 000000000000..25454abfe8b8 --- /dev/null +++ b/src/components/Alert/types.ts @@ -0,0 +1,5 @@ +import type {AlertStatic} from 'react-native'; + +type Alert = AlertStatic['alert']; + +export default Alert; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index ab39e5379230..7f0178863fc9 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -89,7 +89,7 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { source?: AvatarSource; /** Optional callback to fire when we want to preview an image and approve it for use. */ - onConfirm?: ((file: Partial) => void) | null; + onConfirm?: ((file: FileObject) => void) | null; /** Whether the modal should be open by default */ defaultOpen?: boolean; @@ -264,7 +264,7 @@ function AttachmentModal({ } if (onConfirm) { - onConfirm(Object.assign(file ?? {}, {source: sourceState})); + onConfirm(Object.assign(file ?? {}, {source: sourceState} as FileObject)); } setIsModalOpen(false); @@ -318,7 +318,7 @@ function AttachmentModal({ const validateAndDisplayFileToUpload = useCallback( (data: FileObject) => { - if (!isDirectoryCheck(data)) { + if (!data || !isDirectoryCheck(data)) { return; } let fileObject = data; @@ -617,4 +617,4 @@ export default withOnyx({ }, })(memo(AttachmentModal)); -export type {Attachment}; +export type {Attachment, FileObject}; diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index 59928b80c4b1..0387ee087127 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -1,7 +1,8 @@ +import Str from 'expensify-common/lib/str'; import lodashCompact from 'lodash/compact'; import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef, useState} from 'react'; -import {Alert, View} from 'react-native'; +import {Alert, Image as RNImage, View} from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; import RNDocumentPicker from 'react-native-document-picker'; import {launchImageLibrary} from 'react-native-image-picker'; @@ -57,11 +58,22 @@ const getImagePickerOptions = (type) => { }; /** - * See https://github.com/rnmods/react-native-document-picker#options for DocumentPicker configuration options + * Return documentPickerOptions based on the type + * @param {String} type + * @returns {Object} */ -const documentPickerOptions = { - type: [RNDocumentPicker.types.allFiles], - copyTo: 'cachesDirectory', + +const getDocumentPickerOptions = (type) => { + if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) { + return { + type: [RNDocumentPicker.types.images], + copyTo: 'cachesDirectory', + }; + } + return { + type: [RNDocumentPicker.types.allFiles], + copyTo: 'cachesDirectory', + }; }; /** @@ -158,7 +170,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { */ const showDocumentPicker = useCallback( () => - RNDocumentPicker.pick(documentPickerOptions).catch((error) => { + RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error) => { if (RNDocumentPicker.isCancel(error)) { return; } @@ -166,7 +178,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { showGeneralAlert(error.message); throw error; }), - [showGeneralAlert], + [showGeneralAlert, type], ); const menuItemData = useMemo(() => { @@ -181,7 +193,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { textTranslationKey: 'attachmentPicker.chooseFromGallery', pickAttachment: () => showImagePicker(launchImageLibrary), }, - type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE && { + { icon: Expensicons.Paperclip, textTranslationKey: 'attachmentPicker.chooseDocument', pickAttachment: showDocumentPicker, @@ -189,7 +201,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { ]); return data; - }, [showDocumentPicker, showImagePicker, type, shouldHideCameraOption]); + }, [showDocumentPicker, showImagePicker, shouldHideCameraOption]); const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItemData.length - 1, isActive: isVisible}); @@ -220,26 +232,15 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { }; /** - * Handles the image/document picker result and - * sends the selected attachment to the caller (parent component) - * - * @param {Array} attachments + * @param {Object} fileData * @returns {Promise} */ - const pickAttachment = useCallback( - (attachments = []) => { - if (attachments.length === 0) { - onCanceled.current(); - return Promise.resolve(); - } - - const fileData = _.first(attachments); - + const validateAndCompleteAttachmentSelection = useCallback( + (fileData) => { if (fileData.width === -1 || fileData.height === -1) { showImageCorruptionAlert(); return Promise.resolve(); } - return getDataForUpload(fileData) .then((result) => { completeAttachmentSelection.current(result); @@ -252,6 +253,33 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { [showGeneralAlert, showImageCorruptionAlert], ); + /** + * Handles the image/document picker result and + * sends the selected attachment to the caller (parent component) + * + * @param {Array} attachments + * @returns {Promise} + */ + const pickAttachment = useCallback( + (attachments = []) => { + if (attachments.length === 0) { + onCanceled.current(); + return Promise.resolve(); + } + const fileData = _.first(attachments); + if (Str.isImage(fileData.fileName || fileData.name)) { + RNImage.getSize(fileData.fileCopyUri || fileData.uri, (width, height) => { + fileData.width = width; + fileData.height = height; + return validateAndCompleteAttachmentSelection(fileData); + }); + } else { + return validateAndCompleteAttachmentSelection(fileData); + } + }, + [validateAndCompleteAttachmentSelection], + ); + /** * Setup native attachment selection to start after this popover closes * diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index edc8ab11fd27..b2c9fed64467 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -105,6 +105,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) { isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} transactionID={item.transactionID} + reportActionID={item.reportActionID} isHovered={isModalHovered} isFocused={isFocused} optionalVideoDuration={item.duration} diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index fd9b57511cc4..f1b9d16de654 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -26,6 +26,7 @@ type AttachmentCarouselPagerContextValue = { isScrollEnabled: SharedValue; onTap: () => void; onScaleChanged: (scale: number) => void; + onSwipeDown: () => void; }; const AttachmentCarouselPagerContext = createContext(null); diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 8704584c3e18..33d9f20b5e57 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -42,9 +42,15 @@ type AttachmentCarouselPagerProps = { * @param showArrows If set, it will show/hide the arrows. If not set, it will toggle the arrows. */ onRequestToggleArrows: (showArrows?: boolean) => void; + + /** A callback that is called when swipe-down-to-close gesture happens */ + onClose: () => void; }; -function AttachmentCarouselPager({items, activeSource, initialPage, onPageSelected, onRequestToggleArrows}: AttachmentCarouselPagerProps, ref: ForwardedRef) { +function AttachmentCarouselPager( + {items, activeSource, initialPage, onPageSelected, onRequestToggleArrows, onClose}: AttachmentCarouselPagerProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); const pagerRef = useRef(null); @@ -114,9 +120,10 @@ function AttachmentCarouselPager({items, activeSource, initialPage, onPageSelect isScrollEnabled, pagerRef, onTap: handleTap, + onSwipeDown: onClose, onScaleChanged: handleScaleChange, }), - [pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange], + [pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, handleTap, onClose, handleScaleChange], ); const animatedProps = useAnimatedProps(() => ({ diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 228f0d597a32..ed618151e594 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -102,46 +102,56 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [setShouldShowArrows], ); - return ( - - {page == null ? ( + const goBack = useCallback(() => { + Navigation.goBack(); + }, []); + + const containerStyles = [styles.flex1, styles.attachmentCarouselContainer]; + + if (page == null) { + return ( + + + ); + } + + return ( + + {page === -1 ? ( + ) : ( <> - {page === -1 ? ( - - ) : ( - <> - cycleThroughAttachments(-1)} - onForward={() => cycleThroughAttachments(1)} - autoHideArrow={autoHideArrows} - cancelAutoHideArrow={cancelAutoHideArrows} - /> - - updatePage(newPage)} - ref={pagerRef} - /> - - )} + cycleThroughAttachments(-1)} + onForward={() => cycleThroughAttachments(1)} + autoHideArrow={autoHideArrows} + cancelAutoHideArrow={cancelAutoHideArrows} + /> + + updatePage(newPage)} + onClose={goBack} + ref={pagerRef} + /> )} ); } + AttachmentCarousel.propTypes = propTypes; AttachmentCarousel.defaultProps = defaultProps; AttachmentCarousel.displayName = 'AttachmentCarousel'; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index d02957aa8137..f6a56dc73088 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -17,6 +17,7 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import compose from '@libs/compose'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -57,6 +58,9 @@ const propTypes = { // eslint-disable-next-line react/no-unused-prop-types transactionID: PropTypes.string, + /** The id of the report action related to the attachment */ + reportActionID: PropTypes.string, + isHovered: PropTypes.bool, optionalVideoDuration: PropTypes.number, @@ -71,6 +75,7 @@ const defaultProps = { isWorkspaceAvatar: false, maybeIcon: false, transactionID: '', + reportActionID: '', isHovered: false, optionalVideoDuration: 0, }; @@ -92,6 +97,7 @@ function AttachmentView({ maybeIcon, fallbackSource, transaction, + reportActionID, isHovered, optionalVideoDuration, }) { @@ -100,14 +106,14 @@ 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)) || (file && Str.isVideo(file.name)); useEffect(() => { - if (!isFocused) { + if (!isFocused && !(file && isUsedInAttachmentModal)) { return; } updateCurrentlyPlayingURL(isVideo ? source : null); - }, [isFocused, isVideo, source, updateCurrentlyPlayingURL]); + }, [isFocused, isVideo, source, updateCurrentlyPlayingURL, file, isUsedInAttachmentModal]); const [imageError, setImageError] = useState(false); @@ -153,6 +159,16 @@ function AttachmentView({ if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; + const onPDFLoadComplete = (path) => { + const id = (transaction && transaction.transactionID) || reportActionID; + if (path && id) { + CachedPDFPaths.add(id, path); + } + if (!loadComplete) { + setLoadComplete(true); + } + }; + // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -166,7 +182,7 @@ function AttachmentView({ encryptedSourceUrl={encryptedSourceUrl} onPress={onPress} onToggleKeyboard={onToggleKeyboard} - onLoadComplete={() => !loadComplete && setLoadComplete(true)} + onLoadComplete={onPDFLoadComplete} errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} @@ -201,7 +217,7 @@ function AttachmentView({ ); } - if (isVideo || (file && Str.isVideo(file.name))) { + if (isVideo) { return ( ; +}; + +type OpenPickerParams = { + onPicked: (image: File) => void; +}; +type OpenPicker = (args: OpenPickerParams) => void; + +type MenuItem = { + icon: IconAsset; + text: string; + onSelected: () => void; +}; + +type AvatarWithImagePickerProps = { /** Avatar source to display */ - source: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), + source?: AvatarSource; /** Additional style props */ - style: stylePropTypes, + style?: StyleProp; /** Additional style props for disabled picker */ - disabledStyle: stylePropTypes, + disabledStyle?: StyleProp; /** Executed once an image has been selected */ - onImageSelected: PropTypes.func, + onImageSelected?: (file: File | CustomRNImageManipulatorResult) => void; /** Execute when the user taps "remove" */ - onImageRemoved: PropTypes.func, + onImageRemoved?: () => void; /** A default avatar component to display when there is no source */ - DefaultAvatar: PropTypes.func, + DefaultAvatar?: () => React.ReactNode; /** Whether we are using the default avatar */ - isUsingDefaultAvatar: PropTypes.bool, + isUsingDefaultAvatar?: boolean; /** Size of Indicator */ - size: PropTypes.oneOf([CONST.AVATAR_SIZE.XLARGE, CONST.AVATAR_SIZE.LARGE, CONST.AVATAR_SIZE.DEFAULT]), + size?: typeof CONST.AVATAR_SIZE.XLARGE | typeof CONST.AVATAR_SIZE.LARGE | typeof CONST.AVATAR_SIZE.DEFAULT; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: sourcePropTypes, + fallbackIcon?: AvatarSource; /** Denotes whether it is an avatar or a workspace avatar */ - type: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_WORKSPACE]), + type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; /** Image crop vector mask */ - editorMaskImage: sourcePropTypes, + editorMaskImage?: IconAsset; /** Additional style object for the error row */ - errorRowStyles: stylePropTypes, + errorRowStyles?: StyleProp; /** A function to run when the X button next to the error is clicked */ - onErrorClose: PropTypes.func, + onErrorClose?: () => void; /** The type of action that's pending */ - pendingAction: PropTypes.oneOf(['add', 'update', 'delete']), + pendingAction?: OnyxCommon.PendingAction; /** The errors to display */ - // eslint-disable-next-line react/forbid-prop-types - errors: PropTypes.object, + errors?: OnyxCommon.Errors | null; /** Title for avatar preview modal */ - headerTitle: PropTypes.string, + headerTitle?: string; /** Avatar source for avatar preview modal */ - previewSource: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), + previewSource?: AvatarSource; /** File name of the avatar */ - originalFileName: PropTypes.string, + originalFileName?: string; /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, + isFocused: boolean; /** Style applied to the avatar */ - avatarStyle: stylePropTypes.isRequired, + avatarStyle: StyleProp; /** Indicates if picker feature should be disabled */ - disabled: PropTypes.bool, + disabled?: boolean; /** Executed once click on view photo option */ - onViewPhotoPress: PropTypes.func, - - /** Where the popover should be positioned relative to the anchor points. */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), + onViewPhotoPress?: () => void; /** Allows to open an image without Attachment Picker. */ - enablePreview: PropTypes.bool, -}; - -const defaultProps = { - source: '', - onImageSelected: () => {}, - onImageRemoved: () => {}, - style: [], - disabledStyle: [], - DefaultAvatar: () => {}, - isUsingDefaultAvatar: false, - size: CONST.AVATAR_SIZE.DEFAULT, - fallbackIcon: Expensicons.FallbackAvatar, - type: CONST.ICON_TYPE_AVATAR, - editorMaskImage: undefined, - errorRowStyles: [], - onErrorClose: () => {}, - pendingAction: null, - errors: null, - headerTitle: '', - previewSource: '', - originalFileName: '', - disabled: false, - onViewPhotoPress: undefined, - anchorAlignment: { - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, - }, - enablePreview: false, + enablePreview?: boolean; }; function AvatarWithImagePicker({ isFocused, - DefaultAvatar, + DefaultAvatar = () => null, style, disabledStyle, pendingAction, errors, errorRowStyles, - onErrorClose, - source, - fallbackIcon, - size, - type, - headerTitle, - previewSource, - originalFileName, - isUsingDefaultAvatar, - onImageRemoved, - onImageSelected, + onErrorClose = () => {}, + source = '', + fallbackIcon = Expensicons.FallbackAvatar, + size = CONST.AVATAR_SIZE.DEFAULT, + type = CONST.ICON_TYPE_AVATAR, + headerTitle = '', + previewSource = '', + originalFileName = '', + isUsingDefaultAvatar = false, + onImageSelected = () => {}, + onImageRemoved = () => {}, editorMaskImage, avatarStyle, - disabled, + disabled = false, onViewPhotoPress, - enablePreview, -}) { + enablePreview = false, +}: AvatarWithImagePickerProps) { const theme = useTheme(); const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0}); const [isMenuVisible, setIsMenuVisible] = useState(false); - const [errorData, setErrorData] = useState({ - validationError: null, - phraseParam: {}, - }); + const [errorData, setErrorData] = useState({validationError: null, phraseParam: {}}); const [isAvatarCropModalOpen, setIsAvatarCropModalOpen] = useState(false); const [imageData, setImageData] = useState({ uri: '', name: '', type: '', }); - const anchorRef = useRef(); + const anchorRef = useRef(null); const {translate} = useLocalize(); - /** - * @param {String} error - * @param {Object} phraseParam - */ - const setError = (error, phraseParam) => { + const setError = (error: TranslationPaths | null, phraseParam: Record) => { setErrorData({ validationError: error, phraseParam, @@ -198,40 +173,29 @@ function AvatarWithImagePicker({ /** * Check if the attachment extension is allowed. - * - * @param {Object} image - * @returns {Boolean} */ - const isValidExtension = (image) => { - const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(image, 'name', '')); - return _.contains(CONST.AVATAR_ALLOWED_EXTENSIONS, fileExtension.toLowerCase()); + const isValidExtension = (image: File): boolean => { + const {fileExtension} = FileUtils.splitExtensionFromFileName(image?.name ?? ''); + return CONST.AVATAR_ALLOWED_EXTENSIONS.some((extension) => extension === fileExtension.toLowerCase()); }; /** * Check if the attachment size is less than allowed size. - * - * @param {Object} image - * @returns {Boolean} */ - const isValidSize = (image) => image && lodashGet(image, 'size', 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE; + const isValidSize = (image: File): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE; /** * Check if the attachment resolution matches constraints. - * - * @param {Object} image - * @returns {Promise} */ - const isValidResolution = (image) => + const isValidResolution = (image: File): Promise => getImageResolution(image).then( ({height, width}) => height >= CONST.AVATAR_MIN_HEIGHT_PX && width >= CONST.AVATAR_MIN_WIDTH_PX && height <= CONST.AVATAR_MAX_HEIGHT_PX && width <= CONST.AVATAR_MAX_WIDTH_PX, ); /** * Validates if an image has a valid resolution and opens an avatar crop modal - * - * @param {Object} image */ - const showAvatarCropModal = (image) => { + const showAvatarCropModal = (image: File) => { if (!isValidExtension(image)) { setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS}); return; @@ -256,7 +220,7 @@ function AvatarWithImagePicker({ setError(null, {}); setIsMenuVisible(false); setImageData({ - uri: image.uri, + uri: image.uri ?? '', name: image.name, type: image.type, }); @@ -269,11 +233,8 @@ function AvatarWithImagePicker({ /** * Create menu items list for avatar menu - * - * @param {Function} openPicker - * @returns {Array} */ - const createMenuItems = (openPicker) => { + const createMenuItems = (openPicker: OpenPicker): MenuItem[] => { const menuItems = [ { icon: Expensicons.Upload, @@ -318,6 +279,7 @@ function AvatarWithImagePicker({ vertical: y + height + variables.spacing2, }); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMenuVisible, windowWidth]); @@ -383,7 +345,11 @@ function AvatarWithImagePicker({ maybeIcon={isUsingDefaultAvatar} > {({show}) => ( - + + {/* @ts-expect-error TODO: Remove this once AttachmentPicker (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} {({openPicker}) => { const menuItems = createMenuItems(openPicker); @@ -432,7 +398,8 @@ function AvatarWithImagePicker({ {errorData.validationError && ( )} @@ -449,8 +416,6 @@ function AvatarWithImagePicker({ ); } -AvatarWithImagePicker.propTypes = propTypes; -AvatarWithImagePicker.defaultProps = defaultProps; AvatarWithImagePicker.displayName = 'AvatarWithImagePicker'; export default withNavigationFocus(AvatarWithImagePicker); diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx index edec30604b88..7b33c8054950 100644 --- a/src/components/BlockingViews/BlockingView.tsx +++ b/src/components/BlockingViews/BlockingView.tsx @@ -85,13 +85,15 @@ function BlockingView({ width={iconWidth} height={iconHeight} /> - {title} + + {title} - {shouldEmbedLinkWithSubtitle ? ( - {renderContent()} - ) : ( - {renderContent()} - )} + {shouldEmbedLinkWithSubtitle ? ( + {renderContent()} + ) : ( + {renderContent()} + )} + ); } diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js index a957e31a9de4..89312a7ca614 100644 --- a/src/components/CategoryPicker/index.js +++ b/src/components/CategoryPicker/index.js @@ -1,22 +1,19 @@ 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 RadioListItem from '@components/SelectionList/RadioListItem'; +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 +25,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 +47,30 @@ 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/Composer/index.tsx b/src/components/Composer/index.tsx index 516de55c73ba..b6443f3ca385 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -75,7 +75,7 @@ function Composer( shouldContainScroll = false, ...props }: ComposerProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -278,6 +278,7 @@ function Composer( if (!onKeyPress || isEnterWhileComposition(e as unknown as KeyboardEvent)) { return; } + onKeyPress(e); }, [onKeyPress], diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index d8d88970ea78..6bc44aba69cd 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,11 +1,11 @@ -import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; +import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; type TextSelection = { start: number; end?: number; }; -type ComposerProps = { +type ComposerProps = TextInputProps & { /** identify id in the text input */ id?: string; @@ -31,7 +31,7 @@ type ComposerProps = { onNumberOfLinesChange?: (numberOfLines: number) => void; /** Callback method to handle pasting a file */ - onPasteFile?: (file?: File) => void; + onPasteFile?: (file: File) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types @@ -74,12 +74,6 @@ type ComposerProps = { /** Whether the sull composer is open */ isComposerFullSize?: boolean; - onKeyPress?: (event: NativeSyntheticEvent) => void; - - onFocus?: (event: NativeSyntheticEvent) => void; - - onBlur?: (event: NativeSyntheticEvent) => void; - /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; }; diff --git a/src/components/ConfirmContent.tsx b/src/components/ConfirmContent.tsx index 94146a2c2957..c0c146bbae08 100644 --- a/src/components/ConfirmContent.tsx +++ b/src/components/ConfirmContent.tsx @@ -4,13 +4,17 @@ import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import colors from '@styles/theme/colors'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import Header from './Header'; import Icon from './Icon'; +import ImageSVG from './ImageSVG'; import Text from './Text'; type ConfirmContentProps = { @@ -64,6 +68,9 @@ type ConfirmContentProps = { /** Styles for icon */ iconAdditionalStyles?: StyleProp; + + /** Image to display with content */ + image?: IconAsset; }; function ConfirmContent({ @@ -84,79 +91,95 @@ function ConfirmContent({ promptStyles, contentStyles, iconAdditionalStyles, + image, }: ConfirmContentProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const theme = useTheme(); const {isOffline} = useNetwork(); + const StyleUtils = useStyleUtils(); const isCentered = shouldCenterContent; return ( - - - {typeof iconSource === 'function' && ( - - + {!!image && ( + + + + )} + + + + {typeof iconSource === 'function' && ( + + + + )} + +
- )} - -
+ {typeof prompt === 'string' ? {prompt} : prompt} - {typeof prompt === 'string' ? {prompt} : prompt} - - {shouldStackButtons ? ( - <> -